Configurar Fixtures de Test (pytest, Jest, JUnit)
Cómo crear, acotar y limpiar fixtures de test con pytest, hooks de Jest y el ciclo de vida de JUnit 5 — más conftest.py, factories de fixtures y patrones de teardown.
Descripción General
Un fixture de test es el estado conocido y controlado — datos, objetos, entorno — que un test necesita antes de ejecutarse. Los fixtures hacen los tests deterministas: sin ellos, los tests dependen de bases de datos externas, sistemas de archivos o estado residual, y obtienes fallos intermitentes en lugar de señal.
Esta receta cubre el setup de fixtures en los tres frameworks de test más comunes: fixtures de pytest y conftest.py, hooks beforeEach/beforeAll de Jest y anotaciones de ciclo de vida de JUnit 5. Si buscas el deep dive específico de pytest (fixtures parametrizados, parametrización indirecta), consulta Pytest Fixtures y Parametrize.
Cuándo Usar
- Los tests necesitan una fila de base de datos, archivo u objeto en estado conocido antes de las aserciones
- Varios tests comparten un setup costoso (instancia de app, conexión a BD, archivo fixture parseado)
- Quieres variar las entradas sin duplicar código de setup
- Los tests de integración necesitan servicios temporales, colas o estado de esquema
- Cada ejecución debe ver los mismos datos — el determinismo le gana a la comodidad
Cuándo NO Usar
- El objeto bajo test no tiene dependencias externas — instáncialo inline
- El setup es una sola línea — mantenlo en el test por legibilidad
- Tendrías que compartir estado mutable sin reset — arregla el diseño en su lugar
- El fixture oculta el escenario — el setup explícito le gana a la magia
Implementación Paso a Paso
Python (pytest)
import pytest
from dataclasses import dataclass
from typing import Generator
@dataclass
class User:
id: int
name: str
email: str
role: str = "user"
def can_delete(self) -> bool:
return self.role == "admin"
def can_edit(self) -> bool:
return self.role in ("admin", "editor")
# 1. Fixture simple — objeto nuevo por test
@pytest.fixture
def admin_user() -> User:
return User(id=1, name="Alice", email="alice@example.com", role="admin")
# 2. Fixture con teardown (patrón yield)
@pytest.fixture
def temp_db(tmp_path) -> Generator[str, None, None]:
db_path = str(tmp_path / "test_db.sqlite")
init_schema(db_path)
yield db_path # aquí corre el test
cleanup_schema(db_path) # teardown después del test
# 3. Fixture parametrizado — el test corre una vez por valor
@pytest.fixture(params=["admin", "editor", "viewer"])
def role(request) -> str:
return request.param
# 4. Fixture factory — construye variantes bajo demanda
@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
# 5. Fixture con scope session — setup costoso, se calcula una vez
@pytest.fixture(scope="session")
def compiled_model():
return load_ml_model("large-model.pkl")
def test_admin_can_delete(admin_user: User):
assert admin_user.can_delete() is True
def test_permissions_vary_by_role(user_factory):
admin = user_factory(role="admin")
viewer = user_factory(role="viewer")
assert admin.can_edit()
assert not viewer.can_edit()
Fixtures compartidos con conftest.py
Los fixtures en un archivo conftest.py quedan disponibles automáticamente para todos los tests de ese directorio y sus subdirectorios — sin imports:
# tests/conftest.py
import pytest
from myapp.db import create_test_engine
@pytest.fixture(scope="session")
def db_engine():
engine = create_test_engine("sqlite:///:memory:")
yield engine
engine.dispose()
@pytest.fixture
def db_session(db_engine):
# transacción nueva por test, rollback en teardown
conn = db_engine.connect()
trans = conn.begin()
yield conn
trans.rollback()
conn.close()
# tests/orders/test_orders.py — usa fixtures de conftest sin importarlos
def test_order_persists(db_session):
db_session.execute("INSERT INTO orders VALUES (1, 'SKU-1', 2)")
Para variar entradas, prefiere @pytest.mark.parametrize en el propio test — mantiene los datos junto a la aserción:
@pytest.mark.parametrize("role,expected", [
("admin", True),
("editor", False),
("viewer", False),
])
def test_delete_permission(role, expected):
assert User(id=1, name="x", email="x@x.com", role=role).can_delete() == expected
JavaScript (Jest)
// factories.js — helpers compartidos
let counter = 0;
export function createUser(overrides = {}) {
counter += 1; // IDs deterministas, no Math.random()
return {
id: counter,
name: `user_${counter}`,
email: `user_${counter}@test.com`,
role: 'user',
...overrides
};
}
// __tests__/auth.test.js
import { createUser } from './factories';
let dbConnection;
beforeAll(async () => {
dbConnection = await createTestDatabase();
});
afterAll(async () => {
await dbConnection.destroy();
});
beforeEach(() => {
// resetea el estado mutable antes de cada test
dbConnection.truncateAll();
});
describe('authentication', () => {
test('admin puede acceder al panel admin', () => {
const admin = createUser({ role: 'admin' });
expect(canAccessAdmin(admin)).toBe(true);
});
test('viewer no puede acceder al panel admin', () => {
const viewer = createUser({ role: 'viewer' });
expect(canAccessAdmin(viewer)).toBe(false);
});
});
Java (JUnit 5)
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
@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");
}
// Método factory — builder determinista por test
private Order.Builder orderBuilder() {
return Order.builder()
.customerId(1L)
.status(OrderStatus.PENDING);
}
@Test
@DisplayName("Una orden válida puede colocarse")
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());
}
@ParameterizedTest
@CsvSource({
"admin, true",
"editor, false",
"viewer, false"
})
void onlyAdminCanDelete(String role, boolean expected) {
User user = User.builder().role(role).build();
assertEquals(expected, service.canDelete(user));
}
}
Ciclo de Vida del Fixture de un Vistazo
Los hooks de setup y teardown se corresponden directamente entre frameworks:
| pytest | Jest | JUnit 5 | |
|---|---|---|---|
| Una vez por suite | scope="session" | beforeAll / afterAll | @BeforeAll / @AfterAll |
| Por test | scope="function" (default) | beforeEach / afterEach | @BeforeEach / @AfterEach |
| Archivo compartido | conftest.py | setupFiles / jest.setup.js | @TestConfiguration / clase base |
| Variar entradas | @pytest.mark.parametrize | test.each | @ParameterizedTest |
La elección de scope es la fuente típica de fixtures inestables: un fixture session que devuelve estado mutable filtra cambios entre tests. Mantén los datos mutables en scope function — o haz que el teardown los restaure.
Lo Que Funciona
- Usa funciones factory, no datos estáticos.
createUser({ role: 'admin' })es más flexible que unadminUserhardcodeado y evita el drift de copy-paste. - Resetea el estado entre tests: trunca tablas, limpia mocks, reconstruye objetos en
beforeEach. Los fallos dependientes del orden son el síntoma típico de resets omitidos. - Mantén los fixtures cerca del test. Un fixture que usa una sola clase pertenece a esa clase, no a un
conftest.pycompartido. - Nombra los fixtures por lo que representan, no por cómo se construyen:
premium_customermejor queuser_with_tier_gold_and_100_orders. - Usa IDs deterministas (contadores, secuencias, nombres hasheados): un test que solo falla con ciertos valores es una trampa de depuración.
Errores Comunes
- Compartir fixtures mutables entre tests: un test lo modifica y el siguiente falla misteriosamente. Devuelve instancias nuevas o resetea en
beforeEach. - Abusar de autouse: los fixtures implícitos que corren en cada test ocultan por qué falla. Prefiere la inyección explícita.
- Fixtures que hacen demasiado: un fixture que crea un usuario, lo loguea y siembra 10 órdenes no se puede reutilizar. Compón fixtures pequeños.
- Depender de la hora del sistema: los tests con
new Date()fallan a medianoche o en otras zonas horarias. Inyecta un reloj fijo. - Filtrar recursos externos: archivos temporales, conexiones a BD y stubs abiertos causan fallos en cascada en tests sin relación.
Ver También
- Documentación de fixtures de pytest: scopes, parametrización, conftest.
- Setup y teardown de Jest:
beforeAll,beforeEachy sus reglas de scope. - Guía de usuario de JUnit 5: anotaciones de ciclo de vida y tests parametrizados.
- Generar datos de test: Faker y factories para los datos dentro de los fixtures.
Código complementario: los setups de fixtures ejecutables para los tres frameworks están en el repo complementario.
Preguntas frecuentes
¿Cuál es la diferencia entre un fixture y un mock?
Un fixture provee el entorno o los datos que el test necesita: una base de datos sembrada, un archivo temporal, un objeto conocido. Un mock reemplaza una dependencia real para aislar el código bajo test. Normalmente necesitas ambos: el fixture para el estado, el mock para el comportamiento.
¿Qué son los scopes de fixture en pytest?
function (default, nuevo por test), class, module, package y session (una vez por ejecución). Usa scopes más amplios solo para setup costoso e inmutable: un fixture session con estado mutable filtra cambios entre tests.
¿Qué es conftest.py en pytest?
Un archivo especial cuyos fixtures son autodescubiertos por todos los tests de su directorio y subdirectorios. Pon ahí los fixtures realmente compartidos (engine de BD, instancia de app) y mantén los de un solo test junto a su test.
¿Cuándo debería limpiar los fixtures después de un test?
Siempre, salvo que compartas deliberadamente un setup costoso en un scope estrecho. La limpieza previene la contaminación entre tests — la fuente clásica de bugs tipo "pasa solo, falla en suite".
¿Cómo comparto fixtures entre archivos de test?
pytest: conftest.py. Jest: setupFiles o un módulo de helpers compartido. JUnit: una clase base de test o @TestConfiguration. Mantén los fixtures compartidos mínimos y deterministas.
¿Cuál es la diferencia entre params de fixture y parametrize?
@pytest.fixture(params=[...]) corre cada test que usa ese fixture una vez por valor — útil para roles o configs. @pytest.mark.parametrize en el test varía entradas sin tocar el fixture — suele ser la opción más clara para tablas de entrada/salida.
¿Pueden los fixtures depender de otros fixtures?
Sí — un fixture puede pedir otro por nombre y pytest construye el grafo (db_session dependiendo de db_engine arriba). El teardown corre en orden inverso automáticamente.
Recursos Relacionados
Generar Datos de Test (Faker + Factories)
Cómo generar datos de test realistas y deterministas con Faker, factory-boy y generadores type-aware para suites de test confiables en Python, JavaScript y Java.
RecipePytest Fixtures y Parametrize
Cómo usar pytest fixtures y @pytest.mark.parametrize para escribir tests data-driven con lógica de setup reutilizable en proyectos Python.
RecipeStubear APIs HTTP Externos con WireMock en Java
Usá WireMock en tests de Java para stubear servicios HTTP externos. Cubre templating, simulación de delays, stubs stateful y verificación de requests.
RecipeImplementar Property-Based Testing
Cómo escribir tests property-based con Hypothesis, fast-check y jqwik que generan miles de entradas y encuentran casos límite.
RecipeEscribir Unit Tests con Mocks y Stubs
Cómo aislar código bajo test usando objetos mock, stubs y spies para reemplazar dependencias externas como bases de datos, APIs y sistemas de archivos.
RecipeImplementar Mutation Testing
Cómo usar mutation testing con mutmut, Stryker y PIT para evaluar si tus tests realmente verifican el comportamiento o solo ejecutan código.