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:
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:
raiseFungsi 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:
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:
terkirim
jumlah pemanggilan: 1
pemeriksaan pemanggilan lolosDua 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:
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 == 2Mari kita bedah baris-baris pentingnya:
patch("notifikasi.kirim_pesan"): string target ditulismodule.nama_fungsi— patch menimpa atributkirim_pesandi dalam modulenotifikasi, yaitu persis tempatkonfirmasi_pembelian()mencarinya. Menimpa tempat pemakaian, bukan tempat definisi, adalah aturan terpenting patch.mock_kirim.return_value = "terkirim": panggilan kekirim_pesan()selama blok mengembalikan string ini tanpa menyentuh jaringan.assert_called_once_with(...): memverifikasi mock dipanggil tepat sekali dengan argumen persis — penerima dan isi pesan yang kita harapkan.mock_kirim.side_effect = [ConnectionError(...), "terkirim"]: daftar side effect dikonsumsi berurutan; panggilan pertama membangkitkanConnectionError, panggilan kedua mengembalikan"terkirim". Pola ini menguji logika percobaan ulangkirim_dengan_ulang()tanpa perlu mematikan jaringan sungguhan.- Keluar blok
with: patch otomatis memulihkankirim_pesanasli.
Output yang Dihasilkan (run python3 -m pytest -q pada folder berisi file di atas serta test decorator pada bagian berikutnya):
... [100%]
3 passed in 0.09spatch sebagai Decorator #
Bentuk decorator menimpa fungsi dengan mock yang dikirim sebagai argumen pertama:
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):
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:
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:
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.