Lesson 40 of 45 intermediate

End-to-End Testing Fundamentals

Drive the real app in a real browser, but only for the few journeys that must never break

Guess first

Pick an answer. You will see if you were right at the end of the lesson.

Why are E2E tests the small top of the test pyramid?

Open interactive version (quiz + challenge)

Real-world analogy

Before opening, a shop owner walks through as a customer, from the door to the till. The walk finds a sticking door or an offline till, but it takes ten minutes. The walk is the E2E test and the customer is the browser. The door, shelf and till are the frontend, API and database.

What is it?

End-to-end testing drives the finished application the way a user does. A real browser opens the real frontend, which calls the real API and database. Playwright is a popular tool for this. You write steps like "fill in the email, click Sign in, expect the dashboard heading". Playwright runs them in Chromium, Firefox or WebKit. E2E tests give the most realistic check, but they are the slowest and most fragile. So teams keep them few and aim them at the journeys that matter most.

Real-world relevance

An online shop runs five E2E tests on every merge. They cover sign up, log in, search, cart, and paying with a test card. One day a CSS refactor hides the Pay button behind another element, and no unit test notices. The checkout test fails within minutes and the trace shows the button covered. The usual trap is the opposite: 300 E2E tests that take an hour, fail randomly, and are ignored.

Key points

Quick check

Answer from memory — scrolling up first makes it stick less.

Which locator is the most stable for a Sign in button?

A test passes on your machine and fails randomly in CI. Which cause is most likely?

Code example

import { test, expect } from '@playwright/test';

test('a user can log in and see the dashboard', async ({ page }) => {
  await page.goto('/login');

  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill(process.env.E2E_PASSWORD ?? '');
  await page.getByRole('button', { name: 'Sign in' }).click();

  // The assertions retry until the page is ready or the timeout passes
  await expect(page).toHaveURL(/dashboard/);
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

Line-by-line walkthrough

  1. 1. This is a fragment of a test file. The import brings in test and expect from Playwright's test runner. The base URL comes from the Playwright config.
  2. 2. The test receives a fresh browser page and goes to the login screen.
  3. 3. getByLabel finds the Email and Password fields by their visible labels, like a user would, and fills them. The password comes from an environment variable.
  4. 4. getByRole finds the button by its role and name, then clicks it. Playwright waits until the button is ready.
  5. 5. The first assertion expects the URL to match dashboard. It retries until it matches or the timeout passes.
  6. 6. The last assertion finds the heading by role and name, and expects it to be visible. No sleep is needed.

Spot the bug

test('shows the dashboard after login', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill(process.env.E2E_PASSWORD ?? '');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForTimeout(3000);
  expect(await page.getByRole('heading', { name: 'Dashboard' }).isVisible()).toBe(true);
});
Need a hint?
What does the test do if the page needs 3.5 seconds, or only 0.2?
Show answer
The test sleeps for three seconds and then checks once, because isVisible() answers at once and does not retry. On a slow CI machine the heading is not there yet, so the test fails at random. On a fast one the sleep wastes time. Fix: delete the sleep and use await expect(locator).toBeVisible(), which retries until the heading appears or the timeout passes.

Explain like I'm 5

To check a new playground works, you do not look at each bolt. You let a child play on it and watch. That takes a long time, so you only do it for the most important games.

Fun fact

Playwright was created by a team that had earlier built Puppeteer. Auto-waiting is one of its core features, and it exists to cut down on flaky tests caused by fixed sleeps.

Hands-on challenge

Say it out loud in 30 seconds: "What should you cover with end-to-end tests, and how do you keep them from being flaky?" Then check: did you mention the test pyramid, critical journeys like login and checkout, locating by role and label, waiting for conditions instead of sleeping, and traces in CI?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook