pytest Fixtures Guide
Basic Fixtures & Scope
import pytest
# function scope (default) — new instance per test
@pytest.fixture
def db_connection():
conn = create_connection("sqlite:///:memory:")
yield conn
conn.close()
# module scope — shared across all tests in the file
@pytest.fixture(scope="module")
def http_client():
client = TestClient(app)
return client
# session scope — shared for the entire test session
@pytest.fixture(scope="session")
def app_config():
return load_config("test.yaml")
# class scope — shared within a test class
@pytest.fixture(scope="class")
def browser(request):
driver = webdriver.Chrome()
request.cls.driver = driver
yield driver
driver.quit()
# Scope priority (broadest to narrowest):
# session > package > module > class > function
conftest.py — Shared Fixtures
# conftest.py (at project root or test directory)
import pytest
from myapp import create_app, db as _db
@pytest.fixture(scope="session")
def app():
"""Create application with test configuration."""
app = create_app({"TESTING": True, "DATABASE_URL": "sqlite:///:memory:"})
return app
@pytest.fixture(scope="session")
def db(app):
"""Initialize database once for the session."""
with app.app_context():
_db.create_all()
yield _db
_db.drop_all()
@pytest.fixture
def client(app):
"""HTTP test client, fresh per test."""
return app.test_client()
@pytest.fixture
def auth_headers():
"""JWT auth headers for API tests."""
token = generate_test_token(user_id=1, role="admin")
return {"Authorization": f"Bearer {token}"}
# conftest.py is automatically discovered by pytest
# No import needed in test files — just use fixture names as params
yield Fixtures — Setup & Teardown
import pytest
from unittest.mock import patch
@pytest.fixture
def mock_external_api():
"""Patch external API calls for the duration of the test."""
with patch("myapp.services.requests.get") as mock_get:
mock_get.return_value.json.return_value = {"status": "ok"}
mock_get.return_value.status_code = 200
yield mock_get
# cleanup happens after yield automatically
@pytest.fixture
def temp_user(db):
"""Create a user, clean up after test."""
user = User(name="Test User", email="[email protected]")
db.session.add(user)
db.session.commit()
yield user
db.session.delete(user)
db.session.commit()
# Using multiple fixtures
def test_user_profile(client, temp_user, auth_headers):
response = client.get(f"/users/{temp_user.id}", headers=auth_headers)
assert response.status_code == 200
assert response.json["name"] == "Test User"
Parametrize with Fixtures
import pytest
@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database(request):
"""Run tests against multiple DB backends."""
db_type = request.param
conn = connect_to_db(db_type)
yield conn
conn.close()
def test_insert_record(database):
# This test runs 3 times, once per database type
database.execute("INSERT INTO users (name) VALUES ('Alice')")
result = database.execute("SELECT * FROM users").fetchall()
assert len(result) == 1
# Named parameter sets with ids
@pytest.fixture(params=[
pytest.param({"timeout": 5}, id="fast"),
pytest.param({"timeout": 30}, id="slow"),
])
def config(request):
return request.param
autouse Fixtures
import pytest
# Automatically used by all tests in scope
@pytest.fixture(autouse=True)
def reset_database(db):
"""Roll back all changes after each test."""
yield
db.session.rollback()
# autouse=True scoped to a class
class TestPayments:
@pytest.fixture(autouse=True)
def setup_payment_gateway(self):
with patch("myapp.payments.stripe") as mock_stripe:
mock_stripe.charge.return_value = {"id": "ch_test"}
self.stripe = mock_stripe
yield
def test_charge_card(self):
result = process_payment(amount=100)
assert self.stripe.charge.called
tmpdir / tmp_path — Temporary Files
import pytest
from pathlib import Path
# tmp_path (modern, pathlib.Path, recommended)
def test_writes_output_file(tmp_path):
output_file = tmp_path / "output.txt"
write_report(output_file)
assert output_file.exists()
assert output_file.read_text() == "Report done\n"
# tmp_path_factory for session-scoped temp dirs
@pytest.fixture(scope="session")
def shared_data_dir(tmp_path_factory):
base = tmp_path_factory.mktemp("data")
(base / "sample.csv").write_text("id,name\n1,Alice\n")
return base
# tmpdir (legacy, py.path.local)
def test_config_loading(tmpdir):
cfg = tmpdir.join("config.yaml")
cfg.write("debug: true\nport: 8080\n")
loaded = load_config(str(cfg))
assert loaded["port"] == 8080