Patrón Fixture Setup/Teardown
Cómo usar setup y teardown fixtures para crear contexto de test reutilizable. Cubre beforeEach, factory functions, fixture objects y cleanup con ejemplos.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Overview
El patrón fixture setup/teardown estandariza cómo se crea y limpia el contexto de test. En vez de duplicar código de inicialización across tests, los fixtures proveen lógica de setup reutilizable que corre antes de cada test (o test suite) y lógica de teardown que corre después. Esto reduce duplicación de test, asegura starting state consistente, y garantiza cleanup de resources como database connections, temp files, o network sockets.
When to Use
- Tests que comparten inicialización común (database connections, test data, mock objects)
- Tests que requieren cleanup de resources (temp files, network connections, environment variables)
- Cuando el test setup es complejo o error-prone — centralizalo en un fixture
- Cuando los tests deben correr en aislamiento — los fixtures aseguran clean state entre tests
- Equipos que quieren DRY test code — evitar copy-pasting de setup across dozens de tests
When NOT to Use
- Tests triviales sin shared setup — inline setup es más simple
- Cuando el setup es unique per test — un fixture agrega indirección sin beneficio
- Para scripts one-off — los fixtures son para structured test suites
- Cuando el fixture es más complejo que los tests que sirve — simplificá
Solution
Setup/teardown básico en Jest
// JavaScript — Jest setup/teardown
describe('UserService', () => {
let service;
let dbConnection;
beforeEach(() => {
// Setup: corre antes de cada test
dbConnection = createInMemoryDB();
service = new UserService(dbConnection);
service.createUser('Alice', 'alice@x.com');
});
afterEach(() => {
// Teardown: corre después de cada 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 en pytest
# Python — pytest con fixtures
import pytest
from myapp.services import UserService
from myapp.repositories import FakeUserRepository
@pytest.fixture
def repo():
"""Fresh in-memory repository para cada test."""
return FakeUserRepository()
@pytest.fixture
def service(repo):
"""UserService con un fresh repository."""
return UserService(repo)
@pytest.fixture
def service_with_user(service):
"""UserService con un user pre-creado."""
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 con yield (pytest)
# Python — pytest fixture con yield para teardown
import pytest
import tempfile
import os
@pytest.fixture
def temp_db():
"""Crear temp SQLite database, yield, después limpiar."""
fd, path = tempfile.mkstemp(suffix='.db')
os.close(fd)
db = SQLiteDB(path)
db.initialize_schema()
yield db # El test corre aquí
# Teardown: corre después del test
db.close()
os.unlink(path)
@pytest.fixture
def populated_db(temp_db):
"""Database con 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 en JUnit
// Java — JUnit 5 setup/teardown
import org.junit.jupiter.api.*;
class UserServiceTest {
private UserService service;
private FakeUserRepository repo;
@BeforeEach
void setUp() {
// Corre antes de cada test
repo = new FakeUserRepository();
service = new UserService(repo);
service.createUser("Alice", "alice@x.com");
}
@AfterEach
void tearDown() {
// Corre después de cada test
repo.clear();
service = null;
}
@BeforeAll
static void setUpAll() {
// Corre una vez antes de todos los tests en la clase
System.setProperty("TEST_MODE", "true");
}
@AfterAll
static void tearDownAll() {
// Corre una vez después de todos los tests en la clase
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");
});
}
}
Patrón factory function
// JavaScript — factory function para 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
});
});
Patrón fixture object
# Python — fixture object bundleando múltiples fixtures
@pytest.fixture
def test_context():
"""Bundlear todas las dependencias de test en un objeto."""
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():
"""Cargado una vez por test session."""
return load_config("test.yaml")
@pytest.fixture(scope="module")
def test_database():
"""Una database por 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 por 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 por fixture
Setup/teardown de environment variables
# Python — manejar environment variables en tests
import os
import pytest
@pytest.fixture
def env_vars():
"""Setear y restaurar 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
# Restaurar environment original
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 en Jest
// JavaScript — nested describe blocks con 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
# Python — parametrized fixture para testear múltiples 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
Fixtures de recursos externos
# Python — fixture para test database basada en Docker
import pytest
import subprocess
import time
@pytest.fixture(scope="session")
def postgres_container():
"""Iniciar un PostgreSQL Docker container para la test session."""
container_id = subprocess.check_output([
"docker", "run", "-d", "-p", "5432:5432",
"-e", "POSTGRES_PASSWORD=test",
"postgres:16-alpine"
]).decode().strip()
# Esperar a que PostgreSQL esté ready
time.sleep(3)
yield {
"host": "localhost",
"port": "5432",
"password": "test",
}
# Teardown: stop y remove container
subprocess.run(["docker", "stop", container_id])
subprocess.run(["docker", "rm", container_id])
Setup basado en snapshots
// JavaScript — save y restore state entre tests
describe('Database migrations', () => {
let db;
beforeAll(async () => {
db = await connectToTestDB();
await db.migrate();
// Guardar snapshot de clean state
await db.snapshot('clean');
});
afterEach(async () => {
// Restaurar a clean snapshot después de cada 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 () => {
// Este test fallaría sin snapshot restore
expect(await db.count('users')).toBe(0);
});
});
Best Practices
-
For a deeper guide, see Pytest in Production Guide.
-
Mantené los fixtures pequeños y enfocados — un fixture por concern
-
Usá
yielden pytest para teardown garantizado — incluso si el test falla -
Preferí composition sobre inheritance — componé fixtures desde fixtures más pequeños
-
Nombrá los fixtures descriptivamente —
service_with_useres más claro quesetup -
Usá factory functions para test data —
createTestUser(overrides)es flexible -
Limpiá todos los resources — database connections, temp files, env vars, network sockets
-
Usá scoped fixtures sabiamente —
sessionpara setup expensive,functionpara isolation -
No compartas state entre tests — cada test debería ser independiente
Common Mistakes
- Olvidar teardown: dejar temp files, open connections, o env vars modificadas causa cascading failures. Siempre pareá setup con teardown.
- Over-sharing fixtures: un
session-scoped fixture que modifica state causa test order dependency. Usáfunctionscope para mutable state. - Fixtures complejos: si un fixture toma 50 líneas, está haciendo demasiado. Splittealo en fixtures más pequeños composables.
- No resetear mocks: los mocks retienen call history entre tests. Siempre reseteá en
afterEacho usájest.clearAllMocks(). - Side effects ocultos: un fixture que modifica global state (env vars, working directory) sin restaurarlo rompe otros tests.
FAQ
¿Qué es un test fixture?
Un fixture es la lógica reusable de setup y teardown para tests. Crea las precondiciones que un test necesita (objetos, data, connections) y limpia después.
¿Cuál es la diferencia entre beforeEach y beforeAll?
beforeEach corre antes de cada test — asegura aislamiento. beforeAll corre una vez antes de todos los tests en un block — usalo para setup expensive que no muta.
¿Debería usar fixtures o inline setup?
Usá fixtures cuando el setup se comparte across múltiples tests. Usá inline setup cuando es unique a un test. No fuerces todo en fixtures.
¿Qué es un yield fixture en pytest?
Un fixture que usa yield en vez de return. El código antes de yield es setup, el código después es teardown. Esto garantiza cleanup incluso si el test falla.
¿Cómo comparto fixtures across test files?
En pytest, poné shared fixtures en conftest.py — están automáticamente disponibles para todos los tests en ese directorio. En Jest, usá setupFilesAfterEach en config o importá shared setup modules.
Recursos Relacionados
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, and inline vs external snapshots.