Parametrizasi Pytest

Selesai

Dekorator @pytest.mark.parametrize() memungkinkan satu fungsi pengujian berjalan beberapa kali dengan nilai input yang berbeda. Gunakan saat logika pernyataan sama tetapi contohnya bervariasi. Parametrization dapat mengurangi pengulangan dan memungkinkan pytest melaporkan setiap input sebagai item pengujiannya sendiri.

Dekorator biasanya mengambil dua argumen yang diperlukan:

  • argnames: String yang dipisah dengan koma, atau daftar atau tuple dari string, dengan nama-nama argumen yang diteruskan ke fungsi pengujian. Contohnya termasuk "item", , "test_input, expected"["test_input", "expected"], dan ("test_input", "expected").
  • argvalues: Iterable dari nilai atau himpunan parameter untuk digunakan dalam argumen tersebut. Dengan satu nama argumen, setiap item diteruskan sebagai satu nilai pengujian, bahkan jika nilai tersebut adalah tuple. Dengan beberapa nama argumen, setiap item harus menyediakan satu nilai untuk setiap nama argumen, biasanya sebagai tuple, daftar, atau panggilan pytest.param(...).

Argumen opsional ids memungkinkan Anda menyesuaikan ID pengujian yang digunakan pytest untuk melabeli setiap item pengujian yang dihasilkan. Opsi yang lebih canggih, seperti dan , berguna untuk kasus seperti fixture parametrisasi atau sumber daya yang mahal; Anda dapat membacanya dalam panduan cara melakukan parametrisasi dan referensi pytest .

Note

Pytest meneruskan nilai parameter ke pengujian sebagaimana adanya; Pytest tidak menyalinnya. Jika pengujian bermutasi nilai parameter yang dapat diubah, seperti daftar atau kamus, mutasi tersebut dapat terlihat dalam kasus yang dihasilkan kemudian yang menerima objek yang sama.

Kapan menggunakan parametrisasi

Dua skenario umum di mana Anda mungkin ingin menggunakan @pytest.mark.parametrize() meliputi:

  • Ketika pengujian mengulangi input dan mengulangi pernyataan yang sama
  • Ketika beberapa fungsi pengujian menegaskan perilaku yang sama dengan input yang berbeda

Mari kita tinjau setiap contoh terlebih dahulu tanpa menggunakan @pytest.mark.parametrize(), dan kemudian dengannya untuk menunjukkan bagaimana hal itu dapat membuat pengujian lebih mudah dipertahankan dan didiagnosis.

Untuk perulangan

Berikut adalah contoh fungsi pengujian dengan perulangan for:

def test_string_is_digit():
    items = ["1", "10", "33"]
    for item in items:
        assert item.isdigit()

Pola ini bisa sulit didiagnosis ketika satu input gagal:

  • Umpan balik yang tidak lengkap: Pernyataan pertama yang gagal menghentikan perulangan, sehingga nilai selanjutnya tidak dievaluasi dalam eksekusi tersebut.
  • Hasil pengujian tunggal: Pytest melaporkan satu tes yang terkumpul bukan satu kasus uji per input.
  • Pengulangan ulang berulang: Memperbaiki kegagalan pertama mungkin mengungkapkan kegagalan lain hanya setelah Anda menjalankan pengujian lagi.

Mari kita ubah pengujian untuk menyertakan dua item yang akan gagal dalam pernyataan:

def test_string_is_digit():
    items = ["No", "1", "10", "33", "Yes"]
    for item in items:
        assert item.isdigit()

Menjalankan pengujian hanya menunjukkan kegagalan pertama, meskipun ada dua item tidak valid dalam daftar. Contoh perintah menggunakan python -m pytest, yang berfungsi saat Python tersedia sebagai python. Jika shell Anda menggunakan python3 di macOS atau Linux, atau py pada Windows, ganti perintah tersebut.

python -m pytest test_items.py
=================================== FAILURES ===================================
_____________________________ test_string_is_digit _____________________________

    def test_string_is_digit():
        items = ["No", "1", "10", "33", "Yes"]
        for item in items:
>           assert item.isdigit()
E           AssertionError: assert False
E            +  where False = <built-in method isdigit of str object at 0x...>()
E            +    where <built-in method isdigit of str object at 0x...> = 'No'.isdigit

test_items.py:4: AssertionError
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit - AssertionError: assert False
============================== 1 failed in 0.01s ===============================

Output introspeksi asersi Pytest menunjukkan metode terikat str.isdigit dan nilai string yang menyebabkan kegagalan. Header sesi, detail platform, alamat memori, dan waktu proses dapat bervariasi, sehingga contoh menghilangkan atau mempersingkatnya dengan ... dan 0x.... Detail yang relevan adalah bahwa perulangan berhenti pada kegagalan pertama ('No') dan tidak pernah mengevaluasi 'Yes'.

Ini adalah kasus yang berguna untuk @pytest.mark.parametrize(). Sebelum memperbarui pengujian, mari kita jelajahi situasi umum lain yang tidak melibatkan for perulangan.

