Fixtures are pytest’s way of preparing everything a test needs and cleaning up afterwards. The page fixture you have used since Lesson 4 is one, provided by pytest-playwright. In this lesson you will write your own: simple data fixtures, setup and teardown with yield, fixtures that depend on other fixtures, scopes that control how often a fixture runs, autouse fixtures, factories, and a logged-in page fixture for Playwright tests.
📖 What You’ll Learn: what a fixture is and how tests request it by name, return vs yield, teardown that runs even after a failure, fixture scopes (function, class, module, package, session) and how to see them with –setup-show, fixtures that use other fixtures, autouse, the request object, factory fixtures, useful built-in fixtures, and a reusable logged-in Playwright page.
🧩 What Is a Fixture?
A fixture is a function decorated with @pytest.fixture. A test asks for it by adding a parameter with the same name; pytest runs the fixture first and passes its result in. This is called *dependency injection*: the test declares what it needs, and pytest provides it.
📄 tests/test_data_fixtures.py
import pytest
@pytest.fixture
def user():
"""Test data for a standard user."""
return {"username": "standard_user", "password": "secret_sauce"}
@pytest.fixture
def cart_items():
return ["Sauce Labs Backpack", "Sauce Labs Onesie"]
def test_user_has_password(user):
assert user["password"] == "secret_sauce"
def test_cart_count(cart_items):
assert len(cart_items) == 2
def test_both_fixtures(user, cart_items):
assert user["username"].startswith("standard")
assert "Sauce Labs Backpack" in cart_items
- Each test lists the fixtures it needs as parameters, in any order.
- The fixture runs once per test that uses it by default, so every test gets a fresh dictionary or list. One test changing its data can never affect another.
- The docstring appears in
pytest --fixtures, which lists every fixture available to your tests.
🖥️ Output
... [100%]
3 passed in 0.01s
🔁 Setup and Teardown with yield
Many resources must be cleaned up: files, test users, database rows, browser contexts. Put the setup before yield, give the test its value with yield, and put the cleanup after it (tests/test_yield.py):
📄 tests/test_yield.py
import pytest
@pytest.fixture
def temp_user():
print("\n [setup] create user qa_temp")
user = {"name": "qa_temp", "active": True}
yield user # the test runs here
print(" [teardown] delete user qa_temp") # runs after the test, pass or fail
def test_user_is_active(temp_user):
print(" [test] checking user")
assert temp_user["active"]
def test_failure_still_tears_down(temp_user):
print(" [test] about to fail on purpose")
with pytest.raises(AssertionError):
assert temp_user["name"] == "someone_else"
⌨️ Terminal
pytest -q -s tests/test_yield.py
🖥️ Output
[setup] create user qa_temp
[test] checking user
. [teardown] delete user qa_temp
[setup] create user qa_temp
[test] about to fail on purpose
. [teardown] delete user qa_temp
2 passed in 0.01s
- The order is always setup, test, teardown. (The dot that marks a passed test is printed between the test and its teardown.)
- Teardown code after
yieldruns even when the test fails, because pytest resumes the fixture after the test finishes, whatever the result. - If the setup part raises an exception, the test is reported as an *error* and the teardown part does not run (there was nothing set up to clean).
This is exactly how pytest-playwright’s context and page fixtures work: create a context and page, yield the page to your test, close them afterwards.
📏 Fixture Scope: How Often Does It Run?
scope= decides how long a fixture’s value lives and how often it is created:
| Scope | Created once per | Typical use |
|---|---|---|
function (default) | test | Pages, test data, anything a test may change |
class | test class | Shared state for a group of related tests |
module | test file | Expensive setup used by one file |
package | package (folder with __init__.py) | Shared setup for a folder |
session | whole test run | Browsers, API clients, config loaded once |
pytest can show exactly when each fixture is set up and torn down with --setup-show (tests/test_scopes.py):
📄 tests/test_scopes.py
import pytest
@pytest.fixture(scope="session")
def config():
return {"base_url": "https://www.saucedemo.com/"}
@pytest.fixture(scope="module")
def api_token(config):
return "token-for-" + config["base_url"]
@pytest.fixture
def cart():
return []
def test_first(config, api_token, cart):
cart.append("Backpack")
assert len(cart) == 1
def test_second(config, api_token, cart):
assert cart == [] # a fresh cart: function scope
These tests do not use a browser, so the command below adds -p no:playwright to switch the pytest-playwright plugin off for this run and keep the output short:
⌨️ Terminal
pytest -q --setup-show -p no:playwright tests/test_scopes.py
🖥️ Output
SETUP S config
SETUP M api_token (fixtures used: config)
SETUP F cart
tests/test_scopes.py::test_first (fixtures used: api_token, cart, config) .
TEARDOWN F cart
SETUP F cart
tests/test_scopes.py::test_second (fixtures used: api_token, cart, config) .
TEARDOWN F cart
TEARDOWN M api_token
TEARDOWN S config
2 passed in 0.00s
With the plugin active, the same command also lists pytest-playwright’s own automatic fixtures, such as SETUP S delete_output_dir (which clears test-results once per run) and a function-scoped _pw_trace_api_requests. Fixtures whose names start with _ are the plugin’s internals; you never request them yourself.
Reading it:
S,MandFmark session, module and function scope.configandapi_tokenwere created once and reused by both tests;cartwas created fresh for each test, which is why the second test sees an empty list.- Teardown happens in reverse order: function first, then module, then session.
api_tokenasks forconfig: fixtures can use other fixtures, and pytest resolves the whole chain for you.
⚠️ Watch out: A fixture can only use fixtures of the same or wider scope. A session fixture that asks for a function-scoped fixture (such as page) fails with ScopeMismatch: You tried to access the function scoped fixture page with a session scoped request object., because the session value would outlive the page.
Scope in pytest-playwright
pytest-playwright uses these scopes to make tests fast and isolated at the same time:
| Fixture | Scope | Why |
|---|---|---|
playwright, browser_type, browser, browser_name | session | Launching a browser is slow, so it happens once per run |
browser_context_args | session | Settings are calculated once |
context, page | function | Every test gets a clean, isolated session |
🤖 autouse Fixtures
A fixture with autouse=True runs for every test in its scope without being requested. Use it for things every test needs, such as resetting state or setting timeouts:
@pytest.fixture(autouse=True)
def short_action_timeout(page):
page.set_default_timeout(10_000)
Use autouse sparingly: hidden setup makes tests harder to understand. If only some tests need something, request it explicitly.
🏭 Factory Fixtures and the request Object
Sometimes a test needs several objects of the same kind, with different settings. Return a function (a *factory*) instead of a value. And when a fixture needs information about the test that uses it, ask for the built-in request fixture (tests/test_factory.py):
📄 tests/test_factory.py
import pytest
@pytest.fixture
def make_user():
created = []
def _make_user(name: str, role: str = "viewer") -> dict:
user = {"name": name, "role": role}
created.append(user)
return user
yield _make_user
print(f"\n cleaning up {len(created)} users: {[u['name'] for u in created]}")
def test_two_users(make_user, request):
admin = make_user("ana", role="admin")
viewer = make_user("ben")
print(f"\n running {request.node.name}")
assert admin["role"] == "admin" and viewer["role"] == "viewer"
make_useryields the inner function_make_user; the test calls it as many times as it wants.- The fixture remembers what it created and cleans everything up after the test.
request.node.nameis the current test’s name. Therequestobject also gives access to markers, the config and parametrized values.
🧰 Useful Built-in Fixtures
pytest itself provides fixtures you can request at any time (tests/test_builtins.py):
📄 tests/test_builtins.py
import os
def test_tmp_path(tmp_path):
report = tmp_path / "report.txt" # a fresh folder for this test
report.write_text("2 passed")
assert report.read_text() == "2 passed"
def test_monkeypatch_env(monkeypatch):
monkeypatch.setenv("BASE_URL", "https://staging.example.com")
assert os.environ["BASE_URL"] == "https://staging.example.com"
# the variable is restored automatically after the test
def test_capsys(capsys):
print("Products")
captured = capsys.readouterr()
assert captured.out == "Products\n"
| Fixture | What it gives you |
|---|---|
tmp_path | A unique temporary folder (pathlib.Path) per test |
tmp_path_factory | Temporary folders for session-scoped fixtures |
monkeypatch | Temporarily change environment variables, attributes or dict items; undone after the test |
capsys | Capture what the test printed |
request | Information about the requesting test (name, markers, params, config) |
pytestconfig | Access to command-line options and ini values |
⌨️ Terminal
pytest -q -s tests/test_factory.py tests/test_builtins.py
🖥️ Output
running test_two_users
.
cleaning up 2 users: ['ana', 'ben']
...
4 passed in 0.02s
🎭 A Logged-in Page Fixture for Playwright
Here is the pattern you will use constantly: a fixture that builds on page, logs in, and hands the test a page that is already on the products screen (tests/test_logged_in.py):
📄 tests/test_logged_in.py
import re
import pytest
from playwright.sync_api import Page, expect
@pytest.fixture
def logged_in_page(page: Page) -> Page:
page.goto("https://www.saucedemo.com/")
page.get_by_placeholder("Username").fill("standard_user")
page.get_by_placeholder("Password").fill("secret_sauce")
page.get_by_role("button", name="Login").click()
expect(page).to_have_url(re.compile(r".*/inventory\.html"))
return page
def test_six_products_listed(logged_in_page: Page):
expect(logged_in_page.locator(".inventory_item")).to_have_count(6)
def test_add_to_cart(logged_in_page: Page):
logged_in_page.locator("[data-test='add-to-cart-sauce-labs-backpack']").click()
expect(logged_in_page.locator("[data-test='shopping-cart-badge']")).to_have_text("1")
logged_in_pagerequestspage(function scope), so it is function-scoped too, and every test gets its own fresh, logged-in session.- The
expectinside the fixture makes the setup fail loudly (as an *error*) if login is broken, instead of producing confusing failures later. - The tests are now about behaviour, not login steps.
🖥️ Output
.. [100%]
2 passed in 3.73s
In Module 8 you will make this even faster by logging in once and reusing the saved session with storage_state.
❓ Frequently Asked Questions
🤔 Where should I put fixtures that many test files use?
In a conftest.py file. Fixtures defined there are available to all tests in that folder and below without imports. That is the next lesson.
🤔 Can a fixture return nothing?
Yes. A fixture that only does setup (such as setting a timeout) can return None; tests request it, or it is autouse=True.
🤔 What happens if two fixtures have the same name?
The one closest to the test wins: a fixture in the test file overrides one in conftest.py, which overrides one from a plugin. pytest-playwright’s browser_context_args customisation in Lesson 6 relies on exactly this.
🤔 Should I use a session-scoped page to make tests faster?
No. Sharing a page lets tests leak state into each other and makes failures depend on test order. Share the browser (pytest-playwright already does), not the page.
🏁 Summary
- Fixtures are requested by parameter name; pytest runs them and injects the result.
yieldseparates setup from teardown, and teardown runs even after a failed test.- Scope controls lifetime: function (default), class, module, package, session;
--setup-showreveals the order. - Fixtures can use other fixtures of the same or wider scope; factories create many objects; built-ins include
tmp_path,monkeypatch,capsysandrequest.
Next, you will organise fixtures across your project with conftest.py files and learn how pytest overrides them.
🎬 Watch the Playwright tutorial series on YouTube!
Subscribe to SqaToolsByDY • Practice on the SQA Tools Automation Practice Page