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
| Failure | What the user sees | Usual layer |
|---|---|---|
| Empty | A list, search, or cart with nothing in it | Component or one UI test |
| Invalid | A field rejected before the request is sent | Unit or component |
| Timeout | A retry, not a spinner that never ends | UI with the call aborted |
| 5xx | A message and a way back, not a blank page | UI 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.