Beranda / Kursus / Testing for Beginners / II — Praktik
II — Praktik
Artikel 5 dari 9

Praktik: Parametrization & Table-Driven Tests

Menghindari duplikasi test lewat pytest.mark.parametrize, memberi ID yang jelas ke tiap kasus, boundary testing di titik batas validasi, dan kapan sebaiknya TIDAK memakai parametrization.

10 Juli 2026

Lihat kembali test validasi yang mungkin sudah terpikir untuk kita tulis di artikel sebelumnya: satu test untuk "title kosong ditolak", satu lagi untuk "password terlalu pendek ditolak", satu lagi untuk "username terlalu pendek ditolak" — polanya SELALU sama, cuma input dan hasil yang diharapkan yang beda. Menulis satu fungsi test terpisah untuk tiap kasus itu boros dan gampang jadi tidak konsisten. Di artikel ini, kita rapikan jadi satu test yang jauh lebih ringkas.

Masalah dengan Duplikasi Test

def test_register_username_too_short(client):
    response = client.post("/auth/register", json={"username": "al", "password": "supersecret123"})
    assert response.status_code == 422


def test_register_password_too_short(client):
    response = client.post("/auth/register", json={"username": "alice", "password": "short"})
    assert response.status_code == 422


def test_register_missing_password(client):
    response = client.post("/auth/register", json={"username": "alice"})
    assert response.status_code == 422

# ...dan seterusnya, struktur identik berulang-ulang

Masalahnya bukan cuma "banyak baris" — tiap kali ada perubahan di CARA melakukan assertion (misalnya kita mau menambah pengecekan tambahan), kita harus mengubahnya di SEMUA fungsi ini. Duplikasi seperti ini adalah kandidat kuat untuk digabungkan.

pytest.mark.parametrize — Satu Test, Banyak Kasus

import pytest


@pytest.mark.parametrize(
    ("payload", "expected_status"),
    [
        pytest.param({"username": "al", "password": "supersecret123"}, 422, id="username-terlalu-pendek"),
        pytest.param({"username": "alice", "password": "short"}, 422, id="password-terlalu-pendek"),
        pytest.param({"username": "alice"}, 422, id="password-hilang"),
        pytest.param({"password": "supersecret123"}, 422, id="username-hilang"),
        pytest.param({"username": "alice", "password": "supersecret123"}, 201, id="payload-valid"),
    ],
)
def test_register_validation(client: TestClient, payload: dict, expected_status: int):
    response = client.post("/auth/register", json=payload)
    assert response.status_code == expected_status

Satu fungsi test ini menjalankan LIMA kali, masing-masing dengan payload dan expected_status yang berbeda — pytest memperlakukannya sebagai lima test TERPISAH, masing-masing dengan hasil lolos/gagal sendiri:

pytest tests/integration/test_auth.py::test_register_validation -v
test_register_validation[username-terlalu-pendek] PASSED
test_register_validation[password-terlalu-pendek] PASSED
test_register_validation[password-hilang] PASSED
test_register_validation[username-hilang] PASSED
test_register_validation[payload-valid] PASSED

Kenapa pytest.param(..., id=...), Bukan Tuple Biasa?

Parameter id itu opsional secara teknis (tanpa itu, pytest membuat ID otomatis dari representasi Python objeknya, yang sering panjang dan tidak jelas). Tapi memberi ID eksplisit membuat perbedaan besar untuk PENGALAMAN DEBUGGING: bandingkan test_register_validation[username-terlalu-pendek] FAILED dengan test_register_validation[payload0] FAILED — yang pertama langsung memberi tahu skenario mana yang bermasalah, tanpa perlu membuka kode test-nya dulu.

Contoh Lain: Batas Validasi yang Sistematis

Parametrization sangat pas untuk menguji BATAS (boundary) sebuah aturan validasi secara sistematis — bukan cuma "satu contoh salah, satu contoh benar", tapi PERSIS di titik batasnya:

@pytest.mark.parametrize(
    ("title", "expected_status"),
    [
        pytest.param("", 422, id="title-kosong"),
        pytest.param("a", 201, id="title-satu-karakter"),
        pytest.param("a" * 200, 201, id="title-pas-batas-maksimum"),
        pytest.param("a" * 201, 422, id="title-lewat-batas-maksimum"),
    ],
)
def test_create_task_title_validation(client: TestClient, title: str, expected_status: int):
    token = register_and_login(client)
    response = client.post("/api/tasks", json={"title": title}, headers=auth_headers(token))
    assert response.status_code == expected_status

Ingat batasan Field(min_length=1, max_length=200) di TaskCreate dari seri FastAPI? Empat kasus ini menguji PERSIS di sekitar batasnya: kosong (gagal), satu karakter (batas bawah, lolos), 200 karakter (batas atas, lolos), 201 karakter (satu lebih dari batas atas, gagal). Pola "uji tepat di titik batas, bukan cuma di tengah-tengah" ini disebut boundary testing, dan sering kali di titik-titik batas inilah bug validasi bersembunyi (kesalahan < vs <= yang klasik).

Kapan TIDAK Memakai Parametrization

Parametrization paling pas kalau STRUKTUR test-nya identik, cuma data dan hasilnya beda. Kalau setiap "kasus" butuh langkah persiapan yang beda-beda secara signifikan (misalnya satu kasus butuh dua user terdaftar, kasus lain cuma butuh satu), memaksakan jadi satu parametrize malah membuat test lebih sulit dibaca dibanding beberapa fungsi terpisah yang masing-masing jelas maksudnya. Parametrization itu alat untuk mengurangi duplikasi STRUKTURAL, bukan tujuan untuk memaksa semua test jadi satu fungsi besar.

Rangkuman

  • pytest.mark.parametrize menjalankan satu fungsi test berkali-kali dengan input/ekspektasi berbeda, menghindari duplikasi struktural.
  • pytest.param(..., id=...) memberi nama jelas ke tiap kasus, jauh lebih mudah dibaca saat ada yang gagal.
  • Boundary testing — uji persis di titik batas aturan validasi (tepat di bawah, tepat di batas, tepat di atas), bukan cuma contoh acak.
  • Parametrization cocok kalau STRUKTUR test sama; kalau tiap kasus butuh setup yang beda signifikan, fungsi test terpisah lebih jelas.
  • Topik

    TestingPytest