Módulo 7: Datos y aislamiento en integración

6. Sembrar datos de integración

Descripción

Hasta aquí cada test partía de una base vacía, y eso era medio cuento: en la vida real, muchas integraciones necesitan partir de un estado poblado. "Dado que la sala Focus ya tiene dos reservas, cuando consulto find_by_room('focus'), entonces me devuelve esas dos." Ese "dado que ya hay dos reservas" es el estado inicial, y ponerlo antes del test se llama sembrar (en inglés, seed). Sembrar bien es lo que separa una suite de integración legible de una adivinanza. Y el principio es uno solo, pero cuesta respetarlo: el estado inicial de cada test debe ser explícito, mínimo y dicho en voz alta dentro del test. Explícito: no heredado de otro test ni de un archivo misterioso, sino escrito donde puedas verlo. Mínimo: solo las reservas que el test de verdad necesita, ni una más. Y dicho en voz alta: incluso cuando el estado inicial es "nada", el test debería decir "empiezo vacío", para que quien lo lee no tenga que adivinar de qué parte.

La razón de fondo es la que ya conoces del aislamiento, vista desde el otro lado. Un test que asume un estado que no sembró está confiando en que alguien más lo dejó —el vecino, una fixture de scope amplio, la corrida anterior—, y esa confianza es justo la contaminación de la lección 2. Sembrar el estado inicial dentro del test (o en una fixture que corre por test) rompe esa dependencia: el test trae su propio mundo, no lo hereda. Por eso sembrar y aislar son dos caras de lo mismo: el aislamiento garantiza que el test empieza en un estado limpio y conocido; el sembrado decide cuál es ese estado conocido. Vas a aprender a sembrar de forma que el test se lea solo, a decir el seed vacío en voz alta, y a ver con salida real una suite donde cada test declara su propio punto de partida.

Conexión con el módulo: las lecciones 3 a 5 te dieron el aislamiento del recurso —la base limpia por test—; esta pone los datos justos adentro de esa base limpia. Es el paso que convierte "cada test empieza vacío" en "cada test empieza en el estado que necesita, y lo dice". La lección 7 usará estos datos sembrados para demostrar que la suite es independiente y repetible en cualquier orden. Y aquí está la frontera más importante del módulo: cuando los datos de prueba se vuelven complejos —muchos campos, muchas variantes, valores por defecto sensatos con sobrescrituras puntuales—, el patrón que lo resuelve con elegancia es el Builder, y ese es el tema del módulo 7 de test-doubles-and-test-data-guide, no de este. Aquí sembramos mínimo y explícito, con funciones simples; cuando el sembrado pida una fábrica de verdad, te enlazamos allá. El foco de esta guía es el aislamiento del recurso real, no la fábrica de datos.

Analogía: el montaje de la escena antes de rodar

Piensa en cómo se filma una escena de cine. Antes de que el director diga "acción", el equipo de arte monta la escena: pone la taza de café medio llena sobre la mesa, el periódico abierto en la página exacta, la silla ligeramente corrida. Ese montaje es deliberado y mínimo: solo lo que la escena necesita para contar lo que quiere contar. No dejan sobre la mesa los restos del rodaje de ayer —eso saldría en cuadro y confundiría—, ni llenan la mesa de objetos que no importan. Cada elemento que ponen está ahí por una razón, y esa razón es visible: si en la escena el personaje va a leer una noticia, el periódico está abierto en esa noticia, a la vista de todos en el set.

Sembrar datos de integración es montar la escena antes de rodar el test. Pones sobre la base las reservas que el test necesita —las dos de Focus si el test verifica que find_by_room devuelve dos—, mínimas y deliberadas, y las pones a la vista, dentro del test o en una fixture clara, no heredadas del rodaje anterior. Un test que arranca sobre datos que no sembró es como una escena filmada sobre el set sucio del rodaje de ayer: lo que sale en cuadro puede ser cualquier cosa, y nadie sabe por qué. Un test bien sembrado es una escena montada con intención: cada dato inicial está ahí por una razón que puedes leer. Y cuando la escena no necesita nada sobre la mesa, el montador lo confirma —"mesa vacía"—, no la deja al azar.

Sembrar explícito y mínimo

Escribamos una suite donde cada test declara su estado inicial. Usamos la fixture de base fresca de la lección 4 y una función seed simple que guarda las reservas que le pasemos.

# tests/test_seeding.py — sembrar un estado CONOCIDO, minimo y explicito, por test
import sqlite3
import pytest
from datetime import datetime
from reservo.models import Booking
from reservo.sqlite_repo import SqliteBookingRepository

