pytest Fixtures: A Complete Guide
Master pytest fixtures for reusable test setup and teardown. Covers scopes, parameterization, factories, autouse, and fixture composition.
What you'll learn
- ✓What fixtures are and why they replace setup/teardown
- ✓Fixture scopes: function, class, module, session
- ✓Parameterized fixtures for testing multiple scenarios
- ✓Factory fixtures and fixture composition
- ✓Autouse fixtures and conftest.py organization
Prerequisites
- •Basic Python and pytest knowledge
- •Understanding of test setup and teardown
- •Familiarity with decorators
Fixtures in pytest are functions that provide test data, set up resources, and tear them down. Unlike the setUp/tearDown pattern in unittest, fixtures are explicit (tests declare which fixtures they need by name), composable (fixtures can use other fixtures), and scoped (a fixture can run once per test, per class, per module, or per entire session).
Your first fixture
import pytest
@pytest.fixture
def sample_user():
return {"name": "Alice", "email": "alice@test.com", "age": 30}
def test_user_name(sample_user):
assert sample_user["name"] == "Alice"
def test_user_email(sample_user):
assert "@" in sample_user["email"]
The test function declares that it needs sample_user by including it as a parameter. pytest injects the fixture automatically. Each test gets a fresh instance.
Fixtures with setup and teardown
Use yield to split setup (before yield) from teardown (after yield):
import pytest
import sqlite3
@pytest.fixture
def db_connection():
# Setup
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")
conn.execute("INSERT INTO users VALUES (1, 'Alice', 'alice@test.com')")
conn.commit()
yield conn # This is what the test receives
# Teardown
conn.close()
def test_query_users(db_connection):
cursor = db_connection.execute("SELECT name FROM users WHERE id = 1")
assert cursor.fetchone()[0] == "Alice"
def test_insert_user(db_connection):
db_connection.execute("INSERT INTO users VALUES (2, 'Bob', 'bob@test.com')")
cursor = db_connection.execute("SELECT COUNT(*) FROM users")
assert cursor.fetchone()[0] == 2
The teardown code after yield runs even if the test fails. This guarantees cleanup.
Fixture scopes
By default, a fixture runs once per test function. You can change the scope to reduce redundant setup:
@pytest.fixture(scope="function") # Default: runs once per test
def per_test_data():
return create_data()
@pytest.fixture(scope="class") # Runs once per test class
def per_class_data():
return create_data()
@pytest.fixture(scope="module") # Runs once per test file
def per_module_data():
return create_data()
@pytest.fixture(scope="session") # Runs once for the entire test session
def per_session_data():
return create_data()
Session-scoped database
@pytest.fixture(scope="session")
def database():
"""Create a test database once for all tests."""
db = create_test_database()
run_migrations(db)
yield db
db.drop()
@pytest.fixture(scope="function")
def db_session(database):
"""Create a transaction for each test and roll back after."""
session = database.create_session()
session.begin()
yield session
session.rollback()
session.close()
The database is created once. Each test gets a session that rolls back, keeping tests isolated without recreating the database.
Parameterized fixtures
Run the same test with multiple fixture values:
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
def database_type(request):
return request.param
def test_connection(database_type):
# This test runs 3 times, once for each database type
conn = connect(database_type)
assert conn.is_connected()
Parameterized with IDs
@pytest.fixture(params=[
pytest.param({"name": "Alice", "age": 30}, id="adult"),
pytest.param({"name": "Bob", "age": 17}, id="minor"),
pytest.param({"name": "Charlie", "age": 0}, id="newborn"),
])
def user_data(request):
return request.param
def test_age_validation(user_data):
# Test output shows: test_age_validation[adult], test_age_validation[minor], etc.
result = validate_user(user_data)
assert isinstance(result, bool)
Factory fixtures
When you need to create multiple instances with different parameters:
@pytest.fixture
def make_user():
"""Factory fixture for creating users."""
created_users = []
def _make_user(name="Alice", email=None, role="user"):
email = email or f"{name.lower()}@test.com"
user = User(name=name, email=email, role=role)
user.save()
created_users.append(user)
return user
yield _make_user
# Cleanup all created users
for user in created_users:
user.delete()
def test_admin_permissions(make_user):
admin = make_user(name="Admin", role="admin")
regular = make_user(name="Regular", role="user")
assert admin.can_delete_users()
assert not regular.can_delete_users()
Fixture composition
Fixtures can use other fixtures:
@pytest.fixture
def user(db_session):
"""Depends on db_session fixture."""
user = User(name="Alice", email="alice@test.com")
db_session.add(user)
db_session.commit()
return user
@pytest.fixture
def auth_token(user):
"""Depends on user fixture, which depends on db_session."""
return generate_token(user.id)
@pytest.fixture
def auth_client(auth_token):
"""Depends on auth_token."""
client = TestClient(app)
client.headers["Authorization"] = f"Bearer {auth_token}"
return client
def test_get_profile(auth_client):
# All fixtures in the chain are set up automatically
response = auth_client.get("/profile")
assert response.status_code == 200
pytest resolves the dependency chain: db_session -> user -> auth_token -> auth_client.
conftest.py
Fixtures defined in conftest.py are available to all tests in the same directory and subdirectories, without needing to import them:
tests/
conftest.py # Fixtures available to ALL tests
test_users.py
api/
conftest.py # Fixtures available to tests in api/
test_endpoints.py
models/
conftest.py # Fixtures available to tests in models/
test_user_model.py
# tests/conftest.py
import pytest
@pytest.fixture(scope="session")
def app():
return create_app(testing=True)
@pytest.fixture
def client(app):
return app.test_client()
# tests/test_users.py
def test_homepage(client): # 'client' is found in conftest.py
response = client.get("/")
assert response.status_code == 200
Autouse fixtures
Fixtures with autouse=True are applied to all tests in their scope automatically, without tests needing to request them:
@pytest.fixture(autouse=True)
def reset_environment():
"""Reset environment variables for every test."""
original = os.environ.copy()
yield
os.environ.clear()
os.environ.update(original)
@pytest.fixture(autouse=True, scope="session")
def setup_logging():
"""Configure logging once for the entire session."""
logging.basicConfig(level=logging.DEBUG)
yield
Use autouse sparingly. It makes the test setup implicit, which can be confusing.
Built-in fixtures
pytest provides several useful built-in fixtures:
def test_tmp_path(tmp_path):
"""tmp_path provides a temporary directory unique to this test."""
file = tmp_path / "data.txt"
file.write_text("hello")
assert file.read_text() == "hello"
def test_capsys(capsys):
"""capsys captures stdout and stderr."""
print("hello")
captured = capsys.readouterr()
assert captured.out == "hello\n"
def test_monkeypatch(monkeypatch):
"""monkeypatch modifies objects, dicts, and env vars."""
monkeypatch.setenv("API_KEY", "test-key")
assert os.environ["API_KEY"] == "test-key"
monkeypatch.setattr(requests, "get", mock_get)
def test_recwarn(recwarn):
"""recwarn captures warnings."""
warnings.warn("deprecation", DeprecationWarning)
assert len(recwarn) == 1
assert recwarn[0].category == DeprecationWarning
Fixture finalization with addfinalizer
An alternative to yield-based teardown:
@pytest.fixture
def smtp_connection(request):
conn = smtplib.SMTP("smtp.test.com")
def cleanup():
conn.close()
request.addfinalizer(cleanup)
return conn
Use yield for simple cases and addfinalizer when you need multiple cleanup steps or conditional cleanup.
Practical example: API test suite
# conftest.py
import pytest
from myapp import create_app, db as _db
@pytest.fixture(scope="session")
def app():
app = create_app(config="testing")
with app.app_context():
_db.create_all()
yield app
_db.drop_all()
@pytest.fixture(scope="function")
def db(app):
with app.app_context():
_db.session.begin_nested()
yield _db
_db.session.rollback()
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def make_user(db):
def _make(name="Test User", email="test@test.com"):
user = User(name=name, email=email)
db.session.add(user)
db.session.commit()
return user
return _make
@pytest.fixture
def authenticated_client(client, make_user):
user = make_user()
token = create_token(user)
client.environ_base["HTTP_AUTHORIZATION"] = f"Bearer {token}"
return client
# test_api.py
def test_create_user(client):
response = client.post("/api/users", json={
"name": "Alice",
"email": "alice@test.com"
})
assert response.status_code == 201
assert response.json["name"] == "Alice"
def test_get_profile(authenticated_client):
response = authenticated_client.get("/api/profile")
assert response.status_code == 200
assert "name" in response.json
Summary
pytest fixtures replace setup/teardown with explicit, composable, scoped functions. Use yield for cleanup, scopes to control lifecycle (function, class, module, session), and parameterization to run tests with multiple inputs. Put shared fixtures in conftest.py for automatic discovery. Factory fixtures create multiple instances with different configurations. The fixture dependency chain is resolved automatically by pytest, so you focus on declaring what each test needs rather than managing setup order.
Related articles
- Testing End-to-End Testing with Cypress
Write reliable E2E tests with Cypress — selectors, assertions, network stubbing, custom commands, and CI integration.
- Testing Integration Tests vs Unit Tests: Finding the Right Balance
Understand the tradeoffs between unit tests and integration tests. Learn the testing pyramid, trophy, and practical strategies for effective test suites.
- Testing Mocking Strategies for Unit Tests
Learn when and how to mock dependencies in unit tests. Covers stubs, spies, fakes, mock patterns, and how to avoid over-mocking.
- Testing Testing React Components with Testing Library
Learn to test React components the right way with React Testing Library. Covers rendering, queries, user events, async testing, and best practices.