Tutorial Material

Pytest Fixtures Deep Dive

Share to
Pytest Fixtures

In our introductory testing tutorial, we learned how to write basic tests using pytest. However, real-world applications require establishing state before tests run (like connecting to a database or creating dummy data) and cleaning up afterwards. In pytest, this is elegantly handled by Fixtures.

What is a Fixture?

A fixture is a function that pytest runs before (and optionally after) your actual test functions. You request a fixture simply by adding its name as an argument to your test function.

# non-runnable: requires pytest
import pytest

@pytest.fixture
def sample_user():
    return {"id": 1, "username": "jane_doe", "role": "admin"}

def test_user_is_admin(sample_user):
    # pytest automatically injects the return value of sample_user here
    assert sample_user["role"] == "admin"

Setup and Teardown (Yield)

Fixtures aren't just for returning data; they are for managing resources. If you use the yield keyword instead of return, the code before yield is the setup, and the code after yield is the teardown (cleanup) which runs after the test finishes.

# non-runnable: requires pytest
import pytest

@pytest.fixture
def database_connection():
    # Setup
    db = connect_to_database()
    print("Database connected")
    
    yield db  # This is where the testing happens
    
    # Teardown
    db.close()
    print("Database connection closed")

def test_insert_user(database_connection):
    database_connection.insert("dummy user")
    assert database_connection.count() == 1

Fixture Scopes

By default, a fixture runs once per test function (scope="function"). If a fixture is expensive to run (like starting a docker container), you can change its scope to run less often.

# non-runnable: requires pytest
@pytest.fixture(scope="session")
def expensive_api_client():
    # This will only be executed once, even if 100 tests request it
    client = ExpensiveAPIClient()
    client.authenticate()
    yield client
    client.logout()

Using Multiple Fixtures

Tests can request multiple fixtures.

# non-runnable: requires pytest
@pytest.fixture
def user():
    return User(name="Alice")

@pytest.fixture
def shopping_cart():
    return Cart()

def test_add_to_cart(user, shopping_cart):
    shopping_cart.add(user, "Laptop")
    assert "Laptop" in shopping_cart.items

The conftest.py file

If you want fixtures to be available across multiple test files without importing them manually, define them in a file named conftest.py at the root of your test directory. Pytest magically discovers it.

Conclusion

Fixtures are the heart of pytest. They promote reusability, separate setup logic from test logic, and make managing external resources incredibly safe and intuitive using the yield pattern.