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