Skip to content
React rc testing 5 min read

Mocking & Network

Real components rarely live in isolation—they import modules, schedule timers, and fetch data over the network. To test them reliably you need to control those dependencies so a test never depends on a real clock, a real server, or a flaky third party. This page covers three layers of mocking with Vitest: replacing modules with vi.mock, taking control of time with fake timers, and intercepting HTTP with Mock Service Worker (MSW), which is the modern, framework-agnostic way to test data fetching.

When to mock (and when not to)

Mock the boundaries of your unit, not its internals. A component’s own state, derived values, and child components are part of the behavior you want to verify, so leave them alone. The network, the system clock, browser APIs like localStorage, and slow or non-deterministic modules are the things worth replacing. Over-mocking produces tests that pass while the real app breaks; under-mocking produces tests that are slow and flaky. Aim for the seam where your code meets the outside world.

Mocking modules with vi.mock

vi.mock replaces an entire module with a stub before any test runs. Vitest hoists these calls to the top of the file, so the factory cannot reference outside variables unless you opt in. The most common use is faking a module that talks to a service, returning a controlled value instead.

// analytics.js
export function track(event, payload) {
  navigator.sendBeacon("/collect", JSON.stringify({ event, payload }));
}
// CheckoutButton.jsx
import { track } from "./analytics";

export function CheckoutButton({ total }) {
  return (
    <button onClick={() => track("checkout", { total })}>
      Pay ${total}
    </button>
  );
}
// CheckoutButton.test.jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test, vi } from "vitest";
import { track } from "./analytics";
import { CheckoutButton } from "./CheckoutButton";

vi.mock("./analytics", () => ({
  track: vi.fn(),
}));

test("reports a checkout event with the total", async () => {
  const user = userEvent.setup();
  render(<CheckoutButton total={49} />);

  await user.click(screen.getByRole("button", { name: /pay \$49/i }));

  expect(track).toHaveBeenCalledWith("checkout", { total: 49 });
});

To replace only part of a module, use vi.importActual inside the factory and spread the real exports, overriding just what you need.

vi.mock("./config", async (importOriginal) => {
  const actual = await importOriginal();
  return { ...actual, API_URL: "http://localhost/api" };
});

Tip: Call vi.clearAllMocks() in an afterEach (or set clearMocks: true in your Vitest config) so call history never leaks between tests.

Controlling time with fake timers

Code that uses setTimeout, setInterval, or debouncing should not make tests wait in real seconds. vi.useFakeTimers() swaps the timer functions for ones you advance manually with vi.advanceTimersByTime. Because userEvent schedules its own timers, pass it the advance hook so it cooperates with fake timers.

// Toast.jsx
import { useEffect } from "react";

export function Toast({ message, onDismiss }) {
  useEffect(() => {
    const id = setTimeout(onDismiss, 3000);
    return () => clearTimeout(id);
  }, [onDismiss]);

  return <div role="alert">{message}</div>;
}
// Toast.test.jsx
import { render, screen, act } from "@testing-library/react";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { Toast } from "./Toast";

beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());

test("dismisses itself after three seconds", () => {
  const onDismiss = vi.fn();
  render(<Toast message="Saved" onDismiss={onDismiss} />);

  expect(onDismiss).not.toHaveBeenCalled();
  act(() => vi.advanceTimersByTime(3000));
  expect(onDismiss).toHaveBeenCalledOnce();
});

Wrapping advanceTimersByTime in act flushes the resulting state update so React warnings stay quiet. Always restore real timers afterward, or unrelated async tests will hang.

Intercepting the network with MSW

For data fetching, the best approach is not to mock fetch at all but to intercept requests at the network layer with Mock Service Worker. MSW lets you write request handlers once and reuse them in tests, Storybook, and even local development. Your component runs its real fetch (or Axios) code; MSW answers.

Install it and define handlers:

npm install --save-dev msw
// test/server.js
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/api/users/:id", ({ params }) => {
    return HttpResponse.json({ id: params.id, name: "Ada Lovelace" });
  }),
];

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

Start the server once for the whole suite and reset handlers between tests so per-test overrides do not leak:

// test/setup.js
import { afterAll, afterEach, beforeAll } from "vitest";
import { server } from "./server";

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

Now a component that fetches data can be tested without any knowledge that the network is fake:

// UserCard.jsx
import { useEffect, useState } from "react";

export function UserCard({ id }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${id}`)
      .then((r) => (r.ok ? r.json() : Promise.reject(r)))
      .then(setUser)
      .catch(() => setError("Could not load user"));
  }, [id]);

  if (error) return <p role="alert">{error}</p>;
  if (!user) return <p>Loading…</p>;
  return <h2>{user.name}</h2>;
}
// UserCard.test.jsx
import { render, screen } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { expect, test } from "vitest";
import { server } from "./test/server";
import { UserCard } from "./UserCard";

test("renders the fetched user name", async () => {
  render(<UserCard id="42" />);
  expect(await screen.findByRole("heading")).toHaveTextContent("Ada Lovelace");
});

test("shows an error when the request fails", async () => {
  server.use(
    http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })),
  );

  render(<UserCard id="42" />);
  expect(await screen.findByRole("alert")).toHaveTextContent("Could not load user");
});

Output:

 ✓ src/UserCard.test.jsx (2 tests) 63ms
   ✓ renders the fetched user name
   ✓ shows an error when the request fails

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Duration  512ms

The happy path uses the default handler; the error case overrides it for a single test with server.use, which resetHandlers cleans up automatically.

Choosing an approach

TechniqueBest forWatch out for
vi.mockModules, third-party SDKs, analyticsHoisting—no outside variables in the factory
vi.spyOnAsserting a real method was called while keeping its behaviorRestore with mockRestore
Fake timersDebounce, polling, timeoutsRestore real timers; wrap advances in act
MSWAny HTTP request, REST or GraphQLSet onUnhandledRequest: "error" to catch typos

Warning: Avoid manually stubbing global.fetch for data-fetching tests. It couples the test to the exact call shape and breaks the moment you switch HTTP clients. MSW survives that refactor because it intercepts the actual request.

Best Practices

  • Mock at the boundary—network, time, storage—never your component’s own state or children.
  • Prefer MSW over hand-rolled fetch stubs so tests survive client and refactor changes.
  • Define default handlers globally and override per test with server.use, resetting after each test.
  • Set onUnhandledRequest: "error" so an unexpected request fails loudly instead of hanging.
  • Always restore fake timers in afterEach, and wrap timer advances in act to flush React updates.
  • Clear or reset mocks between tests (clearMocks: true) so call history never leaks.
  • Keep vi.mock factories self-contained; reach for importOriginal when you only need to override part of a module.
Last updated June 14, 2026
Was this helpful?