Fixtures are pytest's killer feature. They replace the clunky setUp and tearDown methods from unittest with something far more elegant: dependency injection for tests.
What Are Fixtures?
A fixture is a function that provides data or resources to your tests. Instead of creating test data inside each test, you declare what you need as function parameters:
import pytest
@pytest.fixture
def user():
return {"name": "Alice", "email": "alice@example.com"}
def test_user_has_name(user):
assert user["name"] == "Alice"
def test_user_has_email(user):
assert "email" in userPytest sees that test_user_has_name takes a user parameter, finds the matching fixture, runs it, and passes the result to your test. Clean and explicit.
Fixture Scope
By default, fixtures run once per test function. But you can control this with the scope parameter:
@pytest.fixture(scope="function") # Default: runs for each test
def fresh_list():
return []
@pytest.fixture(scope="class") # Runs once per test class
def class_resource():
return SomeExpensiveObject()
@pytest.fixture(scope="module") # Runs once per module
def db_connection():
conn = create_connection()
yield conn
conn.close()
@pytest.fixture(scope="session") # Runs once for entire test session
def app_config():
return load_config()Use broader scopes for expensive resources like database connections or external services. Use function scope when tests need fresh, isolated data.
Setup and Teardown with yield
The yield statement is your friend for cleanup:
@pytest.fixture
def temp_file():
# Setup
path = Path("/tmp/test_file.txt")
path.write_text("test data")
yield path # This is what the test receives
# Teardown (runs after test completes)
path.unlink()
def test_read_file(temp_file):
assert temp_file.read_text() == "test data"
# File is automatically cleaned up after this testEverything before yield is setup. Everything after is teardown. The teardown runs even if the test fails.
conftest.py: Sharing Fixtures
When multiple test files need the same fixtures, put them in conftest.py:
tests/
├── conftest.py # Fixtures available to all tests
├── test_users.py
├── test_orders.py
└── api/
├── conftest.py # Fixtures for api tests only
└── test_endpoints.py
# tests/conftest.py
import pytest
@pytest.fixture
def api_client():
from myapp import create_app
app = create_app(testing=True)
return app.test_client()
@pytest.fixture
def authenticated_client(api_client):
api_client.post("/login", json={"user": "test", "pass": "test"})
return api_clientPytest automatically discovers conftest.py files. No imports needed—fixtures just work.
Fixtures Using Other Fixtures
Fixtures can depend on other fixtures:
@pytest.fixture
def database():
db = Database(":memory:")
db.create_tables()
yield db
db.close()
@pytest.fixture
def user_repo(database):
return UserRepository(database)
@pytest.fixture
def sample_user(user_repo):
user = User(name="Test User", email="test@example.com")
user_repo.save(user)
return user
def test_find_user(user_repo, sample_user):
found = user_repo.find_by_email("test@example.com")
assert found.name == "Test User"Pytest resolves the dependency chain automatically. When test_find_user runs, it gets both fixtures, properly initialized in order.
Parameterized Fixtures
Fixtures can generate multiple values, running tests for each:
@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database_type(request):
return request.param
def test_connection(database_type):
# This test runs three times, once for each database type
assert database_type in ["sqlite", "postgres", "mysql"]The request Object
The special request fixture gives you information about the test being run:
@pytest.fixture
def resource(request):
name = request.node.name # Current test name
print(f"Setting up for {name}")
resource = create_resource()
def cleanup():
print(f"Tearing down {name}")
resource.close()
request.addfinalizer(cleanup)
return resourceautouse: Fixtures That Always Run
Sometimes you want a fixture to run for every test without explicitly requesting it:
@pytest.fixture(autouse=True)
def reset_environment():
os.environ["MODE"] = "test"
yield
os.environ.pop("MODE", None)Use sparingly. Explicit is usually better than implicit.
Real-World Example
Here's a practical setup for testing a Flask app:
# conftest.py
import pytest
from myapp import create_app, db
@pytest.fixture(scope="session")
def app():
app = create_app({"TESTING": True, "DATABASE_URL": "sqlite://"})
with app.app_context():
db.create_all()
yield app
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def runner(app):
return app.test_cli_runner()
@pytest.fixture
def auth_headers(client):
response = client.post("/auth/login", json={
"username": "testuser",
"password": "testpass"
})
token = response.json["token"]
return {"Authorization": f"Bearer {token}"}Fixtures transform messy test setup into clean, composable building blocks. Start using them, and you'll never go back to the old way.