← Semua pembelajaran / Python untuk AI Engineer
Fase 6 · Data, Testing, Deploy

pytest — Parametrizing tests

Menjalankan satu fungsi test terhadap banyak kombinasi input, dengan laporan kegagalan per kasus.

Sumber asli docs.pytest.org Resmi Rangkuman ~4 menit baca

Intisari

  • @pytest.mark.parametrize menjalankan satu fungsi test untuk tiap set input.
  • Tiap kasus dilaporkan sebagai test terpisah — kamu tahu persis kombinasi mana yang gagal.
  • Beberapa dekorator bertumpuk menghasilkan seluruh kombinasi (perkalian kartesius).
  • Beri nama kasus lewat ids= supaya output-nya terbaca.
  • pytest.param(..., marks=pytest.mark.xfail) untuk menandai kasus yang memang diketahui gagal.

Bentuk dasarnya

import pytest

@pytest.mark.parametrize("teks,jumlah", [
    ("satu dua", 2),
    ("satu", 1),
    ("", 0),
    ("   ", 0),
    ("satu  dua   tiga", 3),
])
def test_hitung_kata(teks, jumlah):
    assert hitung_kata(teks) == jumlah
test_hitung_kata[satu dua-2] PASSED
test_hitung_kata[satu-1]     PASSED
test_hitung_kata[-0]         PASSED
test_hitung_kata[   -0]      FAILED     ← langsung terlihat kasus mana
test_hitung_kata[satu  dua   tiga-3] PASSED

Ini bukan sekadar menghemat baris. Kalau kelima kasus itu ditulis sebagai lima assert dalam satu fungsi, kegagalan pertama menghentikan sisanya — kamu tidak tahu apakah kasus lain juga rusak. Dengan parametrize, semuanya dijalankan dan dilaporkan sendiri-sendiri.

Memberi nama kasus

@pytest.mark.parametrize("teks,harapan", [
    ("[email protected]", True),
    ("bukan-email", False),
    ("@tanpa-lokal.com", False),
    ("tanpa-domain@", False),
], ids=["valid", "tanpa-at", "tanpa-lokal", "tanpa-domain"])
def test_validasi_email(teks, harapan):
    assert valid_email(teks) is harapan
test_validasi_email[valid]          PASSED
test_validasi_email[tanpa-at]       PASSED

Wajib untuk input yang panjang atau berisi karakter aneh — tanpa ids, nama testnya jadi tidak terbaca.

Kombinasi

@pytest.mark.parametrize("model", ["opus", "sonnet", "haiku"])
@pytest.mark.parametrize("streaming", [True, False])
def test_panggil(model, streaming):
    ...        # 3 × 2 = 6 test dijalankan

Kasus yang diketahui gagal

@pytest.mark.parametrize("teks,jumlah", [
    ("satu dua", 2),
    pytest.param("satu\tdua", 2, marks=pytest.mark.xfail(reason="tab belum ditangani")),
    pytest.param("kasus-lambat", 1, marks=pytest.mark.slow),
])
def test_hitung_kata(teks, jumlah):
    assert hitung_kata(teks) == jumlah

xfail mendokumentasikan bug yang sudah diketahui tanpa membuat suite-mu merah. Kalau suatu saat kasus itu lulus, pytest melaporkannya sebagai XPASS — pengingat bahwa xfail-nya sudah bisa dihapus.

Kasus dari file

import json
from pathlib import Path

KASUS = json.loads(Path("tests/data/kasus_chunking.json").read_text(encoding="utf-8"))

@pytest.mark.parametrize("kasus", KASUS, ids=lambda k: k["nama"])
def test_chunking(kasus):
    hasil = potong(kasus["masukan"], ukuran=kasus["ukuran"])
    assert len(hasil) == kasus["jumlah_harapan"]

Pola ini sangat pas untuk dataset eval. Simpan pertanyaan dan jawaban acuan sebagai JSON, lalu parametrize atasnya. Menambah kasus uji jadi soal mengedit file data — bukan menulis kode test baru.

Parametrize sebuah fixture

@pytest.fixture(params=["chroma", "memori"])
def vector_store(request, tmp_path):
    if request.param == "chroma":
        return ChromaStore(path=tmp_path)
    return StoreDiMemori()


def test_simpan_dan_cari(vector_store):
    vector_store.simpan("1", [0.1, 0.2, 0.3])
    assert vector_store.cari([0.1, 0.2, 0.3], k=1) == ["1"]

Setiap test yang memakai fixture ini otomatis dijalankan dua kali — sekali per implementasi. Cara yang bagus untuk memastikan dua backend berperilaku sama.

Contoh nyata: menguji chunking

@pytest.mark.parametrize("teks,ukuran,tumpang,jumlah", [
    ("a" * 100, 50, 0, 2),
    ("a" * 100, 50, 10, 3),
    ("a" * 30, 50, 0, 1),
    ("", 50, 0, 0),
], ids=["pas-dua", "dengan-tumpang", "lebih-kecil", "kosong"])
def test_jumlah_chunk(teks, ukuran, tumpang, jumlah):
    assert len(potong(teks, ukuran, tumpang)) == jumlah


@pytest.mark.parametrize("ukuran,tumpang", [(100, 20), (500, 50), (1000, 100)])
def test_chunk_tidak_kehilangan_teks(ukuran, tumpang):
    teks = "kata " * 500
    chunks = potong(teks, ukuran, tumpang)
    gabung = "".join(chunks)
    for kata in teks.split():
        assert kata in gabung

Menjalankan sebagian

uv run pytest -k "valid"                       # kasus yang namanya mengandung "valid"
uv run pytest "tests/test_a.py::test_x[valid]" # satu kasus persis
uv run pytest -m "not slow"                     # lewati yang ditandai slow
uv run pytest --lf                              # hanya yang gagal terakhir kali

Mendaftarkan marker

[tool.pytest.ini_options]
markers = [
    "slow: test yang lambat",
    "eval: eval kualitas LLM, memanggil API asli",
    "integrasi: butuh layanan eksternal",
]
addopts = "--strict-markers"

--strict-markers membuat marker yang salah ketik jadi error, bukan diam-diam diabaikan. Tanpa itu, @pytest.mark.slwo tidak akan pernah cocok dengan filter mana pun.

Kapan parametrize tidak cocok

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.