E2E and the Django test client

The Django test Client calls the view directly: there is no browser, no JavaScript and no real HTTP (Django testing basics, pytest-django setup). That is fast and enough for most checks. An E2E test uses a real browser, so it also proves that the route, template, CSRF token, session, static files and JavaScript work together.

Test clientE2E with Playwright
Runsthe view in-processChromium + Django over HTTP
JavaScript, HTMXnoyes
Speedmillisecondsseconds
Use forlogic, permissions, redirectscritical paths: login, order, payment

💡 One E2E test per critical scenario; leave the details to the test client. Mark browser tests with @pytest.mark.e2e so the daily run can skip them.

Setup

pytest-django provides the live_server fixture and pytest-playwright provides page, context and browser:

text
pip install pytest-django pytest-playwright
playwright install chromium

# pyproject.toml
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings.test"
markers = ["e2e: slow browser tests"]

# run only the browser tests, with a visible browser
pytest -m e2e --headed

playwright install chromium downloads the browser once. The default mode is headless; --headed shows the window, which helps when you write a test for the first time.

live_server and the first test

live_server starts Django in a separate thread on a free port and gives you live_server.url. The test thread drives Playwright, Playwright talks to Django over HTTP, and both use the same test database.

live_server: one process, two threads
live_server: one process, two threads
python
import re

import pytest
from playwright.sync_api import Page, expect

pytestmark = pytest.mark.e2e

def test_login_shows_dashboard(live_server, page: Page, user):
    page.goto(f"{live_server.url}/accounts/login/")
    page.get_by_label("Username").fill("ann")
    page.get_by_label("Password").fill("secret123")
    page.get_by_role("button", name="Sign in").click()

    expect(page).to_have_url(re.compile(r"/dashboard/$"))
    expect(page.get_by_role("heading", name="Dashboard")).to_be_visible()

Three habits make the test stable: