MOCK-SERVICE-WORKER MSW FRONTEND-TESTING API-MOCKING DEVELOPER-EXPERIENCE WEB-DEVELOPMENT JAVASCRIPT TYPESCRIPT TESTING INTEGRATION-TESTING UNIT-TESTING E2E-TESTING OFFLINE-DEVELOPMENT SERVICE-WORKER BROWSER NODEJS DX

Mock Service Worker (MSW): Membangun Lingkungan Testing & Pengembangan Frontend yang Mandiri dan Cepat

⏱️ 14 menit baca
👨‍💻

Mock Service Worker (MSW): Membangun Lingkungan Testing & Pengembangan Frontend yang Mandiri dan Cepat

1. Pendahuluan

Sebagai developer frontend, kita sering dihadapkan pada skenario ini: Anda sedang mengembangkan fitur baru yang sangat bergantung pada API backend, tetapi API tersebut belum siap, masih dalam pengembangan, atau mungkin sering berubah. Atau, saat menulis tes, Anda harus berinteraksi dengan API nyata yang membuat tes menjadi lambat, tidak stabil, dan sulit direproduksi. Pasti pusing, kan? 😩

Inilah masalah yang ingin dipecahkan oleh Mock Service Worker (MSW). MSW adalah library JavaScript yang memungkinkan Anda mengintercept permintaan jaringan (HTTP/HTTPS) di level browser (menggunakan Service Worker) dan di level Node.js (menggunakan modul http/https). Dengan MSW, Anda bisa membuat mock respons untuk API Anda, sehingga frontend bisa berjalan dan diuji secara independen tanpa perlu backend yang aktif atau stabil.

Bayangkan Anda bisa:

Artikel ini akan membawa Anda menyelami MSW, mulai dari konsep dasar hingga implementasi praktis untuk mempercepat pengembangan dan pengujian aplikasi web Anda. Mari kita mulai! 🚀

2. Apa Itu Mock Service Worker (MSW)?

Pada intinya, MSW memanfaatkan dua teknologi inti untuk bekerja:

  1. Service Worker (untuk lingkungan browser): Service Worker adalah proxy jaringan yang berjalan di latar belakang browser. Ia bisa mengintercept semua permintaan HTTP/HTTPS yang keluar dari aplikasi web Anda. MSW “memasang” dirinya sebagai Service Worker, lalu mencegat permintaan dan mengembalikan respons mock yang telah Anda definisikan. Ini terjadi sebelum permintaan mencapai jaringan sungguhan, sehingga bagi browser, seolah-olah permintaan itu berhasil dan mendapatkan respons dari server.
  2. Modul http/https (untuk lingkungan Node.js): Di lingkungan Node.js (misalnya, untuk tes unit backend, server-side rendering, atau CLI tools), MSW menggunakan modul bawaan Node.js untuk mengintercept permintaan. Konsepnya sama, hanya saja implementasinya berbeda karena tidak ada Service Worker di Node.js.

Perbedaan dengan Mocking Tradisional

Anda mungkin familiar dengan mocking library seperti Jest atau Sinon.js. Mocking tradisional bekerja dengan cara meng-override fungsi atau modul JavaScript secara langsung di level kode.

📌 Contoh Mocking Tradisional (Jest):

// user.js
export async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// user.test.js
import { fetchUser } from './user';

test('should fetch a user', async () => {
  // Mock global fetch function
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({ id: 1, name: 'John Doe' }),
    })
  );

  const user = await fetchUser(1);
  expect(user).toEqual({ id: 1, name: 'John Doe' });
  expect(global.fetch).toHaveBeenCalledWith('/api/users/1');
});

Kelemahan Mocking Tradisional:

Kelebihan MSW:

3. Mengatur MSW di Proyek Frontend Anda (Browser)

Mari kita lihat bagaimana menginstal dan mengkonfigurasi MSW di aplikasi web Anda. Kita akan menggunakan React sebagai contoh, tetapi konsepnya berlaku untuk framework atau library frontend apa pun.

Instalasi

npm install msw --save-dev
# atau
yarn add msw --dev

Inisialisasi Service Worker

MSW perlu mendaftarkan Service Worker-nya. Anda bisa melakukannya dengan perintah CLI:

npx msw init public/ --save

