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') v

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

Service Worker vs Node Handlers

MSW punya dua mode, dipilih sesuai environment:

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

Yang akan kamu pelajari