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", "[email protected]")
assert user["id"] is not None

def test_get_user():
db = Database(":memory:") # same setup repeated
db.migrate()
user = db.create_user("Alice", "[email protected]")
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", "[email protected]")
assert user["id"] is not None

def test_get_user(db):
user = db.create_user("Alice", "[email protected]")
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": "[email protected]"},
{"id": 2, "name": "Bob", "email": "[email protected]"},
]


@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