Lesson 28 of 30
The one test almost everyone writes for a data-fetching component is the happy path with data already loaded — the three tests that actually catch real bugs are the ones for loading, error, and empty-success.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
Why is intercepting requests at the network boundary generally considered more resilient than mocking your own fetch wrapper function directly?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Testing a component that fetches data has a few concerns specific to it that plain component tests don't — mostly because the thing being tested is inherently asynchronous and has more than one real state to render correctly.
Two habits separate a data-fetching test suite that actually catches bugs from one that just looks thorough:
// Intercepting at the network boundary, not mocking fetchPosts() directly
describe("PostList", () => {
it("shows a loading state while the request is pending", () => {
mockNetwork.get("/posts").respondWith({ delayMs: Infinity }); // never resolves
render(<PostList />);
expect(screen.getByRole("status")).toHaveTextContent(/loading/i);
});
it("renders posts once the request resolves", async () => {
mockNetwork.get("/posts").respondWith({ status: 200, body: [{ id: "1", title: "Hello" }] });
render(<PostList />);
await screen.findByText("Hello"); // waits for the async work to actually settle
});
it("renders an error state when the request fails", async () => {
mockNetwork.get("/posts").respondWith({ status: 500 });
render(<PostList />);
await screen.findByText(/something went wrong/i);
});
it("renders a no-results state when the request succeeds with zero items", async () => {
mockNetwork.get("/posts").respondWith({ status: 200, body: [] });
render(<PostList />);
await screen.findByText(/no posts yet/i);
});
});
Four tests, four distinct states — and the empty-success case is the one most suites skip entirely, because it's easy to assume "success with an empty array" just falls out of the happy-path test for free. It doesn't; it's genuinely a different render branch, and a component with a bug there (rendering the loading spinner or the error message for a legitimately empty result) will pass every other test in this suite while being wrong in production.
find*-style queries) is not optional polish — it's what makes an async test correct instead of flaky. Reaching for an immediate, non-waiting assertion after triggering an async fetch is the single most common cause of intermittent test failures in this category."How would you test a component that fetches data?" is a real, common question, and naming network-boundary mocking plus all four fetch-lifecycle states — not just "I'd mock the API and check it renders" — is what shows you've actually caught a flaky or incomplete test in a real codebase before.