Negative Paths in Automation — Empty, Invalid, Timeout, and 5xx

Automate empty states, validation errors, timeouts, and server failures without cloning the happy path.

This post is part of the Automation Strategy series. If you missed the previous post, read Part 7 — Contract Tests vs E2E first.


Introduction — The Suite Knows How to Succeed

Most automated tests walk the path that works. The form is valid. The API returns 200. The list has rows. The user who only meets the product on a bad day is absent.

Negative paths are not a second copy of the suite. They are the few failures that change what the user can do, placed in the cheapest layer that can see them.

Four Failures Worth a Test

FailureWhat the user seesUsual layer
EmptyA list, search, or cart with nothing in itComponent or one UI test
InvalidA field rejected before the request is sentUnit or component
TimeoutA retry, not a spinner that never endsUI with the call aborted
5xxA message and a way back, not a blank pageUI or API test

If two rows would assert the same message, keep one. “Email is required” and “email must contain @” are different. “Server is unavailable” for three endpoints that share one error component is one test.

Do Not Clone the Happy Path

A negative test that signs in, fills six valid fields, and then breaks the seventh is an expensive way to reach one assertion. Build the state you need and stop.

test('empty search tells the user nothing matched', async ({ page }) => {
  await page.route('**/api/search**', (route) =>
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ results: [] }),
    }),
  );
  await page.goto('/search?q=zzzz');
  await expect(page.getByRole('status')).toHaveText('No matches');
});

The route is the setup. The status text is the test.

Validation Belongs Close to the Field

An end-to-end test of every validation message will take minutes and break when copy changes. Test the rule next to the code that implements it. Keep one UI test that proves the message is actually wired to the field the user is looking at.

Server-side rejection is a different case. The client said the value was fine, the server said no. That test submits once and asserts the error from the response, not the client-side hint.

Timeouts and 5xx

Abort or fulfill with 500. Assert three things only:

  • the user is not left on an endless spinner
  • the user can tell the action did not succeed
  • a second click does not create a second order, payment, or invite

The third point is the one that costs money. If you automate nothing else from this post, automate that.

Conclusion

Pick the failures that change the outcome. Put each one in the lowest layer that can see it. Assert the outcome, not the setup.

Action for this week: List the last five production bugs that were not a happy path. Add one automated check for the one that can happen again by a double submit or a 500 from a dependency.


This concludes the Automation Strategy series.