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

Praktik: Unit Test Pertama dengan Pytest

Instalasi dan konvensi dasar pytest, menulis unit test pertama untuk fungsi murni di app/security.py, menguji kasus negatif, dan menandai test dengan marker supaya bisa dipilah cepat vs lambat.

10 Juli 2026

Setelah memahami konsep dasar dan anatomi test yang baik, saatnya praktik. Kita mulai dari yang paling dasar di piramida testing: unit test — menguji fungsi-fungsi murni di task-tracker-api, dimulai dari app/security.py (hashing password dan JWT dari seri Security).

Kenapa Mulai dari Sini?

app/security.py berisi fungsi-fungsi yang IDEAL untuk unit test pertama: tidak butuh database, tidak butuh HTTP request, cuma menerima input dan mengembalikan output.

def hash_password(plain_password: str) -> str: ...
def verify_password(plain_password: str, hashed_password: str) -> bool: ...
def create_access_token(subject: str) -> str: ...
def decode_access_token(token: str) -> Optional[str]: ...

Fungsi seperti ini disebut pure function (atau mendekati murni) — hasilnya cuma bergantung pada input yang diberikan, tidak menyentuh state di luar dirinya (kecuali secret_key dari config, yang tetap konsisten). Ini yang membuatnya gampang diuji: tidak perlu "menyiapkan dunia" yang rumit sebelum memanggilnya.

Instalasi dan Struktur Dasar Pytest

pip install pytest

Pytest punya konvensi penamaan yang otomatis dikenali (disebut test discovery), tanpa perlu registrasi manual:

  • File test harus bernama test_*.py atau *_test.py.
  • Function test di dalamnya harus diawali test_.
  • Pytest otomatis mencari file dan fungsi yang cocok pola ini, di seluruh folder tests/ (atau folder mana pun sesuai konfigurasi).
  • Test Pertama

    # tests/unit/test_security.py
    from app.security import hash_password, verify_password
    
    
    def test_hash_password_is_not_plaintext():
        assert hash_password("supersecret123") != "supersecret123"
    
    
    def test_verify_password_accepts_correct_password():
        hashed = hash_password("supersecret123")
        assert verify_password("supersecret123", hashed) is True
    
    
    def test_verify_password_rejects_wrong_password():
        hashed = hash_password("supersecret123")
        assert verify_password("passwordsalah", hashed) is False
    

    Perhatikan: pytest memakai assert BIASA dari Python — bukan method khusus seperti self.assertEqual(...) di framework testing lain. Ini salah satu alasan pytest populer: sintaksnya sesederhana mungkin. Di balik layar, pytest melakukan assertion rewriting — kalau assert gagal, pytest otomatis menampilkan detail nilai kiri-kanan yang dibandingkan, tanpa kita perlu menulis pesan error manual.

    Jalankan:

    pytest tests/unit/test_security.py -v
    
    tests/unit/test_security.py::test_hash_password_is_not_plaintext PASSED
    tests/unit/test_security.py::test_verify_password_accepts_correct_password PASSED
    tests/unit/test_security.py::test_verify_password_rejects_wrong_password PASSED
    

    Menguji Kasus Negatif — Sama Pentingnya dengan Kasus Positif

    Test yang cuma menguji "kalau semuanya benar" itu setengah pekerjaan. Sama pentingnya: menguji apa yang terjadi kalau input SALAH atau data DIRUSAK:

    def test_decode_access_token_rejects_garbage_token():
        assert decode_access_token("bukan-token-yang-valid") is None
    
    
    def test_decode_access_token_rejects_tampered_token():
        token = create_access_token(subject="alice")
        header, payload, signature = token.split(".")
        # Membalik signature memastikan hasilnya PASTI berubah — lihat catatan
        # di bawah kenapa ini penting.
        tampered = f"{header}.{payload}.{signature[::-1]}"
        assert decode_access_token(tampered) is None
    

    Kisah nyata dari balik layar seri ini: versi awal test kedua di atas menukar cuma SATU karakter di ujung token, bukan membalik seluruh signature-nya. Test itu ternyata flaky — kadang lolos, kadang gagal, tanpa perubahan kode sama sekali. Penyebabnya: base64 (format yang dipakai JWT) punya redundansi bit di posisi tertentu, jadi menukar satu karakter di posisi yang "kurang beruntung" kadang tidak benar-benar mengubah hasil decode-nya. Ini pelajaran nyata soal prinsip Repeatable dari artikel sebelumnya — begitu menemukan test yang hasilnya tidak konsisten, curigai ASUMSI di balik test-nya, bukan cuma kode yang diuji.

    Menandai Test dengan Marker

    Supaya nanti bisa memilah test cepat (unit) dari test lambat (integration), kita tandai dengan marker:

    import pytest
    
    
    @pytest.mark.unit
    def test_hash_password_is_not_plaintext():
        ...
    

    Marker didaftarkan di pytest.ini supaya pytest tidak memperingatkan "marker tidak dikenal":

    [pytest]
    markers =
        unit: test unit murni, tanpa I/O apa pun (database, network, filesystem)
        integration: test integrasi, lewat HTTP client dan database test
    

    Sekarang kita bisa menjalankan cuma unit test:

    pytest -m unit -v
    
    tests/unit/test_security.py::test_hash_password_returns_different_hash_each_time PASSED
    tests/unit/test_security.py::test_hash_password_is_not_plaintext PASSED
    tests/unit/test_security.py::test_verify_password_accepts_correct_password PASSED
    tests/unit/test_security.py::test_verify_password_rejects_wrong_password PASSED
    tests/unit/test_security.py::test_create_and_decode_access_token_roundtrip PASSED
    tests/unit/test_security.py::test_decode_access_token_rejects_garbage_token PASSED
    tests/unit/test_security.py::test_decode_access_token_rejects_tampered_token PASSED
    
    ======================= 7 passed in 3.37s =======================
    

    Tujuh test ini nantinya akan berdampingan dengan puluhan integration test (dibahas mulai artikel berikutnya) — dan begitu keduanya ada, kamu akan langsung merasakan bedanya: pytest -m unit selesai dalam hitungan detik, cocok dijalankan berulang kali SETIAP menyimpan perubahan kode, sementara test yang menyentuh database perlu waktu lebih lama.

    Rangkuman

  • Unit test paling cocok untuk fungsi murni — tidak butuh database, network, atau state eksternal.
  • Pytest otomatis mendeteksi file test_*.py dan fungsi test_*, tanpa registrasi manual, dan memakai assert biasa.
  • Uji kasus negatif, bukan cuma kasus positif — dan kalau menemukan test yang hasilnya tidak konsisten (flaky), curigai asumsi di balik test-nya.
  • Marker (@pytest.mark.unit) memudahkan memilah test cepat dari test lambat, didaftarkan di pytest.ini.
  • Topik

    TestingPytest