def booking(bid, room_id, hour):
    return Booking(id=bid, room_id=room_id, member_id="m-ana",
                   start=datetime(2026, 3, 10, hour),
                   end=datetime(2026, 3, 10, hour + 3),
                   status="confirmed", price_cents=6000)

def seed(repo, *bookings):
    for b in bookings:
        repo.save(b)
    return repo

@pytest.fixture
def repo():
    conn = sqlite3.connect(":memory:")     # base fresca por test (leccion 4)
    yield SqliteBookingRepository(conn)
    conn.close()

def test_find_by_room_returns_only_focus(repo):
    # Estado inicial explicito: dos en focus, una en studio.
    seed(repo,
         booking("bk-1", "focus", 9),
         booking("bk-2", "focus", 12),
         booking("bk-3", "studio", 9))
    focus = repo.find_by_room("focus")
    assert len(focus) == 2
    assert {b.id for b in focus} == {"bk-1", "bk-2"}

def test_empty_room_returns_nothing(repo):
    # El seed minimo para ESTE test es: nada. Y se dice en voz alta.
    seed(repo)                       # sin filas
    assert repo.find_by_room("boardroom") == []

Mira los dos tests. El primero monta su escena: siembra tres reservas —dos de Focus, una de Studio— y luego verifica que find_by_room("focus") devuelve exactamente las dos de Focus, por id. Cualquiera que lea el test ve, en sus propias líneas, de qué estado parte: no hay que buscar en una fixture lejana ni adivinar qué dejó otro test. La reserva de Studio está sembrada a propósito, para probar que el filtro no la incluye —es el objeto que no debe salir en cuadro—. El segundo test siembra nada, y lo dice: seed(repo) con la lista vacía y un comentario que declara "el seed mínimo para este test es: nada". Podría haber omitido la línea —la base ya está vacía por la fixture—, pero escribirla hace explícito que el estado inicial vacío es una decisión, no un descuido.

Qué esperar. En mi máquina (Python 3.14.0, pytest 9.1.1):

python3 -m pytest tests/test_seeding.py -v
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
collected 2 items

tests/test_seeding.py::test_find_by_room_returns_only_focus PASSED         [ 50%]
tests/test_seeding.py::test_empty_room_returns_nothing PASSED              [100%]

============================== 2 passed in 0.01s ===============================

Los dos verdes, y —esto es lo importante— cada uno legible por sí solo: si mañana test_find_by_room_returns_only_focus fallara, no tendrías que reconstruir de dónde salieron sus datos; están ahí, en el seed de sus tres líneas. Esa legibilidad no es un lujo: es lo que hace que un fallo de integración sea diagnosticable en vez de un misterio arqueológico.

Por qué el seed va cerca del test, no lejos

Una tentación natural, apenas dos tests comparten un estado inicial, es mover el seed a una fixture compartida "para no repetir". A veces está bien; a veces arruina la legibilidad. La regla es sobre qué tan lejos del test puede vivir el seed sin que el test se vuelva ilegible.

Un seed que es el estado inicial específico de un test —"este test necesita exactamente estas dos reservas de Focus"— vive mejor dentro del test, porque es parte de lo que el test afirma. Si lo escondes en una fixture, el lector ve la aserción len(focus) == 2 sin ver de dónde salen las dos, y tiene que ir a cazar la fixture. El test deja de leerse solo.

Un seed que es un estado base común y estable —datos de referencia que casi todos los tests necesitan igual, como el catálogo de salas— sí puede vivir en una fixture, porque no es lo que ningún test en particular está probando; es el escenario compartido. La distinción es la misma del montaje de cine: los muebles fijos del set (el escenario base) los pone el equipo una vez; los objetos que cambian de escena a escena (el estado específico) se montan por escena. Aplicar mal esta distinción —esconder en una fixture el dato que el test está probando— es la causa número uno de tests de integración que "funcionan pero nadie entiende".

Y hay un peligro concreto con las fixtures de seed de scope amplio, que conecta de vuelta con la lección 2: si pones el seed en una fixture de scope módulo con escritura, todos los tests comparten esas filas y las modificaciones de uno contaminan a otro. Un seed compartido solo es seguro si es de solo lectura —los tests leen ese estado base pero no lo alteran— o si vive en scope función (se resiembra fresco por test). Sembrar no te exime del aislamiento; se apoya en él.

La frontera: dónde empieza el Builder

Fíjate en la función booking(bid, room_id, hour) del ejemplo: rellena member_id, start, end, status y price_cents con valores razonables, y te deja variar solo el id, la sala y la hora. Es un ayudante mínimo, suficiente para sembrar reservas parecidas sin repetir siete campos cada vez. Y es, exactamente, el punto donde asoma un patrón más grande.

