beginner Por Mathias Paulenko

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.

Temas: testing

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. La solucion abajo muestra cómo crear, aislar y limpiar fixtures usando patrones factory y hooks nativos del framework.

Cuándo Usar

  • For alternatives, see Pytest Fixtures and Parametrize.

  • 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")
        );
    }
}

Lo que funciona

  • Usa funciones factory, no datos estáticos. createUser({ role: 'admin' }) es más flexible que un objeto adminUser hardcodeado 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_customer es mejor que user_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.

Notas de Producción

  • Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
  • Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
  • Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
  • Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.

Puntos Clave

  • Aplica configurar fixtures de test cuando necesites una solución práctica para tu caso de uso.
  • Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
  • Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
  • Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.

Troubleshooting

  • Flaky tests: isolate shared state, time, and randomness. Make tests independent and deterministic; quarantine persistently flaky tests.
  • High coverage but bugs in production: coverage does not guarantee correctness. Add mutation testing, property-based tests, or contract tests.
  • Slow test suite: parallelize, mock slow dependencies, and avoid end-to-end tests for logic that can be unit tested.
  • Tests pass locally but fail in CI: check environment differences, timezone, locale, and dependency versions. Pin tool versions.
  • Debugging a failing integration test: Reset state before each test.

Errores Comunes en Producción

  • Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
  • Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
  • Codificar valores fijos que deberían ser configurables por entorno.
  • Olvidar agregar logging y monitoreo en cada paso.
  • Desplegar sin plan de rollback ni estrategia de backup probada.
  • Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
  • No documentar la versión y configuración usadas en producción.
  • Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.

Preguntas frecuentes

¿Cuál es la diferencia entre un fixture y un mock?
Un fixture proporciona el entorno o datos de prueba (por ejemplo, una base de datos sembrada). Un mock reemplaza una dependencia real para aislar el código bajo prueba.
¿Cuándo debo limpiar los fixtures después de un test?
Siempre, a menos que compartas intencionalmente un setup costoso dentro de una clase de tests de alcance reducido. La limpieza previene la contaminación entre tests y resultados flaky.
¿Cómo comparto fixtures entre archivos de test?
Usa fixtures a nivel de framework (pytest conftest, Jest setupFiles) o un módulo helper compartido. Mantén los fixtures compartidos mínimos y deterministas.