Learning Goals
3 min- Define E2E testing and how it differs from unit/integration.
- Know the tools: Playwright (modern), Selenium (classic).
- Understand why E2E tests are slow, flaky, and should be few.
- Pick which user journeys deserve an E2E test.
Warm-Up · A Robot User
5 minunit: test one function (microseconds)
integration: test components together (milliseconds)
E2E: drive a real BROWSER through
the running app as a USER (SECONDS, sometimes flaky)E2E tests verify the whole system the way a user experiences it — browser, server, database, all live. They catch bugs nothing else can (a broken button, a missing redirect in the real UI). But they're slow and brittle, so the pyramid says: write only a handful, for your most important journeys.
New Concept · E2E with Playwright
14 minInstall
pip install pytest-playwright playwright install # downloads the browser binaries
A first E2E test
# test_e2e.py — needs the app RUNNING at localhost:5000 def test_homepage_loads(page): # 'page' fixture from pytest-playwright page.goto("http://localhost:5000/") assert page.title() == "My Blog" assert page.get_by_text("Welcome").is_visible()
Click and fill — act like a user
def test_create_post(page): page.goto("http://localhost:5000/new") page.get_by_label("Title").fill("My First Post") page.get_by_label("Body").fill("Hello, world!") page.get_by_role("button", name="Publish").click() # after redirect, the post should appear assert page.get_by_text("My First Post").is_visible()
Selectors: prefer user-facing ones
get_by_role("button", name="...") ← best: how users find things
get_by_label("Email") ← good for form fields
get_by_text("Welcome") ← good for content
page.locator("#submit") ← CSS, brittle; use sparinglyWhy E2E is flaky — and how to fight it
flaky causes: timing (page not loaded yet), animations, network,
test data left over from a previous run
fixes: auto-waiting selectors (Playwright waits by default),
reset the DB before each run, avoid sleep() — use waitsPlaywright auto-waits for elements, which kills most timing flakiness. Still: keep E2E tests few, independent, and run them against a fresh test database.
Selenium — the classic alternative
Selenium does the same job (drive a browser) and is older/widely used, but is more verbose and needs explicit waits. Playwright is the modern default; the concepts transfer.
Worked Example · One Golden-Path E2E Test
12 min# test_blog_e2e.py — the ONE journey that matters most import pytest BASE = "http://localhost:5000" def test_user_can_publish_and_read_a_post(page): # 1. land on the empty blog page.goto(BASE) assert page.get_by_text("No posts yet").is_visible() # 2. go to the new-post page page.get_by_role("link", name="New post").click() # 3. fill and publish page.get_by_label("Title").fill("Hello E2E") page.get_by_label("Body").fill("This was created by a robot.") page.get_by_role("button", name="Publish").click() # 4. the post is now visible (the whole stack worked) assert page.get_by_role("heading", name="Hello E2E").is_visible() # 5. back on the home page it's listed page.goto(BASE) assert page.get_by_text("Hello E2E").is_visible()
$ pytest test_blog_e2e.py --headed # --headed shows the real browser! test_user_can_publish_and_read_a_post PASSED 1 passed in 3.4s ← seconds, not milliseconds — that's why we write FEW
Read the diff
This single test exercises everything — routing, the form, validation, the redirect, the database write, the template rendering — as a real user would. It's the most realistic test possible and the slowest (3.4s vs milliseconds). So you write ONE for the critical "publish a post" journey, and rely on faster unit/integration tests for the details. That's the pyramid in action.
Try It Yourself
13 minRun a Flask app, then write an E2E test that opens the home page and asserts a piece of visible text. Run with --headed to watch the browser.
E2E test a form submission: fill fields by label, click the button, assert the result page shows the right content.
List your app's user journeys and rank them. Choose the top 2-3 that deserve an E2E test (the ones where a break would be a disaster). Justify why the rest don't.
Mini-Challenge · Robust Selectors
8 minTake an E2E test written with brittle CSS/id selectors and rewrite it using user-facing selectors (get_by_role, get_by_label, get_by_text). Explain why these survive a redesign that breaks CSS selectors.
Recap
3 minE2E tests drive a real browser through the running app as a user — maximally realistic, but slow and flaky. Use Playwright (auto-waits, user-facing selectors). Write FEW: only your most critical journeys, against a fresh test DB. Rely on the faster unit/integration layers for everything else. Next: a full E2E project with playwright + pytest.
Vocabulary Card
- E2E test
- Drives the whole app through a real browser, as a user would.
- Playwright
- A modern browser-automation tool with auto-waiting selectors.
- user-facing selector
- Finding elements by role/label/text — robust to CSS changes.
- flakiness
- Intermittent E2E failures from timing/data; minimised, never zero.
Homework
4 minInstall pytest-playwright. For a Flask app of yours, write ONE golden-path E2E test of the most important journey, using user-facing selectors. Run it --headed to watch it. Write a paragraph on which journeys you'd cover with E2E and which you'd leave to faster tests.
Model on test_blog_e2e.py. The journey-selection paragraph is key: E2E for "sign up & post", unit/integration for the validation rules behind them.