StackPractices
beginner Por Mathias Paulenko

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.

Temas: testing

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

flowchart diagram: Arranca la suite

Los hooks de setup y teardown se corresponden directamente entre frameworks:

pytestJestJUnit 5
Una vez por suitescope="session"beforeAll / afterAll@BeforeAll / @AfterAll
Por testscope="function" (default)beforeEach / afterEach@BeforeEach / @AfterEach
Archivo compartidoconftest.pysetupFiles / jest.setup.js@TestConfiguration / clase base
Variar entradas@pytest.mark.parametrizetest.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 un adminUser hardcodeado 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.py compartido.
  • Nombra los fixtures por lo que representan, no por cómo se construyen: premium_customer mejor que user_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

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.