Pengujian yang menegaskan perilaku yang sama

Sekelompok tes yang mengajukan pernyataan yang sama juga merupakan kandidat yang baik untuk @pytest.mark.parametrize(). Jika pengujian sebelumnya ditulis ulang dengan satu pengujian untuk setiap item, itu akan memungkinkan pelaporan kegagalan yang lebih baik, tetapi akan berulang:

def test_is_digit_1():
    assert "1".isdigit()

def test_is_digit_10():
    assert "10".isdigit()

def test_is_digit_33():
    assert "33".isdigit()

Pengujian ini lebih terperinci karena kegagalan dapat dikaitkan dengan satu input. Meskipun mungkin tampak tidak biasa untuk memiliki beberapa pengujian serupa, pola ini umum dalam suite pengujian produksi yang mencoba melaporkan kegagalan dengan tepat.

Namun, tes berulang datang dengan masalah berikut:

  • Kode berulang, yang menciptakan beban pemeliharaan.
  • Fungsi serupa mudah diperbarui secara tidak konsisten.
  • Menambahkan input baru memerlukan penyalinan isi pengujian lain, sehingga kasus penting dapat diabaikan.

Parametrization mempertahankan keuntungan pelaporan dari pengujian item terpisah tanpa perlu menyalin isi pengujian yang sama.

Cara menggunakan parametrize

Sekarang setelah Anda mengetahui beberapa kasus penggunaan untuk @pytest.mark.parametrize(), mari kita melakukan pembaruan pada pengujian yang menggunakan perulangan for dengan item yang gagal.

Impor pytest, lalu terapkan @pytest.mark.parametrize() tepat di atas fungsi pengujian:

import pytest

@pytest.mark.parametrize("item", ["No", "1", "10", "33", "Yes"])
def test_string_is_digit(item):
    assert item.isdigit()

Sebelum menjalankan pengujian, mari kita membahas perubahan.

Dalam contoh ini, dekorator menerima dua argumen. Argumen pertama, "item", memberi nama argumen yang diteruskan pytest ke fungsi pengujian. Nama harus cocok dengan item parameter di test_string_is_digit(item). Argumen kedua adalah daftar nilai yang digunakan pytest untuk kasus pengujian yang dihasilkan.

Pelaporan kesalahan yang rinci

Di balik layar, pytest mengumpulkan satu item pengujian untuk setiap nilai dalam daftar parameter. Itu berarti kasus yang lolos dan gagal dilaporkan secara terpisah. Mari kita lihat apa yang terjadi saat menjalankan pengujian:

python -m pytest test_items.py
============================= test session starts ==============================
...
collected 5 items

test_items.py F...F                                                      [100%]

=================================== FAILURES ===================================
___________________________ test_string_is_digit[No] ___________________________

item = 'No'

    @pytest.mark.parametrize("item", ["No", "1", "10", "33", "Yes"])
    def test_string_is_digit(item):
>       assert item.isdigit()
E       AssertionError: assert False
E        +  where False = <built-in method isdigit of str object at 0x...>()
E        +    where <built-in method isdigit of str object at 0x...> = 'No'.isdigit

test_items.py:5: AssertionError
__________________________ test_string_is_digit[Yes] ___________________________

item = 'Yes'

    @pytest.mark.parametrize("item", ["No", "1", "10", "33", "Yes"])
    def test_string_is_digit(item):
>       assert item.isdigit()
E       AssertionError: assert False
E        +  where False = <built-in method isdigit of str object at 0x...>()
E        +    where <built-in method isdigit of str object at 0x...> = 'Yes'.isdigit

test_items.py:5: AssertionError
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False
FAILED test_items.py::test_string_is_digit[Yes] - AssertionError: assert False
========================= 2 failed, 3 passed in 0.07s ==========================

Ada beberapa item penting dalam laporan pengujian. Pertama, pytest melaporkan lima item yang dikumpulkan dari satu fungsi pengujian: tiga lulus dan dua gagal. Kegagalan dilaporkan secara terpisah, termasuk nilai input yang menyebabkan setiap kegagalan. Header test_string_is_digit[No] menunjukkan set parameter, dan parameter lokal item = 'No' muncul di atas tampilan sumber. Baris introspeksi metode terikat (alamat memori ditampilkan sebagai 0x...) mengonfirmasi nilai mana yang memicu kegagalan pernyataan:

___________________________ test_string_is_digit[No] ___________________________

item = 'No'
[...]
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False

Sulit untuk mengabaikan nilai yang menyebabkan kegagalan dengan begitu banyak tempat di mana hal itu dilaporkan.

Gunakan opsi output verbose

Saat pengujian berhasil, pelaporan baris perintah minimal dalam kondisi standar. Berikut adalah tampilan pengujian setelah pembaruan untuk memperbaiki kegagalan (lanjutkan mengedit pada test_items.py yang sama sehingga import pytest sudah ada):