Perintah ini akan membuat file mockServiceWorker.js di folder public/ (atau folder statis lain yang Anda tentukan). Ini adalah Service Worker yang akan digunakan MSW untuk mengintercept request.

Mendefinisikan Mock Handler

Buat file src/mocks/handlers.js untuk mendefinisikan respons mock Anda.

// src/mocks/handlers.js
import { rest } from 'msw';

export const handlers = [
  // Mock request GET ke /api/users
  rest.get('/api/users', (req, res, ctx) => {
    // Mengecek parameter query, misal: /api/users?id=1
    const userId = req.url.searchParams.get('id');

    if (userId === '1') {
      return res(
        ctx.status(200),
        ctx.json({
          id: '1',
          name: 'Budi Santoso',
          email: 'budi.s@example.com',
        })
      );
    }

    // Default response jika tidak ada parameter id atau id berbeda
    return res(
      ctx.status(200),
      ctx.json([
        { id: '1', name: 'Budi Santoso', email: 'budi.s@example.com' },
        { id: '2', name: 'Siti Aminah', email: 'siti.a@example.com' },
        { id: '3', name: 'Agus Salim', email: 'agus.s@example.com' },
      ])
    );
  }),

  // Mock request POST ke /api/products
  rest.post('/api/products', async (req, res, ctx) => {
    const newProduct = await req.json(); // Mengambil body dari request
    console.log('Produk baru diterima:', newProduct);

    return res(
      ctx.status(201), // Status Created
      ctx.json({
        id: 'prod-456', // ID produk yang baru dibuat
        ...newProduct,
      })
    );
  }),

  // Mock request GET ke /api/items/:id (dengan dynamic path parameter)
  rest.get('/api/items/:itemId', (req, res, ctx) => {
    const { itemId } = req.params; // Mengambil path parameter

    if (itemId === 'special') {
      return res(
        ctx.status(200),
        ctx.json({
          id: itemId,
          name: 'Item Spesial',
          description: 'Ini adalah item langka!',
        })
      );
    }

    return res(
      ctx.status(404), // Not Found
      ctx.json({ message: `Item dengan ID ${itemId} tidak ditemukan.` })
    );
  }),

  // Mock error response untuk /api/auth/login
  rest.post('/api/auth/login', (req, res, ctx) => {
    // Anda bisa mensimulasikan kegagalan login
    return res(
      ctx.status(401), // Unauthorized
      ctx.json({ message: 'Username atau password salah.' })
    );
  }),
];

💡 Tips: Gunakan ctx (context) untuk mengontrol respons (status, headers, body, delay, dll.).

Mengaktifkan MSW di Aplikasi Anda

Buat file src/mocks/browser.js:

// src/mocks/browser.js
import { setupWorker } from 'msw';
import { handlers } from './handlers';

// Setup Service Worker
export const worker = setupWorker(...handlers);

Kemudian, panggil worker.start() di entry point aplikasi Anda (misal: src/index.js atau src/main.jsx), tetapi hanya di lingkungan pengembangan.

// src/main.jsx (Contoh untuk React)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';

async function enableMocking() {
  if (import.meta.env.DEV) { // Vite environment variable untuk dev mode
    const { worker } = await import('./mocks/browser');
    return worker.start();
  }
}

enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
});

Sekarang, saat Anda menjalankan aplikasi di mode pengembangan, semua permintaan ke /api/users, /api/products, dan /api/items/:itemId akan diintercept oleh MSW dan mendapatkan respons mock!

4. Menggunakan MSW untuk Testing (Contoh dengan Jest & React Testing Library)

MSW sangat bersinar di lingkungan pengujian. Ini memungkinkan Anda menulis tes yang lebih cepat, lebih stabil, dan benar-benar terisolasi.

Setup untuk Testing

Buat file src/mocks/server.js:

// src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

// Setup mock server untuk Node.js
export const server = setupServer(...handlers);

Kemudian, di file setup test Anda (misal: setupTests.js atau jest.setup.js):

// src/setupTests.js atau jest.setup.js
import '@testing-library/jest-dom';
import { server } from './mocks/server.js';

// Establishes API mocking before all tests.
beforeAll(() => server.listen());

// Resets any request handlers that are declared during a test,
// so they don't affect other tests.
afterEach(() => server.resetHandlers());

