Mocking #
Unit test yang baik menguji satu unit kode secara terisolasi — tanpa melibatkan database sungguhan, API eksternal, atau sistem file. Masalahnya, kode nyata hampir selalu bergantung pada sesuatu yang eksternal. Mocking adalah teknik untuk menggantikan dependensi tersebut dengan objek tiruan yang perilakunya bisa kamu kendalikan sepenuhnya: kembalikan nilai tertentu, lempar eksepsi, atau rekam apakah ia dipanggil dengan argumen yang benar. Python menyediakan unittest.mock yang kuat untuk ini, tersedia langsung di standard library.
Mengapa Mocking Diperlukan #
Tanpa mocking, test yang bergantung pada API eksternal punya banyak masalah:
# ANTI-PATTERN: test yang memanggil API sungguhan
def test_cek_stok():
hasil = cek_stok_produk(product_id=42) # memanggil API nyata!
assert hasil["stok"] > 0
# Masalah:
# ✗ Test lambat — harus menunggu respons jaringan
# ✗ Test tidak deterministik — hasil bisa berbeda tergantung kondisi server
# ✗ Test bisa gagal karena masalah jaringan, bukan bug di kode
# ✗ Bisa ada efek samping: data tertulis ke database production
# ✗ Memerlukan kredensial/environment khusus untuk berjalan
# BENAR: mock dependensi eksternal
def test_cek_stok():
with patch("modul_kita.requests.get") as mock_get:
mock_get.return_value.json.return_value = {"stok": 10}
hasil = cek_stok_produk(product_id=42)
assert hasil["stok"] == 10
# Test ini cepat, deterministik, dan tidak butuh jaringan
Mock dan MagicMock
#
Mock adalah objek tiruan yang menerima semua atribut dan pemanggilan tanpa error — setiap akses ke atributnya menghasilkan Mock baru secara otomatis.
from unittest.mock import Mock
m = Mock()
# Semua atribut otomatis ada sebagai Mock baru
print(m.nama) # <Mock name='mock.nama' id='...'>
print(m.metode()) # <Mock name='mock.metode()' id='...'>
# Atur return value
m.return_value = 42
print(m()) # 42
# Atur atribut spesifik
m.nama = "Produk A"
print(m.nama) # "Produk A"
# Atur return value metode bersarang
m.ambil_data.return_value = {"id": 1, "nama": "Laptop"}
print(m.ambil_data()) # {"id": 1, "nama": "Laptop"}
MagicMock adalah subkelas Mock yang sudah mengimplementasikan magic methods Python (__len__, __iter__, __str__, __enter__, __exit__, dll). Gunakan MagicMock saat kode yang diuji berinteraksi dengan objek menggunakan protokol Python.
from unittest.mock import Mock, MagicMock
# Mock biasa — magic methods tidak berfungsi
m = Mock()
# len(m) → TypeError: object of type 'Mock' has no len()
# MagicMock — magic methods berfungsi
mm = MagicMock()
mm.__len__.return_value = 5
print(len(mm)) # 5
mm.__iter__.return_value = iter([1, 2, 3])
print(list(mm)) # [1, 2, 3]
# MagicMock juga mendukung context manager
mm.__enter__.return_value = mm
mm.__exit__.return_value = False
with mm as obj:
print(obj) # <MagicMock ...>
Parameter spec — Mock yang Type-Safe
#
Tanpa spec, mock menerima atribut apapun — termasuk nama yang salah ketik. Ini bisa menyembunyikan bug:
from unittest.mock import Mock
class LayananEmail:
def kirim(self, tujuan, subjek, isi):
pass
# ANTI-PATTERN: tanpa spec — typo tidak terdeteksi
mock_email = Mock()
mock_email.kirm("[email protected]", "Halo", "Isi pesan") # typo 'kirm'!
# Test tetap lulus padahal nama metode salah
# BENAR: gunakan spec — hanya atribut yang ada di kelas asli yang diizinkan
mock_email = Mock(spec=LayananEmail)
mock_email.kirim("[email protected]", "Halo", "Isi pesan") # ✓ OK
try:
mock_email.kirm("[email protected]", "Halo", "Isi") # ✗ typo
except AttributeError as e:
print(e) # Mock object has no attribute 'kirm'
Biasakan menggunakanspec=atauspec_set=saat membuat mock. Tanpanya, mock menerima semua nama atribut apapun, sehingga typo pada nama metode tidak akan terdeteksi dan test bisa memberikan false positive — test lulus padahal kode sebenarnya salah.
patch — Mengganti Dependensi Sementara
#
patch mengganti objek nyata dengan mock selama durasi test, lalu mengembalikannya ke kondisi semula. Bisa digunakan sebagai decorator atau context manager.
Aturan Path Patch yang Benar #
Ini adalah sumber kebingungan paling umum: patch di mana objek itu digunakan, bukan di mana ia didefinisikan.
# File: layanan_produk.py
import requests # requests didefinisikan di sini
def ambil_produk(product_id):
resp = requests.get(f"https://api.example.com/produk/{product_id}")
return resp.json()
# ANTI-PATTERN: patch di mana requests didefinisikan
@patch("requests.get") # ✗ mungkin tidak bekerja
# BENAR: patch di mana requests digunakan (di modul layanan_produk)
@patch("layanan_produk.requests.get") # ✓ ini yang benar
Aturan sederhana:
patch("MODUL_YANG_MENGGUNAKAN.NAMA_OBJEK")
bukan
patch("MODUL_ASAL_OBJEK.NAMA_OBJEK")
Patch sebagai Decorator #
import unittest
from unittest.mock import patch, Mock
# layanan_produk.py
import requests
def ambil_produk(product_id):
resp = requests.get(f"https://api.example.com/produk/{product_id}")
data = resp.json()
if not data:
raise ValueError("Produk tidak ditemukan")
return data
def buat_pesanan(product_id, jumlah):
produk = ambil_produk(product_id)
return {"produk": produk["nama"], "jumlah": jumlah, "total": produk["harga"] * jumlah}
class TestLayananProduk(unittest.TestCase):
@patch("__main__.requests.get")
def test_ambil_produk_berhasil(self, mock_get):
# Atur respons mock
mock_get.return_value.json.return_value = {
"id": 1, "nama": "Laptop", "harga": 12000000
}
hasil = ambil_produk(1)
self.assertEqual(hasil["nama"], "Laptop")
# Verifikasi URL yang digunakan benar
mock_get.assert_called_once_with("https://api.example.com/produk/1")
@patch("__main__.requests.get")
def test_ambil_produk_tidak_ditemukan(self, mock_get):
mock_get.return_value.json.return_value = {} # respons kosong
with self.assertRaises(ValueError):
ambil_produk(999)
Patch sebagai Context Manager #
Berguna saat hanya sebagian kecil dari test yang butuh mock, atau saat kamu ingin lebih eksplisit tentang scope mock-nya.
def test_buat_pesanan(self):
with patch("__main__.requests.get") as mock_get:
mock_get.return_value.json.return_value = {
"id": 1, "nama": "Mouse", "harga": 250000
}
pesanan = buat_pesanan(product_id=1, jumlah=3)
self.assertEqual(pesanan["total"], 750000)
self.assertEqual(pesanan["produk"], "Mouse")
patch.object — Patch Metode pada Objek Spesifik
#
patch.object lebih eksplisit dan lebih mudah dibaca karena kamu menyebut kelas/modul secara langsung, bukan sebagai string path.
import unittest
from unittest.mock import patch
class LayananEmail:
def kirim(self, tujuan, pesan):
# implementasi nyata: kirim email via SMTP
pass
class NotifikasiPesanan:
def __init__(self, layanan_email):
self.email = layanan_email
def kirim_konfirmasi(self, pesanan):
pesan = f"Pesanan #{pesanan['id']} dikonfirmasi. Total: {pesanan['total']}"
self.email.kirim(pesanan["email_pembeli"], pesan)
return True
class TestNotifikasi(unittest.TestCase):
def test_kirim_konfirmasi(self):
layanan_email = LayananEmail()
with patch.object(LayananEmail, "kirim") as mock_kirim:
notif = NotifikasiPesanan(layanan_email)
pesanan = {"id": 42, "total": 150000, "email_pembeli": "[email protected]"}
hasil = notif.kirim_konfirmasi(pesanan)
self.assertTrue(hasil)
mock_kirim.assert_called_once_with(
"[email protected]",
"Pesanan #42 dikonfirmasi. Total: 150000"
)
return_value vs side_effect
#
return_value untuk mengembalikan nilai tetap. side_effect untuk perilaku yang lebih dinamis: nilai berbeda per panggilan, melempar eksepsi, atau menjalankan fungsi kustom.
from unittest.mock import Mock
# return_value: selalu kembalikan nilai yang sama
mock = Mock(return_value=100)
print(mock()) # 100
print(mock()) # 100
# side_effect dengan list: kembalikan nilai berbeda setiap panggilan
mock = Mock(side_effect=[10, 20, 30])
print(mock()) # 10
print(mock()) # 20
print(mock()) # 30
# mock() # StopIteration — list habis
# side_effect dengan Exception: lempar eksepsi
mock = Mock(side_effect=ConnectionError("Server tidak bisa dihubungi"))
try:
mock()
except ConnectionError as e:
print(e) # Server tidak bisa dihubungi
# side_effect dengan fungsi: logika kustom berdasarkan argumen
def respons_dinamis(url):
if "produk" in url:
return Mock(json=lambda: {"nama": "Laptop"})
return Mock(json=lambda: {"error": "Not found"})
mock_get = Mock(side_effect=respons_dinamis)
print(mock_get("https://api.example.com/produk/1").json()) # {"nama": "Laptop"}
print(mock_get("https://api.example.com/lain").json()) # {"error": "Not found"}
Mock Assertions — Verifikasi Pemanggilan #
Setelah kode dijalankan, kamu bisa memverifikasi apakah mock dipanggil dengan cara yang benar.
from unittest.mock import Mock, call
mock = Mock()
# Panggil mock beberapa kali
mock("pertama", kunci="nilai")
mock("kedua")
# Verifikasi pemanggilan terakhir
mock.assert_called_with("kedua")
# Verifikasi pemanggilan pertama (pakai call_args_list)
self.assertEqual(mock.call_args_list[0], call("pertama", kunci="nilai"))
# Verifikasi jumlah pemanggilan
self.assertEqual(mock.call_count, 2)
# Verifikasi dipanggil tepat sekali dengan argumen tertentu
mock2 = Mock()
mock2("satu-satunya")
mock2.assert_called_once_with("satu-satunya")
# Verifikasi TIDAK pernah dipanggil
mock3 = Mock()
mock3.assert_not_called()
Kasus Nyata: Mock datetime.now()
#
Menguji kode yang bergantung pada waktu saat ini punya gotcha tersendiri — kamu tidak bisa patch datetime.datetime langsung karena ia adalah built-in C type.
# modul_laporan.py
from datetime import datetime
def buat_laporan(data):
waktu_buat = datetime.now().strftime("%Y-%m-%d %H:%M")
return {"waktu": waktu_buat, "data": data}
import unittest
from unittest.mock import patch
from datetime import datetime
class TestLaporan(unittest.TestCase):
@patch("modul_laporan.datetime")
def test_buat_laporan(self, mock_datetime):
# patch datetime di modul yang menggunakannya
mock_datetime.now.return_value = datetime(2025, 6, 15, 10, 30)
hasil = buat_laporan({"item": "Laptop"})
self.assertEqual(hasil["waktu"], "2025-06-15 10:30")
self.assertEqual(hasil["data"], {"item": "Laptop"})
Anti-Pattern Mocking yang Harus Dihindari #
# ✗ Anti-pattern 1: Mock terlalu banyak — test tidak menguji apapun yang nyata
def test_buat_pesanan():
with patch("modul.ambil_produk") as mp, \
patch("modul.hitung_total") as mh, \
patch("modul.simpan_ke_db") as ms, \
patch("modul.kirim_notifikasi") as mn:
mp.return_value = {"nama": "Laptop"}
mh.return_value = 12000000
ms.return_value = True
mn.return_value = True
hasil = buat_pesanan(1, 1)
# Test ini hanya membuktikan bahwa fungsi memanggil mock — bukan logika bisnis
# ✓ Solusi: mock hanya dependensi eksternal (I/O, API), uji logika bisnis sungguhan
# ✗ Anti-pattern 2: patch tanpa spec — menyembunyikan typo
mock_db = Mock()
mock_db.simpann(data) # typo 'simpann' tidak terdeteksi, test tetap lulus
# ✓ Solusi: selalu gunakan spec=
mock_db = Mock(spec=DatabaseService)
# ✗ Anti-pattern 3: assert_called_once_with setelah banyak panggilan
mock.metode("a")
mock.metode("b")
mock.metode.assert_called_once_with("b") # GAGAL — dipanggil dua kali, bukan sekali
# ✓ Solusi: gunakan assert_called_with (panggilan terakhir) atau periksa call_args_list
mock.metode.assert_called_with("b") # verifikasi panggilan terakhir
Ringkasan #
- Mock menggantikan dependensi eksternal agar test cepat, deterministik, dan terisolasi dari jaringan, database, atau sistem eksternal.
Mockuntuk objek biasa;MagicMockuntuk objek yang menggunakan magic methods (len(),iter(), context manager).- Selalu gunakan
spec=— tanpanya, typo nama atribut/metode tidak terdeteksi dan test bisa memberikan false positive.- Aturan patch path: patch di mana objek digunakan, bukan di mana ia didefinisikan —
patch("modul_kita.requests.get"), bukanpatch("requests.get").patch.objectlebih eksplisit dan mudah dibaca untuk mock metode pada kelas atau instance tertentu.return_valueuntuk nilai tetap;side_effectuntuk nilai berbeda per panggilan, melempar eksepsi, atau logika kustom.- Verifikasi pemanggilan mock dengan
assert_called_once_with,assert_called_with,assert_not_called, dancall_count.- Jangan over-mock — jika hampir semua dependensi di-mock, test tidak lagi menguji integrasi antar komponen dan bisa memberikan false confidence.