Testing Things That Touch the Network
Mocking too much gives you false confidence. Mocking too little gives you a flaky suite. Here's where we landed.
Every test suite that talks to a network eventually faces the same fork: fake the network, or hit something real. We've tried both extremes and settled somewhere in between.
Why full mocking burned us
We used to mock every fetch call with hand-written fixtures. Tests were fast and deterministic, but they drifted from reality — a backend field rename shipped without a single test failing, because the mock still had the old shape.
test("renders the author name", async () => {
server.use(
http.get("/api/posts/:id", () => HttpResponse.json(mockPost))
);
render(<PostPage id="123" />);
expect(await screen.findByText(mockPost.author.name)).toBeVisible();
});Contract tests closed the gap
Instead of hand-maintained fixtures, we generate mock responses from the same schema the backend validates against. If the backend changes the shape, the schema changes, and the mock changes with it — no silent drift.
- Unit and component tests still mock the network, but from schema-derived fixtures.
- A small number of end-to-end tests run against a real staging API, specifically to catch what the schema can't: auth, latency, and ordering.
- Flaky end-to-end tests get quarantined immediately, not left red for a week.
The goal was never zero mocking. It was making sure the mocks could not quietly stop telling the truth.
More posts
How Postgres Indexes Actually Work
A deep dive into B-tree internals, and why a sequential scan sometimes beats an index you were sure would help.
Why We Moved Off REST
GraphQL solved a real problem for our mobile clients, but it introduced a few we didn't see coming.