Cuando los datos de prueba crecen —quieres una reserva pro cancelada de tres horas, otra basic confirmada de una hora, otra con un precio atípico— y empiezas a querer "una reserva normal, pero con el estado cancelado" o "una reserva normal, pero de Studio", ese booking(...) con parámetros posicionales se queda corto y se vuelve incómodo. Lo que resuelve ese problema con elegancia —valores por defecto sensatos, sobrescrituras puntuales legibles, encadenamiento— es el patrón Builder (y su primo, la Object Mother). Construir datos de prueba complejos con Builders es un tema por derecho propio, y no es de esta guía: es el módulo 7 de test-doubles-and-test-data-guide.

La frontera, dicha con precisión: en esta guía, el foco es el aislamiento del recurso real —que la base esté limpia y el estado sea conocido— y el sembrado es el medio para poner un estado conocido, resuelto con funciones mínimas como seed y booking. Cuando la fábrica de datos empiece a pedir un diseño propio —muchas variantes, defaults con overrides, legibilidad de "una X normal salvo Y"—, eso es el Builder, y el lugar de aprenderlo a fondo es la guía de dobles. Aquí paramos en el ayudante mínimo, a propósito: cruzar esa frontera sería reescribir un módulo que ya existe. Si tu sembrado se está poniendo pesado, esa incomodidad es la señal de que te toca el Builder de la guía hermana.

Errores comunes

Sembrar de más "por si el test lo necesita". Qué pasa: se cargan diez reservas cuando el test solo verifica dos. Por qué pasa: parece más seguro tener datos de sobra. Cómo detectarlo: si el lector no puede decir cuáles de las filas sembradas importan para la aserción, sembraste de más. Cómo corregirlo: siembra el mínimo que hace verdadera la aserción, más los pocos datos-trampa deliberados (como la reserva de Studio que no debe salir en el filtro). Cada fila sembrada debería tener una razón visible.

Esconder en una fixture el dato que el test está probando. Qué pasa: el estado inicial específico de un test se mueve a una fixture compartida, y el test queda con aserciones sobre números que no se ven venir. Por qué pasa: se busca no repetir. Cómo detectarlo: si al leer el test tienes que abrir otra parte del archivo para entender de dónde salen los datos, la fixture escondió lo que debía estar a la vista. Cómo corregirlo: el estado que el test afirma vive en el test; solo el escenario base común y estable va a una fixture, y de solo lectura.

Sembrar en una fixture de scope amplio con escritura. Qué pasa: el seed va a una fixture de scope módulo, y los tests que modifican esas filas se contaminan. Por qué pasa: se quiere sembrar una vez para ir más rápido. Cómo detectarlo: si aparecen fallos dependientes del orden después de compartir el seed, es que los tests escriben sobre datos compartidos. Cómo corregirlo: un seed compartido solo es seguro de solo lectura; lo que los tests modifican se siembra fresco por test (scope función). Sembrar no reemplaza al aislamiento.

Ejercicios

Ejercicio 1 — ¿Dentro del test o en una fixture? Para cada seed, decide si lo pondrías dentro del test o en una fixture compartida, y por qué: (a) las dos reservas de Focus que un test específico verifica contar; (b) el catálogo de las tres salas (Focus, Studio, Boardroom) que casi todos los tests usan igual y ninguno modifica; (c) una reserva cancelada con un reembolso específico que un solo test necesita para probar cancel.

Ver solución
  • (a) Dentro del test. Es el estado que ese test está probando —cuenta esas dos—, así que debe estar a la vista, en las líneas del test, para que la aserción len == 2 se lea con su origen. Esconderlo en una fixture haría el test ilegible.
  • (b) En una fixture compartida, de solo lectura. Es escenario base común y estable que ningún test en particular prueba y ninguno modifica; puede vivir en una fixture (incluso de scope amplio, por ser solo lectura) para no repetirlo. No es lo que ningún test afirma; es el decorado fijo del set.
  • (c) Dentro del test (o en una fixture de scope función propia de ese test). Es específico de un solo test y es justo lo que ese test prueba —la reserva que se va a cancelar—, así que va a la vista. Como lo usa uno solo, no hay nada que compartir; y si va a fixture, que sea scope función para que se resiembre fresca.

El criterio: el dato que el test afirma va dentro del test; el escenario base común, estable y de solo lectura puede ir a una fixture. La pregunta guía es "¿el lector necesita ver este dato para entender la aserción?": si sí, dentro del test.

Ejercicio 2 — El seed vacío que se dice en voz alta. El test_empty_room_returns_nothing incluye seed(repo) con lista vacía, aunque la base ya está vacía por la fixture. Un compañero quiere borrar esa línea "porque no hace nada". Da dos razones para conservarla.