@pytest.mark.parametrize("item", ["0", "1", "10", "33", "9"])
def test_string_is_digit(item):
    assert item.isdigit()

Menjalankan pengujian menghasilkan output minimal:

python -m pytest test_items.py
============================= test session starts ==============================
...
collected 5 items

test_items.py .....                                                      [100%]

============================== 5 passed in 0.01s ===============================

Meningkatkan verbositas menunjukkan setiap kasus pengujian yang dihasilkan dan ID parameter yang dihasilkan:

python -m pytest -v test_items.py
============================= test session starts ==============================
...
collected 5 items

test_items.py::test_string_is_digit[0] PASSED                            [ 20%]
test_items.py::test_string_is_digit[1] PASSED                            [ 40%]
test_items.py::test_string_is_digit[10] PASSED                           [ 60%]
test_items.py::test_string_is_digit[33] PASSED                           [ 80%]
test_items.py::test_string_is_digit[9] PASSED                            [100%]

============================== 5 passed in 0.01s ===============================

Cara menggunakan beberapa nama argumen

Contoh yang telah kita lihat sejauh ini menggunakan satu nama argumen: "item". Anda juga dapat menyertakan beberapa nama argumen dalam argumen pertama. Saat Anda menggunakan string, pisahkan nama dengan koma. Anda juga dapat memasukkan nama sebagai daftar atau tuple, seperti ["item", "attribute"] atau ("item", "attribute").

Satu kasus penggunaan untuk beberapa nama argumen adalah meneruskan nilai input dan nilai yang diharapkan untuk mengujinya. Dalam argumen kedua, setiap set parameter memerlukan satu nilai untuk setiap nama argumen. Misalnya, jika nama argumen adalah "test_input, expected_value", nilai argumen mungkin .[("3+5", 8), ("3*4", 12)]

Contoh berikutnya menggunakan fungsi hasattr() Python, yang mengembalikan nilai boolean tergantung pada apakah objek memiliki atribut bernama:

>>> hasattr(dict(), "keys")
True
>>> hasattr("string", "append")
False

Setiap kasus pengujian memerlukan objek dan nama atribut, sehingga kita dapat menggunakan @pytest.mark.parametrize() dengan cara berikut. Lanjutkan dalam file yang sama test_items.py sehingga import pytest sudah tersedia:

@pytest.mark.parametrize("item, attribute", [("", "format"), (list(), "append")])
def test_attributes(item, attribute):
    assert hasattr(item, attribute)

Dekorator @pytest.mark.parametrize() masih menggunakan string tunggal untuk argumen pertama, tetapi string tersebut sekarang berisi dua nama argumen yang dipisahkan oleh koma. Nama menjadi argumen untuk fungsi pengujian. Dalam hal ini, mereka adalah item dan attribute. Daftar atau tuple nama berfungsi dengan cara yang sama.

Argumen kedua adalah daftar dengan dua set parameter. Setiap set parameter berisi satu item nilai dan satu attribute nilai. Jika kumpulan parameter memiliki terlalu sedikit atau terlalu banyak nilai untuk nama argumen, pytest menimbulkan kesalahan koleksi sebelum menjalankan kasus pengujian.

Pytest membangun ID pengujian untuk setiap set parameter. Untuk nilai sederhana seperti angka, string, boolean, dan None, pytest menggunakan representasi string nilai yang biasa. Untuk objek lain, pytest menggunakan nama berdasarkan nama argumen dan indeks nol kumpulan parameter. Dalam output ini, string kosong berkontribusi pada bagian ID kosong, sehingga pemisah tanda hubung antara dua ID masih terlihat di [-format]. Karena list() tidak memiliki ID yang dihasilkan sederhana, pytest menggantikan item1:

python -m pytest -v test_items.py::test_attributes
============================= test session starts ==============================
...
collected 2 items

test_items.py::test_attributes[-format] PASSED                           [ 50%]
test_items.py::test_attributes[item1-append] PASSED                      [100%]

============================== 2 passed in 0.01s ===============================

Jika ID yang dihasilkan sulit dibaca, Anda dapat meneruskan argumen opsional ids atau membungkus satu set parameter dalam pytest.param(..., id="name") untuk memberikan nama yang lebih jelas. Contohnya:

import pytest


@pytest.mark.parametrize(
    "item, attribute",
    [
        pytest.param("", "format", id="empty-string-format"),
        (list(), "append"),
    ],
)
def test_attributes(item, attribute):
    assert hasattr(item, attribute)

Panggilan pytest.param("", "format", id="empty-string-format") ini menyediakan seluruh ID untuk kumpulan parameter tersebut, menggantiKAN ID yang dihasilkan -format secara otomatis. Dalam output verbose, pengujian yang dihasilkan diberi nama test_attributes[empty-string-format] dan test_attributes[item1-append]. ID yang jelas membuat output verbose dan pesan kegagalan lebih mudah dibaca.