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

Praktik: Mocking & Test Doubles

Kenapa layanan eksternal tidak boleh dipanggil sungguhan saat test, mocking dengan pytest-mock, aturan "patch di tempat dipakai", dan memverifikasi interaksi lewat assert_called_once_with.

10 Juli 2026

Sejauh ini semua dependency yang kita "ganti" saat test (database, Redis) diganti dengan versi yang BENAR-BENAR BEKERJA — SQLite in-memory beneran menyimpan data, FakeRedis beneran menghitung counter. Tapi ada satu jenis dependency yang tidak bisa (dan tidak boleh) diganti dengan versi kerja: panggilan ke layanan EKSTERNAL yang di luar kendali kita. Di artikel ini kita bahas mocking — istilah yang sudah disinggung di artikel tentang anatomi test yang baik, sekarang kita praktikkan sungguhan.

Fitur Baru: Notifikasi Saat Task Selesai

task-tracker-api sekarang punya fitur baru — mengirim notifikasi (simulasi webhook) setiap kali task ditandai selesai:

# app/notifications.py
import httpx

NOTIFICATION_ENDPOINT = "https://notifications.example-service.test/notify"


def send_task_completed_notification(task_title: str) -> None:
    response = httpx.post(
        NOTIFICATION_ENDPOINT,
        json={"message": f"Task selesai: {task_title}"},
        timeout=5.0,
    )
    response.raise_for_status()

Dipanggil dari endpoint update:

# app/routers/tasks.py
from app.notifications import send_task_completed_notification


@router.patch("/{task_id}", response_model=TaskRead)
def update_task(...):
    ...
    if not was_done and task.done:
        send_task_completed_notification(task.title)
    return task

Membuktikan Kenapa Kita TIDAK BISA Memanggilnya Sungguhan Saat Test

NOTIFICATION_ENDPOINT sengaja diarahkan ke domain yang tidak benar-benar ada. Coba jalankan test TANPA mocking apa pun:

def test_without_mock_real_notification_call_fails(client: TestClient):
    token = register_and_login(client)
    created = client.post("/api/tasks", json={"title": "Tanpa mock"}, headers=auth_headers(token))
    task_id = created.json()["id"]

    with pytest.raises(httpx.HTTPError):
        client.patch(f"/api/tasks/{task_id}", json={"done": True}, headers=auth_headers(token))

Test ini LOLOS justru karena kita MENGHARAPKAN error — pytest.raises(httpx.HTTPError) memverifikasi bahwa memang terjadi error koneksi. Ini membuktikan masalahnya nyata: kalau test lain memanggil endpoint yang sama TANPA antisipasi ini, test itu akan gagal (atau di kasus domain yang benar-benar merespons lambat, jadi lambat) karena mencoba menghubungi layanan yang tidak pernah akan merespons dengan benar. Bayangkan kalau ini API pembayaran sungguhan — kita jelas tidak mau test kita benar-benar memproses pembayaran setiap kali dijalankan.

Mocking dengan pytest-mock

Solusinya: ganti fungsi send_task_completed_notification dengan versi PALSU khusus untuk test itu, yang tidak benar-benar menghubungi jaringan:

def test_completing_task_sends_notification(client: TestClient, mocker):
    mock_send = mocker.patch("app.routers.tasks.send_task_completed_notification")

    token = register_and_login(client)
    created = client.post("/api/tasks", json={"title": "Belajar mocking"}, headers=auth_headers(token))
    task_id = created.json()["id"]

    client.patch(f"/api/tasks/{task_id}", json={"done": True}, headers=auth_headers(token))

    mock_send.assert_called_once_with("Belajar mocking")

mocker adalah fixture dari plugin pytest-mock (instalasi: pip install pytest-mock), pembungkus yang lebih nyaman dari unittest.mock bawaan Python. mocker.patch(...) mengganti fungsi target dengan objek Mock — objek yang menerima pemanggilan apa pun tanpa benar-benar melakukan apa-apa, TAPI mencatat setiap pemanggilan itu (persis definisi "mock" dari artikel tentang anatomi test, beda dengan "fake" yang benar-benar bekerja).

mock_send.assert_called_once_with("Belajar mocking") adalah inti dari mocking: kita tidak cuma memverifikasi HASIL AKHIR (response API), tapi memverifikasi INTERAKSI — bahwa fungsi notifikasi benar-benar dipanggil, TEPAT SATU KALI, dengan argumen yang benar.

Detail Krusial: "Patch di Tempat Dipakai", Bukan di Tempat Didefinisikan

Perhatikan baik-baik path yang di-patch: "app.routers.tasks.send_task_completed_notification" — BUKAN "app.notifications.send_task_completed_notification", walaupun fungsi itu SEBENARNYA didefinisikan di app/notifications.py.

Ini kesalahan paling umum saat belajar mocking di Python. Alasannya soal cara Python meng-import: baris from app.notifications import send_task_completed_notification di tasks.py membuat NAMA BARU send_task_completed_notification di dalam namespace modul app.routers.tasks, menunjuk ke fungsi yang sama. Kalau kita patch di app.notifications, kita cuma mengganti nama di TEMPAT ASALNYA — nama yang sudah di-import ke app.routers.tasks tetap menunjuk ke fungsi ASLI, tidak ikut berubah.

Aturan praktis: patch selalu di modul TEMPAT fungsi itu DIPAKAI (dipanggil), bukan tempat dia didefinisikan.

Memverifikasi Kasus Negatif Juga

def test_creating_task_does_not_send_notification(client: TestClient, mocker):
    mock_send = mocker.patch("app.routers.tasks.send_task_completed_notification")

    token = register_and_login(client)
    client.post("/api/tasks", json={"title": "Task baru"}, headers=auth_headers(token))

    mock_send.assert_not_called()


def test_marking_already_done_task_does_not_notify_again(client: TestClient, mocker):
    mock_send = mocker.patch("app.routers.tasks.send_task_completed_notification")

    token = register_and_login(client)
    created = client.post("/api/tasks", json={"title": "Belajar mocking"}, headers=auth_headers(token))
    task_id = created.json()["id"]

    client.patch(f"/api/tasks/{task_id}", json={"done": True}, headers=auth_headers(token))
    client.patch(f"/api/tasks/{task_id}", json={"done": True}, headers=auth_headers(token))  # PATCH lagi, masih "done"

    mock_send.assert_called_once()  # bukan dua kali

Test kedua ini membuktikan logic if not was_done and task.done: di endpoint kita bekerja benar — notifikasi cuma terkirim saat task TRANSISI dari belum selesai ke selesai, bukan setiap kali endpoint update dipanggil.

Rangkuman

  • Layanan eksternal (API pembayaran, notifikasi, email) TIDAK BOLEH dipanggil sungguhan saat test — lambat, tidak konsisten, dan bisa punya efek samping nyata.
  • mocker.patch(...) dari pytest-mock mengganti fungsi dengan objek Mock yang mencatat setiap pemanggilannya.
  • assert_called_once_with(...), assert_not_called() memverifikasi INTERAKSI, bukan cuma hasil akhir.
  • Patch di tempat dipakai, bukan di tempat didefinisikan — kesalahan paling umum saat mocking di Python.
  • Topik

    TestingMocking