Last modified: Oct 24, 2026

E2E Testing with Playwright and pytest

End-to-end testing checks your app the way a real user does. It opens a browser, clicks buttons, and fills forms. If something breaks, you see it before your users do.

Playwright drives real browsers fast. pytest gives you a clean test runner with fixtures and clear reports. Together, they make browser testing simple and reliable.

Why Combine Playwright with pytest?

Playwright has its own test runner. So why use pytest? Because pytest fits nicely into Python projects that already use it.

You get one test command. You get shared fixtures. You get rich plugins for coverage, parallelism, and reporting. That matters in real teams.

Playwright also auto-waits for elements. That means fewer random failures. Your tests wait for the page to be ready instead of using fixed sleeps.

Installing Playwright and pytest

Start by creating a virtual environment. Then install both packages.


python -m venv venv
source venv/bin/activate
pip install pytest playwright

Next, download the browser binaries. Playwright needs these to run tests.


playwright install

You can also install only Chromium to save space. Use playwright install chromium for that.

Writing Your First End-to-End Test

A basic test opens a page and checks the title. Here is a simple example.


from playwright.sync_api import sync_playwright

def test_homepage_title():
    with sync_playwright() as p:
        browser = p.chromium.launch()      # Start a Chromium browser
        page = browser.new_page()          # Open a new tab
        page.goto("https://example.com")   # Load the page
        assert "Example" in page.title()   # Check the title
        browser.close()                    # Clean up

This works, but it repeats setup in every test. Fixtures fix that problem.

Using pytest Fixtures with Playwright

Fixtures run setup code once and share it across tests. Put them in a conftest.py file. pytest finds them automatically.


import pytest
from playwright.sync_api import sync_playwright

@pytest.fixture(scope="session")
def browser():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)  # Launch once per session
        yield browser
        browser.close()

@pytest.fixture
def page(browser):
    page = browser.new_page()   # Fresh page for each test
    yield page
    page.close()

Now each test just asks for page. The setup code stays in one place.


def test_login_button_visible(page):
    page.goto("https://example.com/login")
    assert page.locator("#login-btn").is_visible()  # Button is on screen

Run the tests with a single command.


pytest -v

The output shows each test and its result.


tests/test_login.py::test_login_button_visible PASSED

Locators and Auto-Waiting

Locators find elements on the page. Playwright waits for them to appear. That removes most timing bugs.


page.get_by_role("button", name="Submit").click()   # Click by accessible role
page.get_by_label("Email").fill("[email protected]")    # Fill by label text
page.get_by_text("Welcome back").wait_for()         # Wait for text

Prefer role and label locators. They match how users and screen readers see the page. They also survive small markup changes.

Testing a Full User Flow

Real tests follow a user journey. This example logs in and checks the dashboard.


def test_user_can_log_in(page):
    page.goto("https://example.com/login")
    page.get_by_label("Email").fill("[email protected]")      # Enter email
    page.get_by_label("Password").fill("secret123")       # Enter password
    page.get_by_role("button", name="Sign in").click()    # Submit form
    expect(page.get_by_text("Dashboard")).to_be_visible() # Check result

Note the expect helper. It retries until the condition passes or times out. Import it from playwright.sync_api.

If your app talks to backend services, test those pieces separately. For example, you can test API calls with OpenAI function calling in Python, and keep browser tests focused on the UI.

Keeping Tests Fast and Clean

Speed matters. Slow suites get skipped. Keep tests small and focused.

Log in once and reuse the session state. Playwright can save cookies and storage.


# Save state after a manual login
page.context.storage_state(path="auth.json")

# Reuse it in other tests
context = browser.new_context(storage_state="auth.json")

Use page objects for shared pages. A page object wraps common actions in one class.


class LoginPage:
    def __init__(self, page):
        self.page = page

    def login(self, email, password):
        self.page.get_by_label("Email").fill(email)
        self.page.get_by_label("Password").fill(password)
        self.page.get_by_role("button", name="Sign in").click()

Tests then read like plain English. That makes failures easier to understand.

Debugging Failing Tests

Some tests fail only sometimes. These are flaky tests. Playwright gives you tools to find the cause.

Run in headed mode to watch the browser. Use the trace viewer to replay each step.


browser = p.chromium.launch(headless=False)  # Watch the browser
context = browser.new_context()
context.tracing.start(screenshots=True, snapshots=True)  # Record a trace
# ... run test steps ...
context.tracing.stop(path="trace.zip")       # Save for review

Open the trace with a simple command.


playwright show-trace trace.zip

You see screenshots, network calls, and console logs for every step. That usually points straight to the bug.

Running Tests in Parallel

Large suites need speed. The pytest-xdist plugin runs tests across workers.


pip install pytest-xdist
pytest -n 4

Four workers run tests at once. Keep tests independent so this works well. Never share mutable state between tests.

Adding End-to-End Tests to CI

Run your suite on every pull request. A simple GitHub Actions workflow does the job.


pip install -r requirements.txt
playwright install --with-deps
pytest -n 4 --junitxml=report.xml

The --with-deps flag installs system libraries. CI machines need them to run browsers.

Save the JUnit report as an artifact. Your team can see which tests failed without digging through logs.

Best Practices to Remember

Test critical paths first. Login, checkout, and signup matter more than every minor page.

Keep tests independent. Each test should pass alone and in any order.

Avoid fixed waits. Use locators and expect instead of time.sleep.

Test the UI in the browser. Test data and APIs with plain pytest. That mix keeps the suite fast.

If your app handles data processing, test those parts in unit tests. For example, you can cover text parsing with tokenization with NLTK in Python, and cover messaging with Kafka produce and consume tests in Python. Keep browser tests focused on user flows.

Conclusion

Playwright and pytest make a strong team. Playwright handles browsers with auto-waiting and rich debugging. pytest gives you fixtures, parallelism, and clean reports.

Start small. Write one test for your most important flow. Add fixtures, then page objects. Turn on tracing when tests get flaky. Add the suite to CI early so bugs never reach users.

With these steps, your end-to-end tests stay fast, readable, and reliable.