Skip to main content

Test Fixtures and Temp Files

What You'll Learn

How to use pytest fixtures to avoid repeating setup code, work with temporary files in tests, and manage shared test state.

What Are Fixtures?

A fixture is a function that provides test setup (and teardown) to tests that request it. Instead of repeating setup in every test:

# ❌ Repeated setup
def test_create_user():
db = Database(":memory:")
db.migrate()
user = db.create_user("Alice", "alice@example.com")
assert user["id"] is not None

def test_get_user():
db = Database(":memory:") # same setup repeated
db.migrate()
user = db.create_user("Alice", "alice@example.com")
found = db.get_user(user["id"])
assert found["name"] == "Alice"

Use a fixture:

# ✅ Fixture used by multiple tests
import pytest
from myapp.database import Database

@pytest.fixture
def db():
database = Database(":memory:")
database.migrate()
yield database # provide the object to the test
database.close() # teardown after the test

def test_create_user(db): # pytest injects the fixture automatically
user = db.create_user("Alice", "alice@example.com")
assert user["id"] is not None

def test_get_user(db):
user = db.create_user("Alice", "alice@example.com")
found = db.get_user(user["id"])
assert found["name"] == "Alice"

yield — Setup and Teardown in One Function

@pytest.fixture
def temp_database():
# SETUP — runs before the test
db = Database("/tmp/test.db")
db.migrate()
print("DB created")

yield db # hand the db to the test

# TEARDOWN — runs after the test (even if test fails)
db.close()
Path("/tmp/test.db").unlink(missing_ok=True)
print("DB cleaned up")

Fixture Scope — How Long Does It Live?

# function (default) — new fixture for each test
@pytest.fixture(scope="function")
def fresh_db():
...

# module — shared by all tests in the same file
@pytest.fixture(scope="module")
def module_db():
...

# session — shared across the entire test run
@pytest.fixture(scope="session")
def api_client():
client = create_client()
yield client
client.close()

Use wider scope for expensive setup (database connections, API clients). Use function scope when tests need isolated state.

Built-In: tmp_path and tmp_path_factory

pytest provides tmp_path — a temporary directory that's automatically cleaned up:

def test_write_and_read_file(tmp_path):
# tmp_path is a pathlib.Path to a unique temp directory
file = tmp_path / "output.txt"
file.write_text("Hello, World!", encoding="utf-8")

content = file.read_text(encoding="utf-8")
assert content == "Hello, World!"

def test_process_directory(tmp_path):
# Create test files
(tmp_path / "data").mkdir()
(tmp_path / "data" / "record1.json").write_text('{"id": 1}', encoding="utf-8")
(tmp_path / "data" / "record2.json").write_text('{"id": 2}', encoding="utf-8")

results = process_directory(tmp_path / "data")
assert len(results) == 2

conftest.py — Shared Fixtures

Fixtures defined in conftest.py are automatically available to all tests in the same directory and subdirectories:

# tests/conftest.py
import pytest
from pathlib import Path
from myapp.database import Database


@pytest.fixture(scope="session")
def test_data_dir() -> Path:
return Path(__file__).parent / "data"


@pytest.fixture
def sample_users() -> list[dict]:
return [
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"},
]


@pytest.fixture
def empty_db():
db = Database(":memory:")
db.migrate()
yield db
db.close()


@pytest.fixture
def seeded_db(empty_db, sample_users):
for user in sample_users:
empty_db.insert("users", user)
return empty_db

Fixtures Calling Other Fixtures

@pytest.fixture
def config():
return {"host": "localhost", "port": 5432}

@pytest.fixture
def db(config): # config injected into db fixture
return Database(config["host"], config["port"])

def test_something(db): # db already has config injected
...

Parametrize with Fixtures

@pytest.fixture(params=["sqlite", "postgres"])
def database(request):
if request.param == "sqlite":
return Database(":memory:")
else:
return Database("postgresql://localhost/testdb")

def test_query(database):
# This test runs twice — once with sqlite, once with postgres
result = database.query("SELECT 1")
assert result is not None

Common Fixture Patterns

JSON test data from files

@pytest.fixture
def sample_config(tmp_path):
import json
config_data = {"host": "localhost", "port": 5432, "debug": True}
config_file = tmp_path / "config.json"
config_file.write_text(json.dumps(config_data), encoding="utf-8")
return config_file

Environment variable fixture

@pytest.fixture
def set_env(monkeypatch):
monkeypatch.setenv("API_KEY", "test-key-123")
monkeypatch.setenv("DEBUG", "true")

Reset global state

@pytest.fixture(autouse=True)
def reset_cache():
cache.clear()
yield
cache.clear()

autouse=True means the fixture runs for every test automatically.

Common Mistakes

MistakeFix
No teardown after yieldAdd cleanup after yield
scope="session" with mutable stateTests interfere — use scope="function"
Not using tmp_path for file testsHardcoded /tmp paths conflict between runs
Fixtures doing too muchSplit large fixtures into smaller ones
Ignoring conftest.pyPut shared fixtures there, not in test files

Quick Reference

# Basic fixture
@pytest.fixture
def my_fixture():
resource = setup()
yield resource
teardown(resource)

# Scoped fixture
@pytest.fixture(scope="session")
def shared_client():
...

# Using tmp_path
def test_fn(tmp_path):
file = tmp_path / "test.txt"
file.write_text("content")

# conftest.py
# (fixtures here are auto-available to all tests)

# autouse — run for every test automatically
@pytest.fixture(autouse=True)
def clean_state():
setup()
yield
teardown()

# monkeypatch env vars
@pytest.fixture
def env(monkeypatch):
monkeypatch.setenv("KEY", "value")

What's Next

Lesson 5: Static Analysis and Formatting