Fixture Setup/Teardown: contexto de test reutilizable
Cómo usar fixtures de setup y teardown para crear contexto de test reutilizable. Cubre beforeEach, funciones fábrica, objetos fixture y limpieza con ejemplos.
Resumen
El patrón fixture setup/teardown define un ciclo de vida para el contexto de cada test: construir lo que el test necesita, ejecutarlo y limpiar después — siempre, falle o no. En vez de duplicar código de inicialización en cada test, escribís el setup una sola vez en un fixture que corre antes de cada test (o suite) y un teardown que corre después. El beneficio es menos código repetido, un estado inicial consistente y nada de conexiones de base de datos, archivos temporales o sockets filtrados al siguiente test.
Cuándo usarlo
- Tests que comparten la misma inicialización (conexiones de base de datos, datos de prueba, mocks)
- Tests que deben liberar recursos (archivos temporales, conexiones de red, variables de entorno)
- Cuando el setup es complejo o propenso a errores — centralizalo en un fixture
- Cuando los tests deben correr aislados — los fixtures garantizan estado limpio entre tests
- Equipos que quieren código de test DRY — evitá copiar setup en decenas de tests
Cuándo no usarlo
- Tests triviales sin setup compartido — el setup inline es más simple
- Cuando el setup es único para cada test — un fixture agrega indirección sin beneficio
- Para scripts de un solo uso — los fixtures son para suites estructuradas
- Cuando el fixture es más complejo que los tests a los que sirve — simplificá
Explicación
El ciclo de vida de un fixture
Todo fixture corre el mismo ciclo de tres fases, una vez por límite de scope:
La garantía que importa está del lado derecho del diagrama: el teardown corre incluso cuando el test falla. Eso es lo que evita que un test roto filtre estado al resto de la suite. Lo que no cubre es un fallo en el propio setup: si el código anterior al yield (o dentro de beforeEach) lanza una excepción, la parte de teardown nunca se ejecuta — todo lo reservado antes de ese punto hay que envolverlo en try/finally o registrarlo con request.addfinalizer() en pytest.
Elegir el scope
Cada cuánto corre un fixture define tanto su coste como su seguridad. El estado mutable pertenece al scope function; el contexto costoso y de solo lectura se puede compartir más amplio.
| Scope | Corre | Sirve para | Riesgo si se abusa |
|---|---|---|---|
function (default) | Una vez por test | Estado mutable, filas de DB, archivos temporales | Más lento si el setup es caro |
class / module | Una vez por clase o archivo | Fixtures compartidos de solo lectura, config | Las mutaciones se filtran entre tests del mismo scope |
session | Una vez por suite | Contenedores Docker, arranque de app, carga de config | Tests dependientes del orden; fugas invisibles en CI |
En Jest el mismo trade-off se mapea a hooks: beforeEach/afterEach equivale a scope function, beforeAll/afterAll a scope module aproximadamente. JUnit lo refleja con @BeforeEach/@AfterEach (por test) y @BeforeAll/@AfterAll (una vez por clase).
Semántica del teardown por framework
- pytest: el código después del
yieldcorre aunque el test falle. Si el setup lanza una excepción antes delyield, lo que sigue nunca corre — usárequest.addfinalizer()o untry/finallyalrededor de la parte riesgosa. - Jest:
afterEachcorre tras el cuerpo del test aunque falle, y también corre sibeforeEachlanza un error.afterAllcorre igual aunque haya fallado algún test del bloque. - JUnit 5:
@AfterEachse ejecuta incluso si el test o un@BeforeEachfalla.@AfterAllcorre cuando la clase termina, sin importar los resultados individuales.
Fixtures anidados y compuestos
Los fixtures se componen como una pila: el setup se resuelve de afuera hacia adentro, el teardown se desenrolla de adentro hacia afuera. En el ejemplo de scopes, test_database (module) se crea antes que clean_db (function), y el teardown de clean_db corre antes de que se desmonte el fixture de módulo. En Jest, los describe anidados funcionan igual — los beforeEach internos corren después de los externos, los afterEach internos antes que los externos.
Ese orden es lo que hace segura la composición: un fixture barato de scope function puede mutar uno costoso compartido y resetearlo después, como hace clean_db con truncate_all().
Solución
Versiones ejecutables de cada ejemplo en el repositorio complementario.
Setup/teardown básico en Jest
// JavaScript — setup/teardown en Jest
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():
"""Repositorio en memoria nuevo para cada test."""
return FakeUserRepository()
@pytest.fixture
def service(repo):
"""UserService con un repositorio limpio."""
return UserService(repo)
@pytest.fixture
def service_with_user(service):
"""UserService con un usuario ya 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 — fixture de pytest con yield para el teardown
import pytest
import tempfile
import os
@pytest.fixture
def temp_db():
"""Crea una base SQLite temporal, la cede al test y limpia después."""
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):
"""Base de datos con datos semilla."""
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 — setup/teardown en JUnit 5
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 de la clase
System.setProperty("TEST_MODE", "true");
}
@AfterAll
static void tearDownAll() {
// Corre una vez después de todos los tests de 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 función fábrica
// JavaScript — función fábrica para datos de prueba
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% de descuento
});
});
Patrón objeto fixture
Cuando un test necesita más de una dependencia, agrupalas en un solo objeto. El notifier del ejemplo sustituye a un servicio real — mirá el patrón Test Double para decidir cuándo usar un mock y cuándo un fake.
# Python — objeto fixture que agrupa varias dependencias
@pytest.fixture
def test_context():
"""Agrupa 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")
Fixtures con scope
# Python — scopes de fixtures en pytest
@pytest.fixture(scope="session")
def test_config():
"""Se carga una vez por sesión de tests."""
return load_config("test.yaml")
@pytest.fixture(scope="module")
def test_database():
"""Una base de datos por módulo de tests."""
db = TestDatabase()
db.connect()
yield db
db.disconnect()
db.drop_all_tables()
@pytest.fixture(scope="function")
def clean_db(test_database):
"""Estado limpio por función de test."""
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 # El fixture truncó las tablas
Setup/teardown de variables de entorno
# Python — manejar variables de entorno en tests
import os
import pytest
@pytest.fixture
def env_vars():
"""Establece y restaura variables de entorno."""
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
# Restaura el entorno 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:'
Fixtures anidados en Jest
// JavaScript — bloques describe anidados con setup por alcance
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);
});
});
});
Variantes
Fixtures parametrizados
Un fixture con params vuelve a correr cada test una vez por valor — la versión a nivel fixture del patrón Test Parametrizado.
# Python — fixture parametrizado para probar varias configuraciones
@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 una base de datos de prueba en Docker
import pytest
import subprocess
import time
@pytest.fixture(scope="session")
def postgres_container():
"""Levanta un contenedor PostgreSQL para la sesión de tests."""
container_id = subprocess.check_output([
"docker", "run", "-d", "-p", "5432:5432",
"-e", "POSTGRES_PASSWORD=test",
"postgres:16-alpine"
]).decode().strip()
# Espera activa hasta que PostgreSQL acepte conexiones — sleep() a secas es inestable en 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("el contenedor postgres no quedó listo")
yield {
"host": "localhost",
"port": "5432",
"password": "test",
}
# Teardown: detiene y borra el contenedor
subprocess.run(["docker", "stop", container_id])
subprocess.run(["docker", "rm", container_id])
Setup basado en snapshots
// JavaScript — guardar y restaurar estado entre tests
describe('Database migrations', () => {
let db;
beforeAll(async () => {
db = await connectToTestDB();
await db.migrate();
// Guarda un snapshot del estado limpio
await db.snapshot('clean');
});
afterEach(async () => {
// Restaura el snapshot limpio 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 la restauración del snapshot
expect(await db.count('users')).toBe(0);
});
});
Buenas prácticas
- Mantené los fixtures chicos y enfocados — un fixture por responsabilidad
- Usá
yielden pytest para teardown garantizado — incluso si el test falla - Preferí composición sobre herencia — componé fixtures a partir de fixtures más chicos
- Nombrá los fixtures descriptivamente —
service_with_useres más claro quesetup - Usá funciones fábrica para datos de prueba —
createTestUser(overrides)es flexible - Limpiá todos los recursos — conexiones de base de datos, archivos temporales, variables de entorno, sockets
- Elegí el scope con criterio —
sessionpara setup costoso,functionpara aislamiento - No compartas estado entre tests — cada test debería ser independiente
- Para pytest a escala, la guía de Pytest en producción cubre organización de conftest, markers e integración con CI.
Errores comunes
- Olvidar el teardown: dejar archivos temporales, conexiones abiertas o variables de entorno modificadas provoca fallos en cadena. Siempre pareá setup con teardown.
- Compartir fixtures de más: un fixture con scope
sessionque muta estado genera dependencia de orden entre tests. Usá scopefunctionpara estado mutable. - Fixtures complejos: si un fixture ocupa 50 líneas, está haciendo demasiado. Dividilo en fixtures más chicos y componibles.
- No resetear mocks: los mocks conservan el historial de llamadas entre tests. Siempre resetealos en
afterEacho usájest.clearAllMocks(). - Efectos secundarios ocultos: un fixture que modifica estado global (variables de entorno, directorio de trabajo) sin restaurarlo rompe otros tests.
Lectura adicional
- Fixtures de pytest — referencia oficial (pytest 7/8) sobre scopes, teardown con
yieldy compartición víaconftest.py. - Ciclo de vida de JUnit 5 — semántica de
@BeforeEach,@AfterEach,@BeforeAlly@AfterAll. - Setup y teardown en Jest —
beforeEach,afterEachy hooks por bloquesdescribe(Jest 29/30). - Testcontainers — librería para contenedores de base de datos/servicios descartables en tests; maneja el readiness check y la limpieza por vos.
Preguntas frecuentes
¿Qué es un fixture de test?
Un fixture es la lógica reutilizable de setup y teardown para tests. Crea las precondiciones que un test necesita (objetos, datos, conexiones) y limpia después.
¿Cuál es la diferencia entre beforeEach y beforeAll?
beforeEach corre antes de cada test — garantiza aislamiento. beforeAll corre una vez antes de todos los tests del bloque — usalo para setup costoso que no muta.
¿Debería usar fixtures o setup inline?
Usá fixtures cuando el setup se comparte entre varios tests. Usá setup inline cuando es único de un test. No fuerces todo en fixtures.
¿Qué es un fixture con yield en pytest?
Un fixture que usa yield en vez de return. El código antes del yield es setup, el de después es teardown. Así la limpieza está garantizada aunque el test falle.
¿Cómo comparto fixtures entre archivos de test?
En pytest, poné los fixtures compartidos en conftest.py — quedan disponibles automáticamente para todos los tests de ese directorio. En Jest, usá setupFilesAfterEach en la config o importá módulos de setup compartidos.
Recursos Relacionados
Patrón Test Double: Reemplazar Dependencias con Stubs,
Cómo usar test doubles para aislar unidades bajo test. Cubre stubs, spies, fakes, mocks y dummy objects con ejemplos en Python, JavaScript y Java.
PatternPatrón Parameterized Test
Cómo escribir parameterized tests para verificar la misma lógica across múltiples inputs. Cubre pytest parametrize, Jest test.each, JUnit ParameterizedTest y data providers.
PatternSnapshot Testing: Captura y Compara la Salida Serializada
Cómo usar el snapshot testing para detectar cambios no deseados en la salida serializada. Cubre snapshots de Jest, pytest-snapshot, syrupy y snapshots inline vs externos.
PatternPatrón Contract Testing
Cómo usar contract testing para verificar que producers y consumers de API acuerdan en shapes de request y response. Cubre Pact consumer-driven contracts y provider verification.