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");lalumath.calculate(2, 3), padahal aslinyacalculatemenjumlahkan. Hasilnya 5 atauundefined? 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);
});
vi.fn()membuat fungsi yang gak melakukan apa-apa (mengembalikanundefined), tapi mencatat tiap pemanggilan: berapa kali, dengan argumen apa. Catatannya ada dibayar.mock.calls, di sini[[90000]].mockReturnValue(nilai)membuatnya selalu mengembalikannilai.mockImplementation((a, b) => a + b)memberinya isi pengganti.toHaveBeenCalled()memeriksa pernah dipanggil,toHaveBeenCalledTimes(1)berapa kali,toHaveBeenCalledWith(90000)dengan argumen apa. Ketiganya cuma bisa dipakai pada tiruan.
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.spyOn(objek, "namaMethod")membungkus method aslinya: tiap pemanggilan dicatat, dan isi aslinya tetap dijalankan. Jadi hasilnya 5, bukanundefined. Itu jawaban tebakannya.- Panggil lewat objeknya,
math.calculate(...), karena yang dibungkus adalah method di objek itu. - Mau aslinya diam?
spy.mockImplementation(() => {}).spy.mockRestore()mengembalikan method aslinya.
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
- Semua di-mock. Kalau semuanya tiruan, yang kamu uji cuma tiruanmu sendiri. Tiru yang lambat, berbayar, atau punya efek samping saja.
- Memanggil lewat salinan.
const hitung = math.calculate;laluhitung(2, 3)setelahspyOn: spy-nya gak mencatat apa pun. Panggilmath.calculate(2, 3). - Lupa membersihkan. Catatan panggilan bocor ke test berikutnya, dan spy tetap terpasang.
- Matcher tiruan pada fungsi biasa.
expect(email.kirim).toHaveBeenCalled()tanpavi.fnatauvi.spyOngagal dengan[Function kirim] is not a spy or a call to a spy!.
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().