Skip to main content

What are Test Fixtures?

Fixtures are functions that run before (and sometimes after) your test functions. They provide a fixed baseline for your tests by setting up necessary preconditions like test data, database connections, or initialized objects.
In pytest, fixtures are defined using the @pytest.fixture decorator and can be shared across multiple tests.

Why Use Fixtures?

Reusability

Write setup code once and use it across multiple tests

Clean Code

Keep test functions focused on testing, not setup

Dependency Injection

Automatically provide dependencies to tests that need them

Scope Control

Control when fixtures are created and destroyed

Real Fixtures from Our Test Suite

Let’s examine the actual fixtures defined in our project:
import json
from pathlib import Path
import pytest

@pytest.fixture(scope="session")
def users():
    """
    Loads testData/users.json and returns a dict.
    Accessible in any test as the 'users' fixture.
    """
    root = Path(__file__).parent.parent  # go from /tests to project root
    data_path = root / "testData" / "users.json"
    with data_path.open(encoding="utf-8") as f:
        return json.load(f)

Understanding Fixture Scopes

Fixtures can have different scopes that control how often they’re created:
Created once per test session
@pytest.fixture(scope="session")
def users():
    # Created once, shared across all tests
    data_path = Path(__file__).parent.parent / "testData" / "users.json"
    with data_path.open(encoding="utf-8") as f:
        return json.load(f)
Use when:
  • Loading static data that doesn’t change
  • Setting up expensive resources
  • Data is read-only
Our users fixture uses session scope because the JSON data is loaded once and never modified.

Using Fixtures in Tests

Here are real examples from our test suite showing fixture usage:
tests/test_functionalities.py
from pages.login import LoginPage

def test_with_testdata(page, users):
    login_page = LoginPage(page)
    login_page.navigate()
    login_page.login(users["validUser"]["username"], users["validUser"]["password"])

    assert page.get_by_test_id("title").is_visible
How it works:
  1. Test declares users parameter
  2. pytest automatically calls the users() fixture
  3. Fixture loads JSON data
  4. Test receives the data dictionary
  5. Test accesses nested values: users["validUser"]["username"]
The page parameter is also a fixture, provided by Playwright’s pytest plugin.

The conftest.py File

conftest.py is a special pytest file where you define fixtures that should be available to all tests in the directory and subdirectories.

Our Project Structure

source/
├── tests/
│   ├── conftest.py          # Fixtures available to all tests
│   ├── test_login.py
│   ├── test_functionalities.py
│   └── test_assertions.py
├── pages/
│   ├── login.py
│   └── cart_page.py
└── testData/
    └── users.json           # Loaded by users fixture

Complete conftest.py Breakdown

import os
from pathlib import Path
import pytest
from dotenv import load_dotenv
import json
  • Path: For cross-platform file path handling
  • pytest: For fixture decorators
  • dotenv: For loading environment variables
  • json: For parsing JSON test data
@pytest.fixture(scope="session")
def users():
    """
    Loads testData/users.json and returns a dict.
    Accessible in any test as the 'users' fixture.
    """
    root = Path(__file__).parent.parent  # go from /tests to project root
    data_path = root / "testData" / "users.json"
    with data_path.open(encoding="utf-8") as f:
        return json.load(f)
Key points:
  • Uses Path(__file__).parent.parent to navigate to project root
  • Opens and parses JSON file
  • Returns dictionary structure from users.json:line_1
  • Session scope means it loads once for all tests
load_dotenv(dotenv_path=Path(__file__).parent.parent / ".env")

@pytest.fixture(scope="session")
def creds():
    """
    Provides credentials from environment variables.
    Fails early with a clear message if missing.
    """
    user = os.getenv("USERNAME")
    pwd = os.getenv("PASSWORD")
    if not user or not pwd:
        raise RuntimeError("Missing USERNAME/PASSWORD in environment. ")
    return {"valid_user": user, "pwd": pwd}
Key points:
  • load_dotenv() called at module level (runs once on import)
  • Reads from .env file in project root
  • Validates that required variables exist
  • Returns dictionary with consistent key names

Advanced Fixture Patterns

Use yield for setup and teardown:
@pytest.fixture
def logged_in_page(page):
    # Setup: Login before test
    login_page = LoginPage(page)
    login_page.navigate()
    login_page.login("standard_user", "secret_sauce")
    
    yield page  # Test runs here
    
    # Teardown: Logout after test
    page.locator("#logout_sidebar_link").click()
Usage:
def test_cart_actions(logged_in_page):
    # Page is already logged in
    cart = CartPage(logged_in_page)
    cart.add_product("sauce-labs-backpack")
    # After test, logout happens automatically

Best Practices

# Good: Clear what it provides
@pytest.fixture
def valid_user_credentials():
    return {"username": "standard_user", "password": "secret_sauce"}

# Avoid: Vague naming
@pytest.fixture
def data():
    return {"username": "standard_user", "password": "secret_sauce"}
# Session: Static data (fast, but shared)
@pytest.fixture(scope="session")
def users():
    return load_json("users.json")

# Function: Fresh instances (slower, but isolated)
@pytest.fixture
def cart_page(page):
    return CartPage(page)
@pytest.fixture(scope="session")
def users():
    """
    Loads testData/users.json and returns a dict.
    Accessible in any test as the 'users' fixture.
    
    Structure:
    {
        "validUser": {"username": "...", "password": "..."},
        "invalidUser": {"username": "...", "password": "..."}
    }
    """
    # implementation
@pytest.fixture(scope="session")
def creds():
    user = os.getenv("USERNAME")
    pwd = os.getenv("PASSWORD")
    if not user or not pwd:
        # Clear error message
        raise RuntimeError("Missing USERNAME/PASSWORD in environment. ")
    return {"valid_user": user, "pwd": pwd}

Common Pitfalls

Mutable Fixture Data with Session Scope
# Dangerous: Session-scoped mutable data
@pytest.fixture(scope="session")
def user_list():
    return ["user1", "user2"]  # Same list shared across tests

def test_modify_users(user_list):
    user_list.append("user3")  # Affects other tests!
Solution: Use function scope for mutable data or return copies.
Forgetting to Use Fixture Parameters
# Wrong: Fixture defined but not used
@pytest.fixture
def users():
    return load_json("users.json")

def test_login(page):  # Missing 'users' parameter
    # Can't access users fixture!
Solution: Add fixture name as test parameter: def test_login(page, users):

Next Steps

Page Object Model

Learn how fixtures work with page objects

Test Data

Explore different ways to manage test data

Build docs developers (and LLMs) love