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 menggunakan spec= atau spec_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.
  • Mock untuk objek biasa; MagicMock untuk 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 didefinisikanpatch("modul_kita.requests.get"), bukan patch("requests.get").
  • patch.object lebih eksplisit dan mudah dibaca untuk mock metode pada kelas atau instance tertentu.
  • return_value untuk nilai tetap; side_effect untuk nilai berbeda per panggilan, melempar eksepsi, atau logika kustom.
  • Verifikasi pemanggilan mock dengan assert_called_once_with, assert_called_with, assert_not_called, dan call_count.
  • Jangan over-mock — jika hampir semua dependensi di-mock, test tidak lagi menguji integrasi antar komponen dan bisa memberikan false confidence.

← Sebelumnya: Unit Test   Berikutnya: JSON →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact