← Back to question bank
UI ComponentMidMedium#6002 · 35m

Practice: mock server states with MSW

Turn "mock server states with MSW" into a concrete interview exercise. Explain the risk, choose the smallest useful test boundary, and describe how the signal prevents regressions.

Answer Strategy

MSW is the right answer to "how do you mock the network in frontend tests". The interview signal is naming the alternative tradeoff: jest.mock('fetch') is faster but couples tests to the call site; spinning up a real local server is more realistic but slow and flaky. MSW intercepts at the network layer so the component code under test runs unchanged, and the same handlers can serve a Storybook or a Playwright preview without rewriting.

Structure the suite around scenarios, not endpoints. Default handlers describe the happy path; each test that needs a different reality calls server.use(...) to override one route. Common per-test scenarios: 404/empty state, 5xx/error toast, slow network/skeleton timing, abort on unmount, and a malformed payload that exercises the schema validator. With MSW you can write all five tests against the same component with one render() call each.

Volunteer the failures. Without server.resetHandlers() between tests, an override leaks across cases and produces ghost failures. Without onUnhandledRequest: 'error', a missing handler silently 404s and the test passes for the wrong reason. Without per-test overrides, tests start mocking inside the component (fetchMock, MSW handlers in describe blocks) and lose the ability to read top-down. Mocking timers alongside MSW is fine but you need vi.useFakeTimers({ toFake: [...] }) so the network IO is left real — otherwise the response never resolves.

Reference Implementation: MSW Server With Per-Test Scenario Overrides

A setup file with default happy-path handlers, plus a test suite that overrides per case for empty, slow, and aborted scenarios.

// Setup file referenced from vitest config setupFiles. The handlers list is
// the contract that ties tests to "what the server might return".
import { setupServer } from 'msw/node';
import { http, HttpResponse, delay } from 'msw';

type Profile = { id: string; name: string };

// Default handlers describe the happy path. Tests override per-case using
// server.use(...) so they can simulate slow, errored, or empty responses
// without touching the global handler list.
export const handlers = [
  http.get('/api/profile/:id', async ({ params }) => {
    return HttpResponse.json<Profile>({ id: String(params.id), name: 'Ada' });
  }),
  http.post('/api/invite', async () => {
    await delay(50); // network-ish baseline; tests can override.
    return HttpResponse.json({ ok: true });
  }),
];

export const server = setupServer(...handlers);

Executable UI Sandbox

UI interview practice should behave like component documentation, not a static snippet. This uses the same isolation pattern as Storybook, Sandpack, CodeSandbox, and StackBlitz: editable source on one side, a sandboxed browser preview on the other. Edit the DOM code, run it, and verify focus, keyboard, pointer, and state behavior in the preview.

Browser sandbox
HTML, CSS, DOM events, focus, and keyboard behavior
Preview running...
Loading editor...

Testing Strategy

Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.

Lifecycle
beforeAll(server.listen), afterEach(server.resetHandlers), afterAll(server.close). Set onUnhandledRequest to error so a missing handler is a hard test failure.
Scenarios
One test per network reality: 200, 404, 500, slow (delay), aborted (request.signal). Assert the user-facing UI for each, not just the call shape.
Schema
Pair MSW with a runtime schema (Zod, valibot) at the API boundary. A test that returns a malformed payload must surface a typed error, not a confused render.
import { afterAll, afterEach, beforeAll, describe, it, expect } from 'vitest';
import { http, HttpResponse, delay } from 'msw';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('ProfileScreen with MSW', () => {
  it('renders the loaded profile (happy path uses default handler)', async () => {
    render(<ProfileScreen profileId="42" />);
    expect(await screen.findByText('Ada')).toBeInTheDocument();
  });

  it('shows the empty state when the server returns 404', async () => {
    server.use(
      http.get('/api/profile/:id', () =>
        HttpResponse.json({ message: 'Not found' }, { status: 404 })
      )
    );
    render(<ProfileScreen profileId="42" />);
    expect(await screen.findByRole('status')).toHaveTextContent(/profile not found/i);
  });

  it('shows the slow-network skeleton until the response settles', async () => {
    server.use(
      http.get('/api/profile/:id', async () => {
        await delay(200);
        return HttpResponse.json({ id: '42', name: 'Grace' });
      })
    );
    render(<ProfileScreen profileId="42" />);
    expect(screen.getByRole('status')).toHaveTextContent(/loading/i);
    expect(await screen.findByText('Grace')).toBeInTheDocument();
  });

  it('aborts the request when the component unmounts mid-flight', async () => {
    const aborted: string[] = [];
    server.use(
      http.get('/api/profile/:id', async ({ request }) => {
        return await new Promise<HttpResponse>(() => {
          request.signal.addEventListener('abort', () => aborted.push(request.url));
        });
      })
    );
    const { unmount } = render(<ProfileScreen profileId="42" />);
    unmount();
    await new Promise((resolve) => setTimeout(resolve, 0));
    expect(aborted).toHaveLength(1);
  });
});

Interviewer Signal

Shows whether you can prove frontend behavior instead of relying on screenshots or manual confidence.

Constraints

  • Choose unit, integration, E2E, visual, or performance testing deliberately.
  • State the failure that the test catches.
  • Avoid brittle assertions that lock implementation details.

Model Answer Shape

  • Start with the user-impacting behavior.
  • Pick the smallest test that sees that behavior.
  • Add one higher-level test only when timing, browser behavior, or integration risk requires it.

Tradeoffs

  • Unit tests are fast and precise but cannot prove browser wiring.
  • E2E tests are realistic but should be reserved for workflows where integration risk matters.

Edge Cases

  • Out-of-order async results.
  • Environment-specific browser behavior.
  • False confidence from mocks that do not match production contracts.

Testing And Proof

  • Regression case for the named risk.
  • Negative path or error state.
  • Cleanup or retry behavior when relevant.

Follow-Ups

  • What would make this test flaky?
  • What would you monitor after shipping the fix?