// Clean up after the tests are finished.
afterAll(() => server.close());

Contoh Test Case

Misalkan Anda memiliki komponen React UserList yang mengambil data pengguna dari /api/users.

// src/components/UserList.jsx
import React, { useEffect, useState } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        const response = await fetch('/api/users');
        if (!response.ok) {
          throw new Error('Failed to fetch users');
        }
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchUsers();
  }, []);

  if (loading) return <div>Loading users...</div>;
  if (error) return <div style={{ color: 'red' }}>Error: {error}</div>;

  return (
    <div>
      <h1>Daftar Pengguna</h1>
      {users.length === 0 ? (
        <p>Tidak ada pengguna.</p>
      ) : (
        <ul>
          {users.map((user) => (
            <li key={user.id}>
              {user.name} ({user.email})
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default UserList;

Sekarang, tes untuk UserList bisa terlihat seperti ini:

// src/components/UserList.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';
import { server } from '../mocks/server';
import { rest } from 'msw';

test('menampilkan daftar pengguna dari API', async () => {
  render(<UserList />);

  // Cek loading state
  expect(screen.getByText(/Loading users.../i)).toBeInTheDocument();

  // Tunggu hingga data dimuat dan tampil
  await waitFor(() => {
    expect(screen.getByText('Daftar Pengguna')).toBeInTheDocument();
  });

  expect(screen.getByText('Budi Santoso (budi.s@example.com)')).toBeInTheDocument();
  expect(screen.getByText('Siti Aminah (siti.a@example.com)')).toBeInTheDocument();
  expect(screen.getByText('Agus Salim (agus.s@example.com)')).toBeInTheDocument();
});

test('menampilkan pesan error jika fetching user gagal', async () => {
  // Override handler default untuk menguji skenario error
  server.use(
    rest.get('/api/users', (req, res, ctx) => {
      return res(ctx.status(500), ctx.json({ message: 'Internal Server Error' }));
    })
  );

  render(<UserList />);

  // Cek loading state
  expect(screen.getByText(/Loading users.../i)).toBeInTheDocument();

  // Tunggu hingga error ditampilkan
  await waitFor(() => {
    expect(screen.getByText(/Error: Failed to fetch users/i)).toBeInTheDocument();
  });
});

test('menampilkan pesan jika tidak ada pengguna', async () => {
  // Override handler untuk mengembalikan array kosong
  server.use(
    rest.get('/api/users', (req, res, ctx) => {
      return res(ctx.status(200), ctx.json([]));
    })
  );

  render(<UserList />);

  await waitFor(() => {
    expect(screen.getByText('Tidak ada pengguna.')).toBeInTheDocument();
  });
});

Dengan MSW, test Anda menjadi:

5. MSW di Lingkungan Node.js (untuk Backend Testing atau SSR)

Seperti yang disebutkan, MSW juga bisa digunakan di lingkungan Node.js. Ini sangat berguna untuk:

Penggunaannya mirip dengan setup testing di atas, menggunakan setupServer dari msw/node. Anda cukup mengimpor server yang sudah Anda definisikan di src/mocks/server.js dan menggunakannya di file tes Node.js Anda.

6. Tips & Best Practices untuk MSW

Untuk memaksimalkan penggunaan MSW, pertimbangkan tips berikut:

🎯 Tantangan: Coba simulasikan skenario di mana pengguna menambahkan item ke keranjang belanja, dan API GET /cart mengembalikan item tersebut setelah POST /cart/add berhasil. Ini akan menguji kemampuan Anda mengelola state mock di MSW!

Kesimpulan

Mock Service Worker (MSW) adalah alat yang sangat powerful dan praktis bagi setiap developer web. Dengan kemampuannya mengintercept permintaan jaringan di level yang lebih rendah, MSW memungkinkan pengembangan frontend yang lebih cepat, pengujian yang lebih stabil, dan demo aplikasi yang lebih andal. Ini adalah langkah besar menuju lingkungan pengembangan dan pengujian yang mandiri dan efisien.

Jangan biarkan ketergantungan backend memperlambat Anda. Mulai gunakan MSW hari ini dan rasakan perbedaan signifikan dalam alur kerja Anda! Selamat mencoba! ✨

🔗 Baca Juga