MSW (Mock Service Worker) adalah library yang mencegat request HTTP di level network, lalu membalasnya dengan jawaban yang kamu siapkan. Kode aplikasimu tetap memanggil fetch atau axios seperti biasa, dan gak tahu jawabannya datang dari MSW, bukan dari server sungguhan. Bayangin operator telepon di kantor. Karyawan menelepon nomor luar seperti biasa, tapi operator mencegat panggilan ke nomor tertentu dan menjawabnya sendiri pakai naskah yang sudah disiapkan. Karyawannya gak perlu diubah, cuma operatornya yang diberi naskah.
🧪 Tebakan cepat: Kode aplikasimu pindah dari
fetchke axios. Test yang memalsukanfetchdenganvi.spyOn(globalThis, "fetch")dan test yang memakai MSW: mana yang tetap jalan tanpa diubah? Jawabannya ada di bagian "Kenapa di level network".
Kode yang mau diuji
// api.js
export async function ambilUsers() {
const res = await fetch("https://api.toko.test/users");
if (!res.ok) throw new Error(`Gagal memuat (${res.status})`);
return res.json();
}
// users.js
export async function namaUser() {
const users = await ambilUsers();
return users.map((u) => u.name);
}
Test-nya harus jalan tanpa server api.toko.test sungguhan, dan juga harus bisa mencoba jawaban gagal seperti status 500.
Handler: naskah jawabannya
// mocks/handlers.js
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("https://api.toko.test/users", () => {
return HttpResponse.json([
{ id: 1, name: "Budi" },
{ id: 2, name: "Siti" },
]);
}),
];
http.get(url, resolver)mendaftarkan handler untuk request GET ke URL itu. Ada jugahttp.post,http.put, danhttp.delete.- Resolver (function kedua) dijalankan tiap kali ada request yang cocok, dan yang dikembalikannya jadi response.
HttpResponse.json(data)membuat response JSON berstatus 200. Status lain:HttpResponse.json(data, { status: 201 })ataunew HttpResponse(null, { status: 500 }).- Path bisa punya parameter,
http.get("/users/:id", ({ params }) => ...), dan body request POST dibaca denganawait request.json().
Pasang di Vitest
// mocks/server.js
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
// test/setup.js, didaftarkan di test.setupFiles pada vitest.config.js
import { beforeAll, afterEach, afterAll } from "vitest";
import { server } from "../mocks/server";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
setupServerdarimsw/nodedipakai di test (Node). Di browser, untuk development atau Storybook, pakaisetupWorkerdarimsw/browserdengan filemockServiceWorker.js. Handler-nya sama.listenmulai mencegat.onUnhandledRequest: "error"bikin request tanpa handler menggagalkan test. Bawaannya cuma"warn": diberi peringatan, lalu request-nya diteruskan ke internet sungguhan.resetHandlersmembuang handler tambahan dari test sebelumnya, dancloseberhenti mencegat.
describe("namaUser", () => {
it("mengembalikan nama semua user", async () => {
expect(await namaUser()).toEqual(["Budi", "Siti"]);
});
it("melempar error saat server menjawab 500", async () => {
server.use(http.get("https://api.toko.test/users", () => new HttpResponse(null, { status: 500 })));
await expect(namaUser()).rejects.toThrow("Gagal memuat (500)");
});
});
server.use(...) menambah handler khusus untuk test itu, yang dicek lebih dulu daripada handler bawaan. await expect(...).rejects.toThrow(...) memeriksa bahwa Promise-nya gagal dengan error itu. Test async dibahas lebih dalam di pelajaran berikutnya.
Cara MSW memilih jawaban
MSW menyimpan daftar handler. Tiap request dicocokkan dari atas: handler pertama yang method dan URL-nya cocok yang menjawab.
| Saat | Daftar handler | GET /users dijawab |
|---|---|---|
| Awal | GET /users → 200 |
200, Budi dan Siti |
Setelah server.use(...) |
GET /users → 500, lalu GET /users → 200 |
500 |
Setelah resetHandlers() |
GET /users → 200 |
200 lagi |
Request GET /orders |
gak ada yang cocok | ditolak, test gagal |
Kalau ditulis sendiri, intinya tiga bagian: menambah handler ke array (handlers.push(...)), mencari yang cocok (handlers.find(...)) lalu menjalankannya atau melempar error kalau gak ada, dan mengosongkan array (handlers.length = 0).
Kenapa di level network
| Cara memalsukan | Kode yang ikut teruji | URL di api.js salah ketik |
Pindah dari fetch ke axios |
|---|---|---|---|
vi.mock("./api") |
cuma namaUser, api.js gak jalan |
test tetap hijau, bug lolos | test gak terpengaruh, karena api.js memang gak dites |
vi.spyOn(globalThis, "fetch") |
namaUser dan api.js, tapi fetch palsu menjawab URL apa pun |
test tetap hijau, bug lolos | rusak: axios gak lewat fetch, request-nya lolos ke internet |
| MSW | semuanya, termasuk fetch atau axios yang asli |
test gagal: [MSW] Error: intercepted a request without a matching request handler |
tetap jalan tanpa diubah |
MSW gak peduli library HTTP apa yang dipakai (library-agnostic), karena yang dicegat request-nya di level network, bukan fungsinya. Jadi jawaban tebakannya: test dengan MSW tetap jalan, test yang memalsukan fetch rusak.
⚠️ Jebakan yang sering ketemu
- Lupa
resetHandlers. Handler 500 dari satu test terbawa ke test berikutnya, dan test itu gagal denganGagal memuat (500). - Membiarkan
onUnhandledRequestdi bawaannya. Request tanpa handler cuma diberi peringatan dan diteruskan ke internet. Pakai"error"supaya langsung ketahuan. - Method atau URL handler beda dengan request-nya.
http.getgak menjawab POST, dan/usersgak cocok dengan/user. - Sintaks MSW lama.
rest.getdanres(ctx.json(...))itu MSW versi 1. Versi 2 memakaihttp.getdanHttpResponse.json(...).
TL;DR: MSW mencegat request di level network, jadi kode fetch atau axios-mu jalan apa adanya dan cuma jawabannya yang palsu. Handler ditulis http.get(url, () => HttpResponse.json(data)). Di Vitest, setupServer(...handlers) dari msw/node dipasang dengan server.listen({ onUnhandledRequest: "error" }) di beforeAll, server.resetHandlers() di afterEach, dan server.close() di afterAll. server.use(...) mengganti jawaban untuk satu test, misalnya status 500. Kelebihannya dibanding vi.mock("./api"): kode API-mu ikut teruji, dan test-nya gak peduli library HTTP apa yang kamu pakai.