An endpoint behind a product list page returns HTTP 200 with an empty array. Why is that worth a separate test from both the populated response and the error response, and what should that test assert?
answer
- success is not always data
- not an error, not a list
- the third rendered state
- stray zero from a length guard
- empty fixture, same envelope
basics
~20 sAn empty successful result is a third state with its own UI: not data, not an error. Stub the endpoint to return an empty collection and assert the empty-state message renders, while the error banner, the spinner and any row are all absent.
solid answer
~50 sSuccess and failure are not the only outcomes — "the request worked and there is nothing to show" is a distinct state with distinct UI, and it is the one teams forget. The bugs it catches are specific: a page that renders an error banner because the code treats a falsy result as a failure; a spinner that never clears because the loading flag is only cleared when rows exist; a bare white panel because nobody wrote the empty-state copy; and the classic render of a stray `0` from a length-guarded expression. So stub the endpoint with a 200 and an empty collection, then assert positively that the empty-state message and its call to action are on screen, and negatively that no row, no error alert and no progress indicator remain. The same reasoning extends to the other shapes a successful response can legitimately take — a null optional field, a page beyond the last one, a filtered search with no matches.
go deeper
Be ready to name three outcomes rather than two — data, nothing, and failure — and to write a stub returning an empty collection with a 200 so the empty-state copy can be asserted.
Explain the specific defects this test catches: empty misread as an error, a spinner cleared only in the rows branch, and a falsy length guard rendering a literal zero.
Insist the empty fixture keeps the real response envelope and field types, and argue for one test per designed rendered state rather than per payload permutation, so the suite grows with the UI and not with the API surface.
Own the product-level decision this test encodes — what empty, not-found and forbidden each mean to the user — and make that mapping explicit so every screen answers it the same way.
## Three outcomes, not two Most data-fetching UI is written and reviewed against two mental cases: it worked, or it broke. Real APIs produce a third routinely — a successful response carrying nothing. A new account has no projects; a search matches nothing; a filter excludes everything; the user paged past the end. Because this state is invisible in development against a seeded database, it is frequently the first thing a real new user sees, which makes it a disproportionately expensive gap. ## What actually goes wrong The defects clustered here are mechanical and easy to reproduce once you stub for them: - **Empty treated as failure.** Client code written as `if (!data.length) return <Error />` or a truthiness check on the payload turns "nothing to show" into "something went wrong", which is actively misleading — the user retries a request that will never produce data. - **A spinner that never clears.** When the loading flag is turned off inside the branch that renders rows, an empty result leaves the page spinning forever. - **No empty UI at all.** The list simply renders zero children, leaving a blank region with no explanation and no next action. - **A stray value on screen.** In JSX, `{items.length && <List items={items} />}` renders the number `0` when the array is empty, because `0` is a falsy value that React still renders as text. The guard has to be a boolean, for example `items.length > 0 && …`. - **A crash on optional fields.** An empty result often comes with `null` where the populated response had an object; code that reaches into it throws. ## The test Stub the endpoint with a genuine success — the same status and the same envelope your API really uses — carrying an empty collection, and assert the whole visible state: ```js server.use(http.get('/api/products', () => HttpResponse.json({ items: [] }))) render(<ProductList />) expect(await screen.findByText(/no products yet/i)).toBeInTheDocument() expect(screen.queryByRole('listitem')).not.toBeInTheDocument() expect(screen.queryByRole('alert')).not.toBeInTheDocument() expect(screen.queryByRole('progressbar')).not.toBeInTheDocument() ``` The negative assertions carry most of the value here. The positive one alone would pass on a page that shows the empty-state copy *and* an error banner, which is precisely the bug the first defect above produces. If the empty state offers an action — "Create your first project", "Clear filters" — assert the control is present and, where it is cheap, that using it moves the page on. An empty state without a way forward is a design defect, and a test that names the call to action documents the intent. ## Keep the empty payload contract-accurate The stub must be empty in the way your API is actually empty. If the real response is `{ items: [], total: 0, nextCursor: null }`, stubbing a bare `[]` tests a shape that never occurs and skips the `nextCursor` handling that will throw in production. The envelope, the status and the field types should match the populated fixture exactly, with only the collection emptied — that is what makes the test evidence about real behaviour rather than about your stub. ## Where it stops This is not an argument for stubbing every conceivable payload permutation. The empty case earns its own test because it corresponds to a *distinct rendered state* that the product deliberately designed. Variations that do not change what the user sees — a different id, one row versus three — do not; they belong in the populated fixture. The test that pays for itself is the one that pins a branch the UI actually has.
- Give a concrete rendering bug that only the empty-result test catches.The length-guard render. `{items.length && <List items={items} />}` evaluates to `0` for an empty array, and React renders `0` as visible text, so the user sees a lone zero where the empty state should be. Populated and error tests both pass; only a stub returning an empty collection puts that character on screen for an assertion to catch.
- Should a 404 from the same endpoint render the same empty state?It depends on what the product means by it, and the test should encode that decision. "This collection is empty" and "this resource does not exist" are different messages for the user, and collapsing them hides genuine routing bugs behind a friendly empty state. Whichever way the team decides, stub both responses and assert they render what was intended.
- Does an empty search result deserve its own test on top of the empty-collection one?Usually yes, because the copy and the call to action differ — "no results for that query, clear your filters" versus "you have not created anything yet". They are two distinct designed states, and the rule of thumb is one test per rendered state the product actually designed, not one per payload permutation.
saying these in an interview costs you the question
- Treats an empty array as an error condition.
- Says the populated-response test already covers the empty case.
- Stubs a bare empty array instead of the real response envelope.
- Asserts the empty message without ruling out a stale spinner.
- Guards rendering with items.length and ships a visible zero.