Ver solución

Dos razones para conservar el seed(repo) vacío, aunque técnicamente no cambie el estado:

  1. Declara una decisión, no un descuido. Con la línea presente, queda claro que el estado inicial vacío es intencional —el test prueba a propósito el caso "sala sin reservas"—, no que alguien se olvidó de sembrar. Sin la línea, un lector futuro no sabe si el vacío es deliberado o un hueco; podría "arreglarlo" agregando datos y cambiar lo que el test quería probar. La línea explícita protege la intención.

  2. Uniformiza la lectura de la suite. Si todos los tests empiezan con una línea de seed(...) que declara su estado inicial —tenga datos o esté vacía—, la suite se lee con un patrón consistente: "primero el seed, luego la acción, luego la aserción". Un test que salta el seed rompe ese ritmo y obliga al lector a preguntarse si falta algo. La consistencia hace la suite más fácil de leer y de mantener.

En resumen: la línea no cambia el estado, pero cambia la comunicación. En tests, decir en voz alta "empiezo vacío" vale más que ahorrar una línea, porque el estado inicial es parte de lo que el test afirma y merece ser visible.

Ejercicio 3 — ¿Función mínima o Builder? Tu función booking(bid, room_id, hour) fija member_id, status y price_cents. Llega un requerimiento: necesitas sembrar reservas variando también el tier del socio, el status (confirmada o cancelada) y el price_cents, en muchas combinaciones distintas, y quieres poder escribir "una reserva normal salvo que está cancelada". ¿Sigues estirando booking(...) o cruzas a otra herramienta? Justifica y nombra la herramienta y dónde se estudia.

Ver solución

No sigo estirando booking(...). Agregar más parámetros posicionales (booking(bid, room_id, hour, tier, status, price_cents, ...)) lo vuelve ilegible —una llamada con seis o siete argumentos posicionales donde nadie recuerda cuál es cuál— y no resuelve lo que de verdad quiero: expresar "una reserva normal salvo una cosa" sin repetir todos los campos. Ese es exactamente el problema que el patrón Builder (y la Object Mother) resuelven: defaults sensatos para todos los campos, y sobrescrituras puntuales y legibles para el uno o dos que cambian —algo como a_booking().cancelled().with_price_cents(3000).build()—.

Construir datos de prueba complejos con Builders es un tema propio y no es de esta guía: se estudia a fondo en el módulo 7 de test-doubles-and-test-data-guide. La incomodidad que sentí al querer estirar booking(...) es precisamente la señal de que crucé la frontera: pasé del "sembrado mínimo para aislar un recurso" (esta guía) a la "fábrica de datos de prueba" (la guía de dobles). El foco de este módulo es el aislamiento del recurso real; cuando la construcción de los datos se vuelve el problema, la herramienta y su lugar de estudio están en la guía hermana.

Resumen y siguiente paso

En esta lección pusiste los datos justos dentro de la base limpia. Sembraste estados iniciales explícitos, mínimos y dichos en voz alta: el test que monta su escena con tres reservas y verifica que el filtro devuelve solo las dos correctas, y el que declara su seed vacío para dejar claro que el estado inicial es una decisión. Viste por qué el dato que el test afirma vive dentro del test —para que se lea solo— y solo el escenario base común y de solo lectura va a una fixture, y por qué un seed compartido con escritura reintroduce la contaminación de la lección 2. Y marcaste la frontera clave del módulo: cuando la construcción de datos se vuelve compleja —variantes, defaults con overrides, "una X normal salvo Y"—, la herramienta es el patrón Builder, que se estudia en test-doubles-and-test-data-guide, no aquí; en esta guía el foco es el aislamiento del recurso real.

Antes de avanzar deberías poder: sembrar un estado inicial mínimo y explícito dentro de un test; decidir qué seed va en el test y cuál en una fixture de solo lectura; y reconocer cuándo el sembrado pide cruzar a la frontera del Builder.

Lo que sigue es nombrar y demostrar los dos principios que todo este módulo perseguía. En la lección 7 vas a cerrar con independencia y repetibilidad: cada test parte de un estado conocido (independiente de sus vecinos) y da el mismo resultado siempre (repetible, corras cuando corras). Vas a ver, con salida real, la suite aislada pasando en orden normal, en orden invertido y corrida dos veces —el mismo verde siempre—, y a entender por qué el orden de ejecución nunca debe cambiar un veredicto. Es el criterio con el que juzgarás si una suite de integración está bien hecha, y la antesala del mini-proyecto.

Recursos