Mengapa MSW?
Mock Service Worker (MSW) menangkap request HTTP di level network, bukan di level modul. Bedanya signifikan:
// Pendekatan lama: vi.mock('./api')
vi.mock("./api", () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: "Budi" }),
}));
// Masalah: test tidak menguji fetch/axios asli, hanya mengganti fungsi.
// Kalau ganti library HTTP client, semua mock harus ditulis ulang.
MSW tidak mengganti kodemu. Kode aplikasi tetap memanggil fetch() atau axios seperti biasa — MSW mencegat request di network layer dan membalasnya. Hasilnya: kode yang diuji lebih realistis.
Keuntungan MSW
- Library-agnostic: jalan dengan fetch, axios, apollo, dll tanpa perubahan
- Reusable: handler yang sama dipakai di unit test, Storybook, dan development
- Realistic: menguji request/response seperti di produksi, termasuk header dan status code
Service Worker vs Node Handlers
MSW punya dua mode, dipilih sesuai environment:
- Service Worker (browser) — untuk E2E, Storybook, atau development. File
mockServiceWorker.jsmencegat fetch di browser. - Node handlers (
setupServer) — untuk Vitest/Jest. Mencegat request di proses Node viahttpinterceptor.
Setup untuk Vitest
npm install -D msw
// src/mocks/handlers.ts
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/users", () => {
return HttpResponse.json([
{ id: 1, name: "Budi" },
{ id: 2, name: "Siti" },
]);
}),
http.get("/api/users/:id", ({ params }) => {
return HttpResponse.json({ id: Number(params.id), name: "Budi" });
}),
http.post("/api/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: 99, ...body }, { status: 201 });
}),
];
// src/mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
// src/test/setup.ts — dipanggil otomatis oleh Vitest
import { beforeAll, afterEach, afterAll } from "vitest";
import { server } from "../mocks/server";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers()); // penting: reset per-test
afterAll(() => server.close());
Mensimulasikan Error & Delay
import { http, HttpResponse, delay } from "msw";
// 500 error
http.get("/api/users", () => {
return new HttpResponse(null, { status: 500 });
});
// Network delay
http.get("/api/slow", async () => {
await delay(2000);
return HttpResponse.json({ ok: true });
});
// Timeout/abort
http.get("/api/timeout", () => HttpResponse.error());
Per-Test Override (Scenarios)
server.use() menambahkan handler yang hanya berlaku untuk test saat ini — di-reset oleh resetHandlers():
import { http, HttpResponse } from "msw";
import { server } from "./mocks/server";
it("menampilkan pesan error saat API gagal", async () => {
server.use(
http.get("/api/users", () => {
return new HttpResponse(null, { status: 500 });
})
);
render(<UserList />);
expect(await screen.findByText("Gagal memuat")).toBeInTheDocument();
});
Integrasi dengan React Testing Library
Tidak perlu setup khusus — komponen fetch data seperti biasa, MSW yang mencegat:
it("menampilkan daftar user dari API", async () => {
render(<UserList />);
// Awalnya loading
expect(screen.getByText("Memuat...")).toBeInTheDocument();
// findBy menunggu hasil fetch dari MSW
expect(await screen.findByText("Budi")).toBeInTheDocument();
expect(screen.getByText("Siti")).toBeInTheDocument();
});
Best Practices
onUnhandledRequest: "error"— gagalkan test kalau ada request tak ter-mock (mencegah request asli bocor)- Reset handler per-test — jangan biarkan override dari test A mempengaruhi test B
- Satu sumber handler — share antara test, Storybook, dan dev mock
- Prefer MSW daripada
vi.mockuntuk HTTP — lebih realistis dan library-agnostic