Test yang baik berjalan cepat, murah, dan hasilnya sama setiap saat — deterministik. Tiga jenis ketergantungan merusak sifat tersebut: panggilan jaringan yang lambat dan bisa gagal, pembacaan waktu sistem yang berubah setiap detik, serta database yang menyimpan sisa data antar test. Solusinya bukan menghapus dependensi dari kode produksi, melainkan menggantinya dengan objek palsu hanya selama test berjalan — teknik yang disebut mocking. Module unittest.mock dari standard library menyediakan seluruh peralatannya.

Module Contoh dengan Dependensi Eksternal #

Kita pakai module notifikasi.py yang mengirim pesan lewat layanan eksternal:

python
def kirim_pesan(email, isi):
    """Kirim pesan ke server penyedia email eksternal."""
    print(f"Mengirim pesan ke {email}: {isi}")
    return "terkirim"


def konfirmasi_pembelian(email, total):
    isi = f"Terima kasih! Total pembelian Anda Rp{total}."
    return kirim_pesan(email, isi)


def kirim_dengan_ulang(email, isi, percobaan_maks=3):
    for percobaan in range(1, percobaan_maks + 1):
        try:
            return kirim_pesan(email, isi)
        except ConnectionError:
            if percobaan == percobaan_maks:
                raise

Fungsi konfirmasi_pembelian() tidak bisa diuji apa adanya: menguji berarti mengirim email sungguhan. Yang perlu diverifikasi justru logika di sekitar pengiriman — isi pesan yang terbentuk dan penerima yang dituju.

Objek Mock Dasar #

Class Mock adalah objek serba-bisa: setiap atribut dan pemanggilan terhadapnya tercatat, dan nilai baliknya bisa dipasang lewat return_value:

python
from unittest.mock import Mock

mock = Mock(return_value="terkirim")
print(mock("[email protected]", "halo"))
print("jumlah pemanggilan:", mock.call_count)
mock.assert_called_once_with("[email protected]", "halo")
print("pemeriksaan pemanggilan lolos")

Output yang Dihasilkan:

text
terkirim
jumlah pemanggilan: 1
pemeriksaan pemanggilan lolos

Dua kemampuan kunci terlihat di sini: return_value membuat mock(...) mengembalikan "terkirim", dan mock.call_count menghitung berapa kali mock dipanggil.

patch sebagai Context Manager #

Mock sendiri belum menggantikan apa pun di kode produksi — tugas itu milik patch, yang menimpa atribut target selama blok tertentu:

python
from unittest.mock import patch
from notifikasi import konfirmasi_pembelian, kirim_dengan_ulang

def test_konfirmasi_mengirim_email_lengkap():
    with patch("notifikasi.kirim_pesan") as mock_kirim:
        mock_kirim.return_value = "terkirim"
        hasil = konfirmasi_pembelian("[email protected]", 150000)

    mock_kirim.assert_called_once_with(
        "[email protected]",
        "Terima kasih! Total pembelian Anda Rp150000.",
    )
    assert hasil == "terkirim"

def test_kirim_ulang_sampai_berhasil():
    with patch("notifikasi.kirim_pesan") as mock_kirim:
        mock_kirim.side_effect = [ConnectionError("jaringan mati"), "terkirim"]
        hasil = kirim_dengan_ulang("[email protected]", "tes kirim")

    assert hasil == "terkirim"
    assert mock_kirim.call_count == 2

Mari kita bedah baris-baris pentingnya:

  1. patch("notifikasi.kirim_pesan"): string target ditulis module.nama_fungsi — patch menimpa atribut kirim_pesan di dalam module notifikasi, yaitu persis tempat konfirmasi_pembelian() mencarinya. Menimpa tempat pemakaian, bukan tempat definisi, adalah aturan terpenting patch.
  2. mock_kirim.return_value = "terkirim": panggilan ke kirim_pesan() selama blok mengembalikan string ini tanpa menyentuh jaringan.
  3. assert_called_once_with(...): memverifikasi mock dipanggil tepat sekali dengan argumen persis — penerima dan isi pesan yang kita harapkan.
  4. mock_kirim.side_effect = [ConnectionError(...), "terkirim"]: daftar side effect dikonsumsi berurutan; panggilan pertama membangkitkan ConnectionError, panggilan kedua mengembalikan "terkirim". Pola ini menguji logika percobaan ulang kirim_dengan_ulang() tanpa perlu mematikan jaringan sungguhan.
  5. Keluar blok with: patch otomatis memulihkan kirim_pesan asli.

Output yang Dihasilkan (run python3 -m pytest -q pada folder berisi file di atas serta test decorator pada bagian berikutnya):

text
...                                                                     [100%]
3 passed in 0.09s

patch sebagai Decorator #

Bentuk decorator menimpa fungsi dengan mock yang dikirim sebagai argumen pertama:

python
from unittest.mock import patch
from notifikasi import konfirmasi_pembelian

@patch("notifikasi.kirim_pesan")
def test_konfirmasi_dengan_decorator(mock_kirim):
    mock_kirim.return_value = "terkirim"
    hasil = konfirmasi_pembelian("[email protected]", 50000)

    mock_kirim.assert_called_once()
    assert hasil == "terkirim"

Pilihan antara with dan decorator selera saja; context manager lebih sering dipakai karena wilayah penggantian terlihat dari indentasi.

Membaca Kegagalan Verifikasi Mock #

Ketika argumen pemanggilan tidak sesuai harapan, pesan kegagalannya membandingkan panggilan yang diharapkan dengan yang benar-benar terjadi. Berikut hasil test yang mengharapkan [email protected] padahal kode mengirim ke [email protected] (bagian yang menampilkan jejak internal unittest/mock.py dipotong agar ringkas):

text
E           AssertionError: expected call not found.
E           Expected: kirim_pesan('[email protected]', 'Terima kasih! Total pembelian Anda Rp150000.')
E             Actual: kirim_pesan('[email protected]', 'Terima kasih! Total pembelian Anda Rp150000.')

autospec: Mock yang Meniru Signature #

Mock polos menerima argumen apa pun tanpa protes, sehingga test bisa lulus meski kode produksi salah memanggil kirim_pesan(). Argumen autospec=True menyalin signature fungsi asli:

python
from unittest.mock import patch
import notifikasi

with patch("notifikasi.kirim_pesan", autospec=True) as mock_kirim:
    mock_kirim.return_value = "terkirim"
    try:
        notifikasi.kirim_pesan("[email protected]")
    except TypeError as e:
        print("TypeError:", e)

Output yang Dihasilkan:

text
TypeError: missing a required argument: 'isi'

Pemanggilan yang kehilangan argumen isi langsung ditolak — mock menolak menjadi tempat lolosnya kesalahan signature.

Dengan dependensi eksternal terisolasi, satu perilaku kode masih tersisa yang belum tergantung mesin verifikasi: jalur error — kita bahas cara mengujinya pada Chapter Testing Exceptions.