End-to-end tests drive the system as a user or client would: browser, or real HTTP against a composed environment. They catch wiring bugs that unit tests never see. They are slow, flaky if you let them be, and a terrible place to put business-rule coverage.
What belongs in E2E
- A handful of journeys that pay the bills: sign-in, checkout, “admin can refund.”
- Contract with the real reverse proxy, auth cookies, and CSRF if that is how production works.
What does not: every validation message, every branch of a pricing function. Those are unit/integration tests.
Browsers vs HTTP
Playwright, Cypress, Selenium — DOM, JS, real clicks. Use them when the risk is the UI. For APIs, an HTTP client against docker-compose/testcontainers is an E2E of the service without the flakiness of CSS selectors. Do not use a browser to assert JSON.
Stability
- Wait for conditions (network idle, test id visible), not
sleep(5). - Test IDs (
data-testid) beat CSS classes from the design system. - Isolate data: unique emails, or a reset endpoint in non-prod.
- Record traces on failure; otherwise you will re-run locally for an hour.
Example
Wait until the confirmation node exists, then read it. Playwright for .NET:
static async Task Confirm(IPage page)
{
var confirmed = page.GetByTestId("order-confirmed");
await confirmed.WaitForAsync();
var text = await confirmed.InnerTextAsync();
if (text.Length == 0)
throw new Exception("missing confirmation");
}
What breaks it
Five seconds and a design-system class. The class gets renamed, and the sleep is too short on a slow runner and pointless on a fast one. Use the test id above.
static async Task ConfirmBySleep(IPage page)
{
Thread.Sleep(5000);
var button = page.Locator(".btn-primary");
_ = await button.CountAsync();
}
Pitfalls
- 300 E2E tests, CI of 40 minutes, 8 flakes a day.
- Testing against a shared “dev” database other people mutate.
- Asserting pixel screenshots of the whole page as the only check.
See QA and BDD (Gherkin is optional; a Playwright test can still be a scenario).