Setup Test Fixtures
How to manage test fixtures with factory patterns, setup/teardown hooks, and deterministic data for reliable unit and integration tests across Python, JavaScript, and Java.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
Test fixtures are the known, controlled datasets and environment state that make tests deterministic. Without fixtures, tests depend on external databases, filesystems, or random state, producing flaky failures that waste debugging time. This recipe shows how to create, isolate, and clean up fixtures using factory patterns and framework-native hooks.
When to Use
- Tests need a database user, file, or object that exists in a known state before assertions
- Multiple tests share the same expensive setup logic
- You want to vary input data without duplicating boilerplate
- Integration tests need temporary services, queues, or schema state
- You need deterministic, repeatable data for every test run
When NOT to Use
- The object under test has no external dependencies — instantiate directly in the test
- Setup is trivial (a single line) — inline it to keep tests readable
- You are tempted to share mutable fixtures between tests without resetting state
- The fixture hides the actual test scenario — prefer readable, explicit setup over magic
Step-by-Step Implementation
Python (pytest)
import pytest
from dataclasses import dataclass
from typing import Generator
@dataclass
class User:
id: int
name: str
email: str
role: str = "user"
# Simple fixture
@pytest.fixture
def admin_user() -> User:
return User(id=1, name="Alice", email="alice@example.com", role="admin")
# Fixture with teardown (yield pattern)
@pytest.fixture
def temp_database() -> Generator[str, None, None]:
db_path = "/tmp/test_db.sqlite"
init_schema(db_path)
yield db_path
cleanup_schema(db_path)
# Parametrized fixture (runs test with multiple values)
@pytest.fixture(params=["admin", "editor", "viewer"])
def role(request) -> str:
return request.param
# Factory fixture — creates many variants
@pytest.fixture
def user_factory():
_counter = 0
def make(name=None, role="user"):
nonlocal _counter
_counter += 1
return User(
id=_counter,
name=name or f"user_{_counter}",
email=f"user_{_counter}@test.com",
role=role
)
return make
# Usage in tests
def test_admin_can_delete(admin_user: User):
assert admin_user.can_delete() is True
def test_user_permissions(user_factory):
admin = user_factory(role="admin")
viewer = user_factory(role="viewer")
assert admin.can_edit()
assert not viewer.can_edit()
# Session-scoped fixture (expensive, compute once)
@pytest.fixture(scope="session")
def compiled_model():
return load_ml_model("large-model.pkl")
# Autouse fixture (runs for every test in module)
@pytest.fixture(autouse=True)
def reset_mocks():
yield
mock_registry.clear()
JavaScript (Jest)
// Setup and teardown
let dbConnection;
beforeAll(async () => {
dbConnection = await createTestDatabase();
});
afterAll(async () => {
await dbConnection.destroy();
});
beforeEach(() => {
// Reset state before every test
dbConnection.truncateAll();
});
// Factory function
function createUser(overrides = {}) {
return {
id: Math.floor(Math.random() * 100000),
name: 'Test User',
email: 'test@example.com',
role: 'user',
...overrides
};
}
// Jest fixture pattern with setupFiles
// jest.setup.js
import { factory } from './factories';
global.factory = factory;
// __tests__/auth.test.js
describe('authentication', () => {
test('admin can access admin panel', () => {
const admin = factory.user({ role: 'admin' });
expect(canAccessAdmin(admin)).toBe(true);
});
test('viewer cannot access admin panel', () => {
const viewer = factory.user({ role: 'viewer' });
expect(canAccessAdmin(viewer)).toBe(false);
});
});
// Inline fixture for simple cases
describe('order calculations', () => {
const baseOrder = () => ({
items: [],
discountCode: null,
customer: { id: 1, tier: 'standard' }
});
test('applies discount', () => {
const order = { ...baseOrder(), discountCode: 'SAVE20' };
expect(calculateTotal(order)).toBe(80);
});
});
Java (JUnit 5)
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class OrderServiceTest {
private DatabaseConnection db;
private OrderService service;
@BeforeAll
void init() {
db = DatabaseConnection.forTest("jdbc:h2:mem:test");
service = new OrderService(db);
}
@AfterAll
void cleanup() {
db.close();
}
@BeforeEach
void reset() {
db.truncateTables("orders", "order_items");
}
// Factory method
private Order.Builder orderBuilder() {
return Order.builder()
.customerId(1L)
.status(OrderStatus.PENDING);
}
@Test
@DisplayName("Valid order can be placed")
void placeValidOrder() {
Order order = orderBuilder()
.addItem(Item.of("SKU-001", 2, BigDecimal.valueOf(29.99)))
.build();
OrderResult result = service.place(order);
assertTrue(result.isSuccess());
assertEquals(OrderStatus.CONFIRMED, result.getOrder().getStatus());
}
// Parametrized fixture data
@ParameterizedTest
@CsvSource({
"admin, true",
"editor, false",
"viewer, false"
})
void adminCanDelete(String role, boolean expected) {
User user = User.builder().role(role).build();
assertEquals(expected, service.canDelete(user));
}
}
// Shared fixtures via @TestConfiguration (Spring)
@TestConfiguration
public class TestFixtures {
@Bean
@Primary
public Clock fixedClock() {
return Clock.fixed(
Instant.parse("2024-06-01T10:00:00Z"),
ZoneId.of("UTC")
);
}
}
Best Practices
- Use factory functions, not static data.
createUser({ role: 'admin' })is more flexible than a hardcodedadminUserobject and prevents copy-paste drift. - Reset state between tests. Shared mutable fixtures cause order-dependent failures. Truncate tables, clear mocks, and reinitialize objects in
beforeEach. - Keep fixtures close to the test. A fixture used by only one test class should be defined in that class, not in a global conftest. Proximity improves readability.
- Name fixtures after what they represent, not how they are built.
premium_customeris better thanuser_with_tier_gold_and_100_orders. - Use deterministic IDs. Random IDs make debugging harder when a test fails only on certain values. Use a counter or hash of test name.
Common Mistakes
- Sharing mutable fixtures across tests. One test modifies the fixture and the next test fails mysteriously. Always return new instances or reset in
beforeEach. - Overusing autouse fixtures. Implicit fixtures that run for every test make it hard to trace why a test fails. Prefer explicit injection.
- Fixtures that do too much. A fixture that creates a user, logs them in, and sets up 10 orders is hard to reuse. Compose small fixtures instead.
- Hardcoding time in tests. Tests that depend on
new Date()fail at midnight or in different time zones. Use a clock fixture. - Not cleaning up external resources. Temporary files, database connections, and network stubs left open leak resources and cause cascading failures.