API Mocking dengan MSW — Testing

Mengapa MSW? Mock Service Worker (MSW) menangkap request HTTP di level network, bukan di level modul. Bedanya signifikan: // Pendekatan lama: vi.mock('./api')…

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 fetch ke axios. Test yang memalsukan fetch dengan vi.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" },
    ]);
  }),
];

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());
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

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.

Yang akan kamu pelajari