Mocking #
Mocking adalah teknik mengganti dependensi nyata dengan versi tiruan yang perilakunya bisa dikontrol saat testing. Tanpa mocking, unit test menjadi integration test — lambat, tidak deterministic, dan sulit diisolasi. Tapi mocking yang berlebihan atau salah jenis juga berbahaya: test yang terlalu banyak mock tidak membuktikan bahwa sistem bekerja secara keseluruhan, dan ketika implementasi nyata berubah, mock yang sudah tidak akurat bisa membuat test tetap hijau padahal ada bug. Kunci dari mocking yang efektif adalah memahami taksonomi test double — ada lima jenis yang berbeda, masing-masing untuk kasus yang berbeda — dan memilih yang paling sederhana untuk kasus yang sedang diuji.
Taksonomi Test Double #
Test double adalah istilah umum untuk semua jenis pengganti dalam testing. Ada lima jenis berbeda dengan tujuan yang berbeda:
flowchart TD
A[Test Double] --> B[Dummy]
A --> C[Stub]
A --> D[Spy]
A --> E[Mock]
A --> F[Fake]
B --> B1[Mengisi parameter wajib\ntapi tidak pernah digunakan\ncth: null, objek kosong]
C --> C1[Mengembalikan nilai yang\nditetapkan — tidak peduli\nbagaimana dipanggil]
D --> D1[Implementasi nyata berjalan\ntapi pemanggilan dicatat\nuntuk diverifikasi kemudian]
E --> E1[Implementasi nyata diganti\nDAN verifikasi interaksi\nadalah tujuan utama test]
F --> F1[Implementasi alternatif yang\nbenar-benar berfungsi\ncth: in-memory database]
style C fill:#339af0,color:#fff
style D fill:#fcc419,color:#000
style E fill:#ff6b6b,color:#fff
style F fill:#51cf66,color:#fffjest.fn() — Mock Function Dasar
#
jest.fn() membuat fungsi mock kosong yang mencatat semua pemanggilan:
// Mock function dasar
const mockKirimEmail = jest.fn();
// Verifikasi pemanggilan
mockKirimEmail("[email protected]", "Subjek", "Isi");
expect(mockKirimEmail).toHaveBeenCalledTimes(1);
expect(mockKirimEmail).toHaveBeenCalledWith("[email protected]", "Subjek", "Isi");
// Konfigurasi return value
const mockHitungDiskon = jest.fn().mockReturnValue(90_000);
expect(mockHitungDiskon(100_000, 10)).toBe(90_000);
// Return value berbeda per pemanggilan
const mockAmbilData = jest
.fn()
.mockReturnValueOnce("data pertama") // Pemanggilan 1
.mockReturnValueOnce("data kedua") // Pemanggilan 2
.mockReturnValue("data default"); // Pemanggilan selanjutnya
// Async mock
const mockFetch = jest.fn().mockResolvedValue({ status: 200, data: [] });
const mockGagal = jest.fn().mockRejectedValue(new Error("Network error"));
// Mock dengan implementasi kustom
const mockFilter = jest.fn().mockImplementation((arr: number[], min: number) =>
arr.filter((n) => n >= min)
);
jest.Mocked<T> — Typed Mock untuk Interface
#
Saat bekerja dengan interface atau kelas, jest.Mocked<T> memastikan semua properti mock bertipe jest.MockedFunction yang benar:
// Interface yang akan di-mock
interface RepositoriProduk {
ambilById(id: string): Promise<Produk | null>;
simpan(produk: Produk): Promise<void>;
hapus(id: string): Promise<boolean>;
cari(query: string): Promise<Produk[]>;
}
interface Produk {
id: string;
nama: string;
harga: number;
stok: number;
}
// Membuat typed mock untuk interface
function buatMockRepo(): jest.Mocked<RepositoriProduk> {
return {
ambilById: jest.fn(),
simpan: jest.fn(),
hapus: jest.fn(),
cari: jest.fn(),
};
}
// Service yang akan ditest
class LayananProduk {
constructor(private readonly repo: RepositoriProduk) {}
async tambahStok(id: string, jumlah: number): Promise<void> {
const produk = await this.repo.ambilById(id);
if (!produk) throw new Error(`Produk ${id} tidak ditemukan`);
if (jumlah <= 0) throw new RangeError("Jumlah harus positif");
produk.stok += jumlah;
await this.repo.simpan(produk);
}
async hapusProduk(id: string): Promise<void> {
const produk = await this.repo.ambilById(id);
if (!produk) throw new Error(`Produk ${id} tidak ditemukan`);
const berhasil = await this.repo.hapus(id);
if (!berhasil) throw new Error("Gagal menghapus produk dari database");
}
}
// Test menggunakan typed mock
describe("LayananProduk", () => {
let mockRepo: jest.Mocked<RepositoriProduk>;
let layanan: LayananProduk;
const produkContoh: Produk = {
id: "PRD-001",
nama: "Kurma Ajwa",
harga: 85_000,
stok: 50,
};
beforeEach(() => {
mockRepo = buatMockRepo();
layanan = new LayananProduk(mockRepo);
});
describe("tambahStok", () => {
test("menambah stok dan menyimpan produk", async () => {
// Arrange
mockRepo.ambilById.mockResolvedValue({ ...produkContoh }); // Salinan agar tidak mutable
// Act
await layanan.tambahStok("PRD-001", 10);
// Assert
expect(mockRepo.ambilById).toHaveBeenCalledWith("PRD-001");
expect(mockRepo.simpan).toHaveBeenCalledWith(
expect.objectContaining({ stok: 60 }) // 50 + 10
);
});
test("melempar error jika produk tidak ditemukan", async () => {
mockRepo.ambilById.mockResolvedValue(null);
await expect(layanan.tambahStok("PRD-999", 5)).rejects.toThrow(
"Produk PRD-999 tidak ditemukan"
);
expect(mockRepo.simpan).not.toHaveBeenCalled();
});
test("melempar RangeError untuk jumlah negatif", async () => {
mockRepo.ambilById.mockResolvedValue({ ...produkContoh });
await expect(layanan.tambahStok("PRD-001", -1)).rejects.toThrow(RangeError);
expect(mockRepo.simpan).not.toHaveBeenCalled();
});
});
});
jest.spyOn() — Memata-matai Implementasi Nyata
#
jest.spyOn() berbeda dari jest.fn() — ia mempertahankan implementasi asli sambil mencatat pemanggilan. Gunakan ini saat kamu ingin memverifikasi interaksi tapi masih ingin implementasi nyata berjalan, atau saat ingin mengganti implementasi sementara:
// Modul yang berisi fungsi yang akan di-spy
import * as Utils from "./utils";
// Spy tanpa mengubah implementasi — masih memanggil fungsi asli
const spy = jest.spyOn(Utils, "formatMata");
Utils.formatMata(100_000);
expect(spy).toHaveBeenCalledWith(100_000);
// Implementasi nyata tetap berjalan
// Spy dengan override implementasi sementara
jest.spyOn(Utils, "formatMata").mockReturnValue("Rp 100.000");
// Setelah test selesai, jest.restoreMocks() mengembalikan implementasi asli
// Spy pada metode kelas
class KalkulatorHarga {
private hitungPPN(harga: number): number {
return harga * 0.11;
}
hitungTotal(harga: number): number {
return harga + this.hitungPPN(harga);
}
}
test("hitungTotal menggunakan PPN yang benar", () => {
const kalkulator = new KalkulatorHarga();
// Spy pada metode private via cast
const spyPPN = jest
.spyOn(kalkulator as unknown as { hitungPPN: (h: number) => number }, "hitungPPN")
.mockReturnValue(11_000);
const total = kalkulator.hitungTotal(100_000);
expect(spyPPN).toHaveBeenCalledWith(100_000);
expect(total).toBe(111_000);
});
jest.mock() — Mock Seluruh Modul
#
jest.mock() mengganti seluruh modul dengan implementasi mock. Jest secara otomatis mengangkat (hoist) panggilan jest.mock() ke atas file, sehingga selalu dieksekusi sebelum import apapun:
// src/layanan/email.ts — modul yang akan di-mock
export async function kirimEmail(
tujuan: string,
subjek: string,
isi: string
): Promise<void> {
// Implementasi nyata: panggil SMTP server
console.log(`Mengirim email ke ${tujuan}`);
}
export async function kirimEmailBulk(
tujuan: string[],
subjek: string
): Promise<void> {
await Promise.all(tujuan.map((t) => kirimEmail(t, subjek, "")));
}
// src/layanan/email.test.ts
import { kirimEmail, kirimEmailBulk } from "./email";
// jest.mock di-hoist ke atas — selalu dieksekusi sebelum import
jest.mock("./email");
// Dapatkan referensi yang sudah ter-mock
const mockKirimEmail = kirimEmail as jest.MockedFunction<typeof kirimEmail>;
const mockKirimEmailBulk = kirimEmailBulk as jest.MockedFunction<typeof kirimEmailBulk>;
beforeEach(() => {
mockKirimEmail.mockResolvedValue(undefined);
mockKirimEmailBulk.mockResolvedValue(undefined);
});
test("kirimEmail dipanggil dengan argumen yang benar", async () => {
await kirimEmail("[email protected]", "Test", "Isi test");
expect(mockKirimEmail).toHaveBeenCalledWith(
"[email protected]",
"Test",
"Isi test"
);
});
Partial Mock — Mock Sebagian Modul #
Sering kamu hanya perlu mock beberapa fungsi dari modul, bukan semuanya:
// Mock sebagian modul — gunakan jest.requireActual untuk sisanya
jest.mock("./utils", () => {
const implementasiAsli = jest.requireActual<typeof import("./utils")>("./utils");
return {
...implementasiAsli, // Gunakan implementasi nyata untuk sebagian besar
kirimNotifikasi: jest.fn(), // Hanya mock ini
catatAudit: jest.fn(), // Dan ini
};
});
Manual Mock — Folder __mocks__
#
Untuk dependensi yang di-mock di banyak test file, buat manual mock di folder __mocks__ yang berdampingan dengan file aslinya:
src/
├── layanan/
│ ├── __mocks__/
│ │ └── database.ts ← Manual mock otomatis digunakan
│ └── database.ts ← Implementasi nyata
└── ...
// src/layanan/__mocks__/database.ts — Manual mock
import type { KoneksiDatabase } from "../database";
// Implementasi fake yang benar-benar berfungsi di memori
export class KoneksiDatabaseMock implements KoneksiDatabase {
private data = new Map<string, unknown>();
async query<T>(sql: string, params?: unknown[]): Promise<T[]> {
// Implementasi in-memory sederhana untuk testing
console.log(`[DB Mock] Query: ${sql}`, params);
return [] as T[];
}
async execute(sql: string, params?: unknown[]): Promise<{ rowsAffected: number }> {
console.log(`[DB Mock] Execute: ${sql}`, params);
return { rowsAffected: 1 };
}
async tutup(): Promise<void> {
this.data.clear();
}
}
// Aktifkan manual mock di test:
// jest.mock("../database");
Mock Date dan Timer #
// Kode yang bergantung pada tanggal/waktu — sulit ditest tanpa mock
export function apakahKadaluwarsa(kedaluwarsa: Date): boolean {
return new Date() > kedaluwarsa;
}
export function hitungUmurHari(tglLahir: Date): number {
const sekarang = new Date();
const diff = sekarang.getTime() - tglLahir.getTime();
return Math.floor(diff / (1000 * 60 * 60 * 24));
}
// Test dengan kontrol waktu
describe("apakahKadaluwarsa", () => {
beforeEach(() => {
// Set waktu saat ini ke tanggal yang kita kontrol
jest.useFakeTimers();
jest.setSystemTime(new Date("2025-05-07T12:00:00Z"));
});
afterEach(() => {
jest.useRealTimers(); // Kembalikan timer nyata
});
test("mengembalikan true jika sudah kadaluwarsa", () => {
const kemarin = new Date("2025-05-06T12:00:00Z");
expect(apakahKadaluwarsa(kemarin)).toBe(true);
});
test("mengembalikan false jika belum kadaluwarsa", () => {
const besok = new Date("2025-05-08T12:00:00Z");
expect(apakahKadaluwarsa(besok)).toBe(false);
});
test("hitungUmurHari menghitung dengan benar", () => {
const tglLahir = new Date("2025-05-04T12:00:00Z"); // 3 hari yang lalu
expect(hitungUmurHari(tglLahir)).toBe(3);
});
});
// Mock timer untuk test async yang bergantung pada setTimeout
describe("dengan fake timers", () => {
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
test("callback dipanggil setelah delay", () => {
const callback = jest.fn();
setTimeout(callback, 5000);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(5000); // Maju 5 detik sekaligus
expect(callback).toHaveBeenCalledTimes(1);
});
});
Mock fetch dengan MSW
#
Untuk mocking HTTP request, msw (Mock Service Worker) adalah solusi terbaik — ia mencegat request di level jaringan sehingga kode produksi tidak perlu diubah:
npm install --save-dev msw
// src/mocks/handlers.ts — definisi handler untuk test
import { http, HttpResponse } from "msw";
export const handlers = [
// Mock endpoint GET pengguna
http.get("https://api.example.com/pengguna/:id", ({ params }) => {
const { id } = params;
if (id === "999") {
return HttpResponse.json({ error: "Tidak ditemukan" }, { status: 404 });
}
return HttpResponse.json({
id,
nama: "Budi Santoso",
email: "[email protected]",
});
}),
// Mock endpoint POST
http.post("https://api.example.com/pengguna", async ({ request }) => {
const body = await request.json();
return HttpResponse.json(
{ id: "usr-baru", ...(body as object) },
{ status: 201 }
);
}),
];
// src/setup-tests.ts — setup MSW untuk Jest
import { setupServer } from "msw/node";
import { handlers } from "./mocks/handlers";
const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: "warn" }));
afterEach(() => server.resetHandlers()); // Reset handler setelah setiap test
afterAll(() => server.close());
// Override handler untuk test tertentu
test("menangani timeout dengan benar", async () => {
server.use(
http.get("https://api.example.com/pengguna/1", () => {
return HttpResponse.error(); // Simulasi network error
})
);
await expect(ambilPengguna("1")).rejects.toThrow();
});
Anti-Pattern: Over-Mocking #
// ANTI-PATTERN: Mock terlalu banyak — test ini tidak membuktikan apapun
test("getData memproses data", async () => {
// Mock semua dependensi hingga tidak ada kode nyata yang berjalan
jest.mock("./repository");
jest.mock("./validator");
jest.mock("./transformer");
jest.mock("./cache");
const { getData } = await import("./service");
const hasil = await getData("id-123");
// Ini hanya membuktikan bahwa mock bekerja, bukan service-nya
expect(hasil).toBeDefined();
});
// BENAR: Mock hanya dependensi eksternal, biarkan logika bisnis berjalan nyata
test("getData memvalidasi dan mentransformasi data", async () => {
// Hanya mock I/O eksternal (database, API, file system)
const mockRepo = { ambilById: jest.fn().mockResolvedValue(rawData) };
// Validator dan transformer nyata berjalan — inilah yang ditest!
const layanan = new LayananData(mockRepo, new ValidatorNyata(), new TransformerNyata());
const hasil = await layanan.getData("id-123");
expect(hasil).toMatchObject({ ... }); // Verifikasi transformasi nyata
});
Kapan Fake Lebih Baik dari Mock #
Fake adalah implementasi alternatif yang benar-benar berfungsi — lebih kompleks dari mock tapi jauh lebih stabil dan ekspresif untuk test suite yang besar:
// Fake — implementasi in-memory yang benar-benar berfungsi
class RepositoriPenggunaFake implements RepositoriPengguna {
private store = new Map<string, Pengguna>();
async simpan(pengguna: Pengguna): Promise<void> {
this.store.set(pengguna.id, { ...pengguna }); // Simpan salinan
}
async ambilById(id: string): Promise<Pengguna | null> {
return this.store.get(id) ?? null;
}
async hapus(id: string): Promise<boolean> {
return this.store.delete(id);
}
async cariByEmail(email: string): Promise<Pengguna | null> {
for (const p of this.store.values()) {
if (p.email === email) return { ...p };
}
return null;
}
// Helper untuk test — reset state antar test
bersihkan(): void {
this.store.clear();
}
// Helper untuk inspeksi — cek state internal
semuaPengguna(): Pengguna[] {
return [...this.store.values()];
}
}
// Gunakan fake di multiple test suite — lebih stabil dan ekspresif dari mock
const repoFake = new RepositoriPenggunaFake();
beforeEach(() => repoFake.bersihkan()); // Reset sebelum setiap test
test("pengguna bisa didaftarkan dan diambil", async () => {
const layanan = new LayananPengguna(repoFake);
await layanan.daftar("[email protected]", "Budi");
const pengguna = await repoFake.ambilById("usr-001");
expect(pengguna?.email).toBe("[email protected]");
});
Ringkasan #
- Pilih test double yang paling sederhana — dummy untuk argumen yang tidak dipakai, stub untuk nilai kembalian, spy untuk verifikasi interaksi sambil mempertahankan implementasi nyata, mock untuk verifikasi interaksi sebagai tujuan utama test, fake untuk implementasi alternatif yang benar-benar berfungsi.
jest.Mocked<T>untuk typed mock — daripadaas anyatauas jest.Mock, gunakanjest.Mocked<T>yang memberikan type checking penuh pada semua method mock.jest.spyOn()+mockReturnValue()untuk override sementara — spy mempertahankan implementasi asli danrestoreMocks: truedijest.config.tsotomatis mengembalikannya setelah setiap test.jest.mock()di-hoist otomatis — Jest mengangkatjest.mock()ke atas file sebelum semua import dieksekusi; ini berarti kamu tidak bisa menggunakan variabel lokal di dalamjest.mock()kecuali jika namanya diawali denganmock.- Partial mock dengan
jest.requireActual— gunakan untuk mock hanya beberapa fungsi dari modul sambil mempertahankan sisanya; ini jauh lebih aman daripada mock seluruh modul.- MSW untuk mock HTTP — lebih baik daripada mock
fetchsecara manual karena mencegat di level jaringan; kode produksi tidak perlu diubah sama sekali.jest.useFakeTimers()untuk kontrol waktu — gunakanjest.setSystemTime()untuk mengontrolnew Date()danjest.advanceTimersByTime()untuk maju waktu tanpa menunggu; selalujest.useRealTimers()diafterEach.- Hindari over-mocking — jika hampir semua dependensi di-mock, test tersebut hanya membuktikan bahwa mock bekerja bukan bahwa kode produksi bekerja; mock hanya dependensi I/O eksternal.
- Fake lebih baik untuk test suite besar — fake yang digunakan berulang di banyak test lebih stabil dan ekspresif dari mock yang perlu di-setup ulang setiap test; pertimbangkan fake untuk repository, cache, dan queue.