Configurar Fixtures de Test
Cómo gestionar fixtures de test con patrones factory, hooks de setup/teardown y datos deterministas para tests unitarios e integración confiables en Python, JavaScript y Java.
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.
Descripción General
Los fixtures de test son los datasets y estados de entorno conocidos y controlados que hacen los tests deterministas. Sin fixtures, los tests dependen de bases de datos externas, sistemas de archivos o estado aleatorio, produciendo fallos flaky que desperdician tiempo de debugging. Esta receta muestra cómo crear, aislar y limpiar fixtures usando patrones factory y hooks nativos del framework.
Cuándo Usar
- Los tests necesitan un usuario de base de datos, archivo u objeto que existe en un estado conocido antes de las aserciones
- Múltiples tests comparten la misma lógica de setup costosa
- Quieres variar datos de entrada sin duplicar boilerplate
- Los tests de integración necesitan servicios temporales, colas o estado de esquema
- Necesitas datos deterministas y repetibles para cada ejecución de test
Cuándo NO Usar
- El objeto bajo test no tiene dependencias externas — instáncialo directamente en el test
- El setup es trivial (una sola línea) — inclúyelo inline para mantener los tests legibles
- Estás tentado a compartir fixtures mutables entre tests sin resetear estado
- El fixture oculta el escenario real del test — prefiere setup explícito y legible sobre 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"
# Fixture simple
@pytest.fixture
def admin_user() -> User:
return User(id=1, name="Alice", email="alice@example.com", role="admin")
# Fixture con teardown (patrón yield)
@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)
# Fixture parametrizado (ejecuta test con múltiples valores)
@pytest.fixture(params=["admin", "editor", "viewer"])
def role(request) -> str:
return request.param
# Fixture factory — crea muchas variantes
@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
# Uso en 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()
# Fixture con scope de sesión (costoso, computar una vez)
@pytest.fixture(scope="session")
def compiled_model():
return load_ml_model("large-model.pkl")
# Fixture autouse (corre para cada test en el módulo)
@pytest.fixture(autouse=True)
def reset_mocks():
yield
mock_registry.clear()
JavaScript (Jest)
// Setup y teardown
let dbConnection;
beforeAll(async () => {
dbConnection = await createTestDatabase();
});
afterAll(async () => {
await dbConnection.destroy();
});
beforeEach(() => {
// Resetear estado antes de cada test
dbConnection.truncateAll();
});
// Función factory
function createUser(overrides = {}) {
return {
id: Math.floor(Math.random() * 100000),
name: 'Test User',
email: 'test@example.com',
role: 'user',
...overrides
};
}
// Patrón de fixture Jest con setupFiles
// jest.setup.js
import { factory } from './factories';
global.factory = factory;
// __tests__/auth.test.js
describe('authentication', () => {
test('admin puede acceder al panel admin', () => {
const admin = factory.user({ role: 'admin' });
expect(canAccessAdmin(admin)).toBe(true);
});
test('viewer no puede acceder al panel admin', () => {
const viewer = factory.user({ role: 'viewer' });
expect(canAccessAdmin(viewer)).toBe(false);
});
});
// Fixture inline para casos simples
describe('cálculos de orden', () => {
const baseOrder = () => ({
items: [],
discountCode: null,
customer: { id: 1, tier: 'standard' }
});
test('aplica descuento', () => {
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");
}
// Método factory
private Order.Builder orderBuilder() {
return Order.builder()
.customerId(1L)
.status(OrderStatus.PENDING);
}
@Test
@DisplayName("Orden válida puede ser colocada")
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());
}
// Datos de fixture parametrizados
@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));
}
}
// Fixtures compartidos vía @TestConfiguration (Spring)
@TestConfiguration
public class TestFixtures {
@Bean
@Primary
public Clock fixedClock() {
return Clock.fixed(
Instant.parse("2024-06-01T10:00:00Z"),
ZoneId.of("UTC")
);
}
}
Mejores Prácticas
- Usa funciones factory, no datos estáticos.
createUser({ role: 'admin' })es más flexible que un objetoadminUserhardcodeado y previene el drift de copy-paste. - Resetea estado entre tests. Los fixtures mutables compartidos causan fallos dependientes del orden. Trunca tablas, limpia mocks y reinicializa objetos en
beforeEach. - Mantén fixtures cerca del test. Un fixture usado por solo una clase de test debería definirse en esa clase, no en un conftest global. La proximidad mejora la legibilidad.
- Nombra fixtures por lo que representan, no por cómo se construyen.
premium_customeres mejor queuser_with_tier_gold_and_100_orders. - Usa IDs deterministas. Los IDs aleatorios dificultan el debugging cuando un test falla solo en ciertos valores. Usa un contador o hash del nombre del test.
Errores Comunes
- Compartir fixtures mutables entre tests. Un test modifica el fixture y el siguiente falla misteriosamente. Siempre devuelve nuevas instancias o resetea en
beforeEach. - Sobreusar fixtures autouse. Los fixtures implícitos que corren para cada test dificultan rastrear por qué un test falla. Prefiere inyección explícita.
- Fixtures que hacen demasiado. Un fixture que crea un usuario, lo loguea y configura 10 órdenes es difícil de reutilizar. Compón fixtures pequeños en su lugar.
- Hardcodear tiempo en tests. Los tests que dependen de
new Date()fallan a medianoche o en diferentes zonas horarias. Usa un fixture de reloj. - No limpiar recursos externos. Los archivos temporales, conexiones de base de datos y stubs de red dejados abiertos filtran recursos y causan fallos en cascada.