Mocking & Stubbing — Testing

Mengapa Perlu Mocking? Saat unit testing, kita ingin menguji satu unit secara terisolasi. Tapi bagaimana jika unit tersebut bergantung pada API, database…

Mocking artinya mengganti bagian yang dipanggil kodemu, seperti layanan pembayaran, pengirim email, atau database, dengan tiruan yang jawabannya kamu atur dan yang mencatat bagaimana ia dipanggil. Tiruan seperti ini disebut test double. Namanya dari stunt double di film: pemeran pengganti untuk adegan berbahaya. Adegannya tetap jalan, tapi aktor aslinya gak perlu lompat dari gedung. Di test, kode yang kamu uji tetap jalan apa adanya, tapi kartu kredit gak benar-benar terdebit dan email gak benar-benar terkirim.

🧪 Tebakan cepat: const spy = vi.spyOn(math, "calculate"); lalu math.calculate(2, 3), padahal aslinya calculate menjumlahkan. Hasilnya 5 atau undefined? Jawabannya ada di bagian "vi.spyOn: mengamati tanpa mengganti".

Kenapa butuh tiruan

function prosesPesanan(pesanan, layanan) {
  const hasil = layanan.bayar(pesanan.total);
  if (hasil.status === "sukses") {
    layanan.kirimEmail(pesanan.email, "Pembayaran diterima");
  }
  return hasil.status;
}

Yang mau diuji logikanya: email cuma dikirim kalau pembayaran sukses. Kalau test-nya memanggil bayar dan kirimEmail yang asli, tiap kali test jalan ada uang yang ditarik, email sungguhan terkirim, dan hasilnya bergantung pada internet. Jadi test memberikan tiruan lewat parameter layanan.

vi.fn(): fungsi tiruan

import { describe, it, expect, vi } from "vitest";

it("mengirim email kalau bayar sukses", () => {
  const bayar = vi.fn();
  bayar.mockReturnValue({ status: "sukses" }); // jawabannya kita atur
  const kirimEmail = vi.fn(); // nanti kita periksa pemanggilannya

  const status = prosesPesanan({ total: 90000, email: "[email protected]" }, { bayar, kirimEmail });

  expect(status).toBe("sukses");
  expect(bayar).toHaveBeenCalledWith(90000);
  expect(kirimEmail).toHaveBeenCalledTimes(1);
});

Kalau prosesPesanan keliru mengirim email walau pembayarannya gagal, test kebalikannya (expect(kirimEmail).not.toHaveBeenCalled()) melapor:

 × pesanan.test.js > prosesPesanan > tidak mengirim email kalau bayar gagal 1ms
   → expected "vi.fn()" to not be called at all, but actually been called 1 times

Di balik layar: tiruan buatan sendiri

function createMock() {
  let returnValue;

  function mockFn(...args) {
    mockFn.callCount++; // catat: dipanggil sekali lagi
    mockFn.lastArgs = args; // catat: argumen terakhir, [] kalau tanpa argumen
    return returnValue;
  }

  mockFn.callCount = 0;
  mockFn.lastArgs = [];
  mockFn.mockReturnValue = function (val) {
    returnValue = val;
  };
  return mockFn;
}

Function di JavaScript juga objek, jadi bisa membawa properti sendiri seperti callCount. Pencatatannya di dalam badan mockFn, jadi terbarui tiap kali dipanggil, dan mockReturnValue cuma menyimpan nilai yang nanti dikembalikan badan itu. vi.fn() bekerja dengan cara yang sama.

vi.spyOn: mengamati tanpa mengganti

const math = {
  calculate(a, b) {
    return a + b;
  },
};

const spy = vi.spyOn(math, "calculate");
const hasil = math.calculate(2, 3);

expect(hasil).toBe(5); // isi aslinya tetap jalan
expect(spy).toHaveBeenCalledWith(2, 3);

vi.mock: mengganti satu modul

import { formatDate } from "./utils";

vi.mock("./utils", () => ({
  formatDate: vi.fn(() => "1 Januari 2025"),
}));

vi.mock mengganti seluruh modul untuk semua import di file test itu. Vitest memindahkan vi.mock ke paling atas file, jadi tiruannya sudah terpasang sebelum import jalan. Pakai untuk modul yang di-import langsung oleh kodemu, bukan yang dikirim lewat parameter.

Lima jenis test double

Jenis Yang dilakukan Contoh
Dummy cuma mengisi parameter wajib, gak pernah dipakai objek kosong untuk parameter logger yang gak disentuh
Stub memberi jawaban tetap bayar.mockReturnValue({ status: "sukses" })
Spy mencatat panggilan, isi aslinya tetap jalan vi.spyOn(math, "calculate")
Mock menggantikan, lalu pemanggilannya diperiksa expect(kirimEmail).toHaveBeenCalledTimes(1)
Fake versi kerja yang ringan database di memori, berupa array, pengganti database asli

Di Vitest, satu vi.fn() bisa jadi stub dan mock sekaligus. Yang gak perlu tiruan sama sekali: pure function tanpa dependency, misalnya hitungTotal. Panggil saja aslinya.

Bersihkan di antara test

Tiruan yang dibuat sekali di atas file menyimpan catatan dari semua test. Test kedua bisa melihat panggilan dari test pertama dan gagal dengan pesan yang sama seperti bug tadi.

beforeEach(() => {
  vi.clearAllMocks(); // hapus catatan panggilan semua tiruan
});

afterEach(() => {
  vi.restoreAllMocks(); // kembalikan method asli yang di-spyOn
});

⚠️ Jebakan yang sering ketemu

TL;DR: Test double menggantikan dependency yang lambat, berbayar, atau punya efek samping. vi.fn() membuat tiruan yang mencatat panggilan, mockReturnValue mengatur jawabannya, dan toHaveBeenCalled, toHaveBeenCalledTimes, toHaveBeenCalledWith memeriksa catatannya. vi.spyOn(objek, "method") mencatat tanpa mengganti, jadi isi aslinya tetap jalan. vi.mock("./modul", ...) mengganti satu modul utuh. Jenisnya: dummy, stub, spy, mock, fake. Pure function gak perlu di-mock, dan tiruan dibersihkan di antara test dengan vi.clearAllMocks() dan vi.restoreAllMocks().

Yang akan kamu pelajari