Fixture Setup/Teardown: Reusable Test Context Lifecycle
How to use setup and teardown fixtures to create reusable test context. Covers beforeEach, factory functions, fixture objects, and cleanup with examples.
Overview
The fixture setup/teardown pattern gives tests a defined lifecycle for their context: build what the test needs, run the test, then clean it up — always, pass or fail. Instead of duplicating initialization code across tests, you write the setup once in a fixture that runs before each test (or suite) and a teardown that runs after. The payoff is less copy-paste, a consistent starting state, and no leaked database connections, temp files, or sockets polluting the next test.
When to Use
- Tests that share common initialization (database connections, test data, mock objects)
- Tests requiring cleanup of resources (temp files, network connections, environment variables)
- When test setup is complex or error-prone — centralize it in a fixture
- When tests must run in isolation — fixtures ensure clean state between tests
- Teams that want DRY test code — avoid copy-pasting setup across dozens of tests
When NOT to Use
- Trivial tests with no shared setup — inline setup is simpler
- When setup is unique per test — a fixture adds indirection without benefit
- For one-off scripts — fixtures are for structured test suites
- When the fixture is more complex than the tests it serves — simplify
Explanation
The fixture lifecycle
Every fixture runs the same three-phase cycle, once per scope boundary:
The guarantee that matters is on the right side of the diagram: teardown runs even when the test fails. That’s what keeps a crashed test from leaking state into the rest of the suite. What it does not cover is setup itself failing — if code before the yield (or inside beforeEach) throws, the teardown side never executes, so anything allocated before that point must be wrapped in try/finally or registered with request.addfinalizer() in pytest.
Choosing a scope
How often a fixture runs decides both its cost and its safety. Mutable state belongs in function scope; expensive, read-only context can be shared wider.
| Scope | Runs | Good for | Risk if misused |
|---|---|---|---|
function (default) | Once per test | Mutable state, DB rows, temp files | Slower when setup is expensive |
class / module | Once per class or file | Shared read-only fixtures, config | Mutations leak across tests in the same scope |
session | Once per suite | Docker containers, app boot, config loads | Order-dependent tests; state bleed invisible in CI |
In Jest the same trade-off maps to hooks: beforeEach/afterEach is function scope, beforeAll/afterAll is roughly module scope. JUnit mirrors it with @BeforeEach/@AfterEach (per test) and @BeforeAll/@AfterAll (once per class).
Teardown semantics by framework
- pytest: code after
yieldruns even if the test fails. If setup raises before theyield, nothing after it runs — userequest.addfinalizer()or atry/finallyaround the risky part. - Jest:
afterEachruns after the test body even on failure, and also runs ifbeforeEachthrows.afterAllstill runs if any test in the block failed. - JUnit 5:
@AfterEachexecutes even when the test or a@BeforeEachfails.@AfterAllruns once the class finishes, regardless of individual results.
Nested and composed fixtures
Fixtures compose like a stack: setup resolves outermost-in, teardown unwinds innermost-out. In the scoped example above, test_database (module) is created before clean_db (function), and clean_db’s teardown runs before the module fixture is torn down. In Jest, nested describe blocks work the same way — inner beforeEach hooks run after outer ones, inner afterEach hooks run before outer ones.
That ordering is what makes composition safe: a cheap function-scoped fixture can mutate a shared expensive one and reset it afterwards, as clean_db does with truncate_all().
Solution
Runnable versions of every example live in the companion repository.
Basic setup/teardown in Jest
// JavaScript — Jest setup/teardown
describe('UserService', () => {
let service;
let dbConnection;
beforeEach(() => {
// Setup: runs before each test
dbConnection = createInMemoryDB();
service = new UserService(dbConnection);
service.createUser('Alice', 'alice@x.com');
});
afterEach(() => {
// Teardown: runs after each test
dbConnection.close();
service = null;
});
test('finds existing user', () => {
const user = service.getUserByEmail('alice@x.com');
expect(user).toBeDefined();
expect(user.name).toBe('Alice');
});
test('rejects duplicate email', () => {
expect(() => service.createUser('Bob', 'alice@x.com')).toThrow(DuplicateEmailError);
});
});
Setup/teardown in pytest
# Python — pytest with fixtures
import pytest
from myapp.services import UserService
from myapp.repositories import FakeUserRepository
@pytest.fixture
def repo():
"""Fresh in-memory repository for each test."""
return FakeUserRepository()
@pytest.fixture
def service(repo):
"""UserService with a fresh repository."""
return UserService(repo)
@pytest.fixture
def service_with_user(service):
"""UserService with one pre-created user."""
service.create_user("Alice", "alice@x.com")
return service
class TestUserService:
def test_create_user(self, service):
user = service.create_user("Bob", "bob@x.com")
assert user.name == "Bob"
assert user.email == "bob@x.com"
def test_find_user(self, service_with_user):
user = service_with_user.get_user_by_email("alice@x.com")
assert user.name == "Alice"
def test_duplicate_email(self, service_with_user):
with pytest.raises(DuplicateEmailError):
service_with_user.create_user("Bob", "alice@x.com")
Setup/teardown with yield (pytest)
# Python — pytest fixture with yield for teardown
import pytest
import tempfile
import os
@pytest.fixture
def temp_db():
"""Create a temp SQLite database, yield it, then clean up."""
fd, path = tempfile.mkstemp(suffix='.db')
os.close(fd)
db = SQLiteDB(path)
db.initialize_schema()
yield db # Test runs here
# Teardown: runs after test
db.close()
os.unlink(path)
@pytest.fixture
def populated_db(temp_db):
"""Database with seed data."""
temp_db.insert('users', {'name': 'Alice', 'email': 'alice@x.com'})
temp_db.insert('users', {'name': 'Bob', 'email': 'bob@x.com'})
yield temp_db
def test_user_count(populated_db):
assert populated_db.count('users') == 2
def test_find_alice(populated_db):
user = populated_db.find_one('users', {'name': 'Alice'})
assert user['email'] == 'alice@x.com'
Setup/teardown in JUnit
// Java — JUnit 5 setup/teardown
import org.junit.jupiter.api.*;
class UserServiceTest {
private UserService service;
private FakeUserRepository repo;
@BeforeEach
void setUp() {
// Runs before each test
repo = new FakeUserRepository();
service = new UserService(repo);
service.createUser("Alice", "alice@x.com");
}
@AfterEach
void tearDown() {
// Runs after each test
repo.clear();
service = null;
}
@BeforeAll
static void setUpAll() {
// Runs once before all tests in the class
System.setProperty("TEST_MODE", "true");
}
@AfterAll
static void tearDownAll() {
// Runs once after all tests in the class
System.clearProperty("TEST_MODE");
}
@Test
void testFindUser() {
User user = service.findByEmail("alice@x.com");
assertEquals("Alice", user.getName());
}
@Test
void testDuplicateEmail() {
assertThrows(DuplicateEmailException.class, () -> {
service.createUser("Bob", "alice@x.com");
});
}
}
Factory function pattern
// JavaScript — factory function for test data
function createTestUser(overrides = {}) {
return {
id: 1,
name: 'Alice',
email: 'alice@x.com',
role: 'member',
createdAt: new Date('2026-01-01'),
...overrides,
};
}
function createTestOrder(overrides = {}) {
return {
id: 100,
userId: 1,
items: [
{ productId: 1, quantity: 2, price: 10.0 },
{ productId: 2, quantity: 1, price: 25.0 },
],
status: 'pending',
total: 45.0,
...overrides,
};
}
describe('OrderService', () => {
let service;
beforeEach(() => {
service = new OrderService(new FakeOrderRepository());
});
test('calculates order total', () => {
const order = createTestOrder({
items: [
{ productId: 1, quantity: 3, price: 15.0 },
],
});
const total = service.calculateTotal(order);
expect(total).toBe(45.0);
});
test('rejects empty order', () => {
const order = createTestOrder({ items: [] });
expect(() => service.calculateTotal(order)).toThrow(EmptyOrderError);
});
test('applies discount for premium user', () => {
const user = createTestUser({ role: 'premium' });
const order = createTestOrder({ userId: user.id });
const total = service.calculateTotal(order, user);
expect(total).toBe(40.5); // 10% discount
});
});
Fixture object pattern
Bundle several collaborators behind one object when tests need more than one dependency. The notifier below is a stand-in for a real service — see the Test Double pattern for when to pick a mock versus a fake.
# Python — fixture object bundling multiple fixtures
@pytest.fixture
def test_context():
"""Bundle all test dependencies into one object."""
repo = FakeUserRepository()
service = UserService(repo)
notifier = MagicMock()
service.set_notifier(notifier)
class TestContext:
def __init__(self, repo, service, notifier):
self.repo = repo
self.service = service
self.notifier = notifier
def create_user(self, name, email):
return self.service.create_user(name, email)
def assert_notified(self, email):
self.notifier.send.assert_called_once()
assert self.notifier.send.call_args.kwargs['to'] == email
return TestContext(repo, service, notifier)
def test_user_creation_notifies(test_context):
test_context.create_user("Alice", "alice@x.com")
test_context.assert_notified("alice@x.com")
Scoped fixtures
# Python — pytest fixture scopes
@pytest.fixture(scope="session")
def test_config():
"""Loaded once per test session."""
return load_config("test.yaml")
@pytest.fixture(scope="module")
def test_database():
"""One database per test module."""
db = TestDatabase()
db.connect()
yield db
db.disconnect()
db.drop_all_tables()
@pytest.fixture(scope="function")
def clean_db(test_database):
"""Fresh state per test function."""
test_database.truncate_all()
yield test_database
test_database.truncate_all()
class TestUserRepository:
def test_create(self, clean_db):
clean_db.insert('users', {'name': 'Alice'})
assert clean_db.count('users') == 1
def test_count_empty(self, clean_db):
assert clean_db.count('users') == 0 # Truncated by fixture
Environment variable setup/teardown
# Python — manage environment variables in tests
import os
import pytest
@pytest.fixture
def env_vars():
"""Set and restore environment variables."""
original = dict(os.environ)
os.environ['APP_ENV'] = 'test'
os.environ['DATABASE_URL'] = 'sqlite:///:memory:'
os.environ['REDIS_URL'] = 'redis://localhost:6379/15'
yield os.environ
# Restore original environment
os.environ.clear()
os.environ.update(original)
def test_app_starts_in_test_mode(env_vars):
assert env_vars['APP_ENV'] == 'test'
assert env_vars['DATABASE_URL'] == 'sqlite:///:memory:'
Nested fixtures in Jest
// JavaScript — nested describe blocks with scoped setup
describe('OrderService', () => {
let service;
beforeEach(() => {
service = new OrderService();
});
describe('with pending order', () => {
let order;
beforeEach(() => {
order = service.createOrder({ userId: 1, total: 100 });
});
test('can be cancelled', () => {
service.cancel(order.id);
expect(service.getOrder(order.id).status).toBe('cancelled');
});
test('can be completed', () => {
service.complete(order.id);
expect(service.getOrder(order.id).status).toBe('completed');
});
});
describe('with completed order', () => {
let order;
beforeEach(() => {
order = service.createOrder({ userId: 1, total: 100 });
service.complete(order.id);
});
test('cannot be cancelled', () => {
expect(() => service.cancel(order.id)).toThrow(InvalidStateError);
});
});
});
Variants
Parameterized fixtures
A fixture with params reruns each test once per value — the fixture-level version of the Parameterized Test pattern.
# Python — parametrized fixture for testing multiple configs
@pytest.fixture(params=[
{"engine": "sqlite", "url": "sqlite:///:memory:"},
{"engine": "postgres", "url": "postgresql://localhost/test"},
{"engine": "mysql", "url": "mysql://localhost/test"},
])
def db(request):
config = request.param
db = Database.connect(config["url"])
db.create_tables()
yield db
db.drop_tables()
db.close()
def test_user_crud(db):
db.insert('users', {'name': 'Alice'})
user = db.find_one('users', {'name': 'Alice'})
assert user is not None
External resource fixtures
# Python — fixture for Docker-based test database
import pytest
import subprocess
import time
@pytest.fixture(scope="session")
def postgres_container():
"""Start a PostgreSQL Docker container for the test session."""
container_id = subprocess.check_output([
"docker", "run", "-d", "-p", "5432:5432",
"-e", "POSTGRES_PASSWORD=test",
"postgres:16-alpine"
]).decode().strip()
# Poll until PostgreSQL accepts connections — sleep() alone is flaky in CI
for _ in range(30):
ready = subprocess.run(
["docker", "exec", container_id, "pg_isready"],
capture_output=True,
)
if ready.returncode == 0:
break
time.sleep(1)
else:
subprocess.run(["docker", "stop", container_id])
raise RuntimeError("postgres container did not become ready")
yield {
"host": "localhost",
"port": "5432",
"password": "test",
}
# Teardown: stop and remove container
subprocess.run(["docker", "stop", container_id])
subprocess.run(["docker", "rm", container_id])
Snapshot-based setup
// JavaScript — save and restore state between tests
describe('Database migrations', () => {
let db;
beforeAll(async () => {
db = await connectToTestDB();
await db.migrate();
// Save snapshot of clean state
await db.snapshot('clean');
});
afterEach(async () => {
// Restore to clean snapshot after each test
await db.restore('clean');
});
afterAll(async () => {
await db.close();
});
test('user table exists', async () => {
const exists = await db.tableExists('users');
expect(exists).toBe(true);
});
test('can insert user', async () => {
await db.insert('users', { name: 'Alice' });
expect(await db.count('users')).toBe(1);
});
test('starts with clean state', async () => {
// This test would fail without snapshot restore
expect(await db.count('users')).toBe(0);
});
});
Best Practices
- Keep fixtures small and focused — one fixture per concern
- Use
yieldin pytest for guaranteed teardown — even if the test fails - Prefer composition over inheritance — compose fixtures from smaller fixtures
- Name fixtures descriptively —
service_with_useris clearer thansetup - Use factory functions for test data —
createTestUser(overrides)is flexible - Clean up all resources — database connections, temp files, env vars, network sockets
- Use scoped fixtures wisely —
sessionfor expensive setup,functionfor isolation - Don’t share state between tests — each test should be independent
- For pytest at scale, the Pytest in Production guide covers conftest organization, markers, and CI integration.
Common Mistakes
- Forgetting teardown: leaving temp files, open connections, or modified env vars causes cascading failures. Always pair setup with teardown.
- Over-sharing fixtures: a
session-scoped fixture that modifies state causes test order dependency. Usefunctionscope for mutable state. - Complex fixtures: if a fixture takes 50 lines, it’s doing too much. Split it into smaller composable fixtures.
- Not resetting mocks: mocks retain call history between tests. Always reset in
afterEachor usejest.clearAllMocks(). - Hidden side effects: a fixture that modifies global state (env vars, working directory) without restoring it breaks other tests.
See Also
- pytest fixtures — official reference (pytest 7/8) for fixture scopes,
yieldteardown, andconftest.pysharing. - JUnit 5 lifecycle —
@BeforeEach,@AfterEach,@BeforeAll,@AfterAllsemantics. - Jest setup and teardown —
beforeEach,afterEach, and scopeddescribehooks (Jest 29/30). - Testcontainers — library for disposable database/service containers in tests; handles readiness checks and cleanup for you.
Frequently Asked Questions
What is a test fixture?
A fixture is the reusable setup and teardown logic for tests. It creates the preconditions a test needs (objects, data, connections) and cleans up afterward.
What is the difference between beforeEach and beforeAll?
beforeEach runs before every test — it gives each one isolation. beforeAll runs once before all tests in a block — use for expensive setup that doesn't mutate.
Should I use fixtures or inline setup?
Use fixtures when two or more tests share the same setup. Use inline setup when it's unique to one test. Don't force everything into fixtures.
What is a yield fixture in pytest?
A fixture that uses yield instead of return. Code before yield is setup, code after is teardown. This guarantees cleanup even if the test fails.
How do I share fixtures across test files?
In pytest, put shared fixtures in conftest.py — they're automatically available to all tests in that directory. In Jest, use setupFilesAfterEach in config or import shared setup modules.
Related Resources
Test Double: Replace Dependencies with Stubs, Spies, Fakes
How to use test doubles to isolate units under test. Covers stubs, spies, fakes, mocks, and dummy objects with examples in Python, JavaScript, and Java.
PatternParameterized Test: Run the Same Logic Across Multiple
How to write parameterized tests to verify the same logic across multiple inputs. Covers pytest parametrize, Jest test.each, JUnit ParameterizedTest, and data providers.
PatternSnapshot Testing: Capture and Compare Serialized Output
How to use snapshot testing to detect unintended changes in serialized output. Covers Jest snapshots, pytest-snapshot, syrupy, and inline vs external snapshots.
PatternContract Testing: Verify Consumer-Producer API Contracts
How to use contract testing to verify that API producers and consumers agree on request and response shapes. Covers Pact consumer-driven contracts and provider verification.