Módulo 2: Arquitectura de fixtures: la columna vertebral

1. Presentación del módulo: la fixture como columna vertebral

Descripción

En el módulo 1 diagnosticaste la enfermedad. La suite de tests de Reservo empezó como un archivo con tres tests y hoy tiene cientos, y cada uno de esos cientos arma su propio escenario a mano: crea la sala Focus con su tarifa, crea un socio, arma el Calendar, y —los que prueban el flujo completo— instancia un BookingService con sus cinco colaboradores. Las mismas seis o siete líneas, copiadas y pegadas doscientas veces. Viste el costo: cuando el constructor de BookingService cambió y pasó a pedir un sexto colaborador, alguien tuvo que tocar doscientos archivos, y se le escaparon treinta. Nombraste también la cura: un framework de tests tiene capas —fixtures, utilidades, configuración, datos— y la primera de todas, la que sostiene a las demás, son las fixtures. Este módulo construye esa capa.

Aquí las fixtures dejan de ser "una forma cómoda de compartir setup", que es como probablemente las conociste en fundamentos, y se vuelven lo que de verdad son en un framework serio: la columna vertebral de la suite. Una columna vertebral es la pieza que sostiene todo el peso y que, si está bien diseñada, deja que todo lo demás cuelgue de ella sin esfuerzo. La calidad de tu capa de fixtures decide, más que cualquier otra cosa, cuánto duele agregar el test número doscientos uno. Al terminar el módulo vas a saber diseñar esa columna: escribir fixtures que se reutilizan en toda la suite, colocarlas en la jerarquía correcta de conftest.py para que pytest las encuentre sin que importes nada, elegir el scope de cada una como una decisión consciente y no como un default que copiaste, y componer fixtures unas sobre otras hasta que un solo nombre —booking_service— te entregue un objeto compuesto listo para usar, con todas sus piezas armadas por debajo.

Conexión con el módulo: esta es la lección-mapa. No escribimos la columna todavía; construimos el vocabulario y el criterio que las siete lecciones siguientes dan por sabidos. La lección 2 define qué es una fixture por dentro y cuál es la garantía que la hace valer la pena. La lección 3 mueve las fixtures a conftest.py y te enseña su jerarquía —el archivo de la raíz contra uno por carpeta—. La 4 trata el scope como la decisión de arquitectura que es. La 5 es el corazón: componer fixtures en un grafo de dependencias, que es lo que convierte seis fixtures sueltas en una columna vertebral. La 6 añade yield para limpiar lo que una fixture abrió. La 7 cubre las fixtures que fabrican y autouse. Y la 8 junta todo: diseñas y entregas el conftest.py de Reservo. La frontera se mantiene firme: los marcadores y la config son el módulo 4; los builders de datos a fondo son el módulo 7 y la guía de dobles. Aquí el foco es uno solo —las fixtures como arquitectura—.

Una obra en construcción y su andamio

Imagina que levantas un edificio. Cada piso necesita el mismo andamio: una plataforma a la altura correcta, con sus tablones y sus soportes, para que los albañiles trabajen. Tienes dos maneras de conseguirlo.

La primera es que cada cuadrilla arme su propio andamio desde cero cada mañana. La cuadrilla de plomería carga los tubos, los ensambla, pone los tablones, trabaja, y al final del día lo desarma. Al día siguiente llega la cuadrilla de electricidad y arma otro andamio idéntico en el mismo lugar, desde cero. Funciona, pero mira el desperdicio: el mismo andamio se arma y se desarma decenas de veces, cada cuadrilla repite el trabajo de la anterior, y —lo peor— si el diseño del andamio cambia (ahora los soportes van más separados por una norma nueva), hay que avisarle a cada cuadrilla por separado, y la que no se enteró arma el andamio viejo y trabaja insegura sin saberlo.

La segunda manera es que la obra tenga un sistema de andamios compartido: piezas estándar que cualquier cuadrilla pide y recibe montadas, con un plano único que dice cómo se ensamblan. La cuadrilla de plomería pide "plataforma del tercer piso" y la recibe lista; la de electricidad pide la misma y la recibe igual de lista. Si la norma de los soportes cambia, se cambia en un lugar —el plano del sistema— y todas las cuadrillas reciben el andamio nuevo automáticamente, sin que nadie tenga que avisarles una por una. El andamio dejó de ser algo que cada quien improvisa y se volvió infraestructura de la obra.

Las fixtures son ese sistema de andamios, y tu suite de tests es la obra. Cada test necesita un escenario montado —la sala, el socio, el calendario, el servicio— igual que cada cuadrilla necesita su plataforma. Sin fixtures, cada test arma su andamio a mano y lo desarma, y el día que el BookingService pide un colaborador nuevo tienes que tocar doscientos tests uno por uno. Con fixtures, el escenario se define una vez en un lugar central, cada test lo pide por su nombre y lo recibe montado, y el día del cambio tocas una fixture y toda la suite recibe el escenario nuevo. Ese es todo el valor de la columna vertebral de fixtures, y es la diferencia entre una suite que crece con dolor y una que crece sola.

Ejemplo trabajado: la suite antes y después de la columna

Veámoslo con la suite de Reservo. No escribas nada todavía —es la foto de a dónde vamos en el módulo—, pero míralo con atención porque es el módulo entero en dos pantallas.

Así se ve hoy un par de tests de la suite de Reservo, cada uno armando su escenario a mano:

# tests/test_booking_flow.py  —  ANTES: cada test arma su andamio
from datetime import datetime

from reservo.calendar import Calendar
from reservo.doubles import (FakeBookingRepository, FakePaymentGateway,
                             FixedClock, SpyEmailSender)
from reservo.models import Room, Member
from reservo.service import BookingService


def test_booking_a_pro_charges_6000():
    room = Room(id="focus", name="Focus", capacity=4, hourly_cents=2500)
    member = Member(id="m-ben", name="Ben", tier="pro")
    calendar = Calendar()
    payments = FakePaymentGateway()
    emails = SpyEmailSender()
    repo = FakeBookingRepository()
    clock = FixedClock(datetime(2026, 3, 1, 9))
    service = BookingService(calendar, clock, payments, emails, repo)

    start = datetime(2026, 3, 10, 9)
    end = datetime(2026, 3, 10, 12)
    booking = service.book(room, member, start, end)

    assert booking.price_cents == 6000


def test_booking_saves_and_confirms():
    room = Room(id="focus", name="Focus", capacity=4, hourly_cents=2500)
    member = Member(id="m-ana", name="Ana", tier="basic")
    calendar = Calendar()
    payments = FakePaymentGateway()
    emails = SpyEmailSender()
    repo = FakeBookingRepository()
    clock = FixedClock(datetime(2026, 3, 1, 9))
    service = BookingService(calendar, clock, payments, emails, repo)

    start = datetime(2026, 3, 10, 9)
    end = datetime(2026, 3, 10, 12)
    booking = service.book(room, member, start, end)

    assert repo.get(booking.id).status == "confirmed"

Cuenta las líneas de andamio: cada test dedica ocho líneas a armar el escenario antes de llegar a lo único que le importa —una a book, una al assert—. Y las ocho líneas son casi idénticas entre los dos tests. Ahora multiplica por los doscientos tests de la suite. El día que BookingService.__init__ pida un sexto colaborador, esas ocho líneas se vuelven nueve, en doscientos lugares.

Así se ve después de construir la columna de fixtures de este módulo:

# tests/test_booking_flow.py  —  DESPUÉS: el andamio ya está montado
from datetime import datetime


def test_booking_a_pro_charges_6000(booking_service, focus_room, pro_member, payments):
    start = datetime(2026, 3, 10, 9)
    end = datetime(2026, 3, 10, 12)
    booking = booking_service.book(focus_room, pro_member, start, end)
    assert booking.price_cents == 6000


def test_booking_saves_and_confirms(booking_service, focus_room, basic_member, repo, emails):
    start = datetime(2026, 3, 10, 9)
    end = datetime(2026, 3, 10, 12)
    booking = booking_service.book(focus_room, basic_member, start, end)
    assert repo.get(booking.id).status == "confirmed"

El andamio desapareció del test. Cada test pide por su nombre las piezas que necesita —booking_service, focus_room, pro_member— y las recibe montadas. Las ocho líneas de armado se fueron a un conftest.py central que escribiremos en el módulo, y el test quedó reducido a lo que de verdad afirma: reservar y verificar el cobro. El día que BookingService pida un colaborador nuevo, tocas una fixture —booking_service, en el conftest.py— y los doscientos tests siguen verdes sin que abras ninguno.

Qué esperar. Cuando corras esa suite ya con su columna de fixtures montada (Python 3.14.0, pytest 9.1.1), verás la línea verde de siempre:

============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
rootdir: /private/tmp/reservo_m2
collected 6 items

tests/integration/test_book_flow.py ..                                   [ 33%]
tests/unit/test_factory_demo.py ..                                       [ 66%]
tests/unit/test_pricing_fixtures.py ..                                   [100%]

============================== 6 passed in 0.01s ===============================

Verde es verde, aquí no hay sorpresa. La sorpresa —la que da nombre al módulo— aparece cuando le pides a pytest que te muestre el andamio montándose por debajo, con --setup-show:

        SETUP    F calendar
        SETUP    F clock
        SETUP    F payments
        SETUP    F emails
        SETUP    F repo
        SETUP    F booking_service (fixtures used: calendar, clock, emails, payments, repo)
        SETUP    F focus_room
        SETUP    F pro_member
        tests/integration/test_book_flow.py::test_booking_a_pro_charges_6000 (fixtures used: booking_service, calendar, clock, emails, focus_room, payments, pro_member, repo) .
        TEARDOWN F pro_member
        TEARDOWN F focus_room
        TEARDOWN F booking_service
        ...

Fíjate en la línea de booking_service: dice (fixtures used: calendar, clock, emails, payments, repo). pytest armó primero las cinco piezas de abajo y luego, con ellas, el booking_service de arriba —el grafo de dependencias se resolvió solo—. Eso es la columna vertebral en acción, y es exactamente lo que vas a saber diseñar al terminar el módulo. No te preocupes por entender cada línea todavía; cada una tiene su lección. Guárdala como la foto del destino.

Las seis piezas que arma este módulo

Vale la pena nombrar las seis piezas de la columna, porque cada lección construye una y quiero que tengas el plano completo antes de empezar.

Uno: la fixture como pieza reutilizable (lección 2). Antes que la arquitectura, el ladrillo. Qué es una fixture por dentro —una función con @pytest.fixture que arma algo—, cómo un test la pide (nombrándola como parámetro), y la garantía que la distingue de una simple constante compartida: cada test recibe una instancia fresca, así que un test que modifica su sala no contamina al siguiente. Esa garantía de aislamiento es la razón técnica de que las fixtures sean la base y no un lujo.

Dos: conftest.py y su jerarquía (lección 3). Una fixture reutilizable no sirve de nada si vive encerrada en un archivo de tests. conftest.py es el archivo especial donde pytest busca fixtures y las pone a disposición de todos los tests de esa carpeta y sus subcarpetas —sin que importes nada—. Y hay jerarquía: el conftest.py de la raíz ofrece fixtures a toda la suite; uno dentro de tests/integration/ ofrece fixtures solo a esa rama. Esa jerarquía es la que te deja tener fixtures globales y fixtures locales sin que se estorben.

Tres: el scope como decisión de arquitectura (lección 4). Cada fixture tiene un scope: cada cuánto se arma. El default, function, la arma de nuevo para cada test —máximo aislamiento—. Pero puedes pedir module o session para armarla una sola vez y compartirla, ganando velocidad a cambio de aislamiento. Elegir el scope no es un detalle: es una decisión de arquitectura con un trade-off real, y elegirlo mal introduce bugs sutiles o suites lentas.

Cuatro: componer fixtures, el grafo de dependencias (lección 5). Aquí las fixtures dejan de ser sueltas y se vuelven columna. Una fixture puede pedir otra como parámetro: booking_service pide calendar, y calendar podría pedir room. pytest resuelve ese grafo de dependencias solo, armando cada pieza en orden. Componer es lo que te deja construir objetos complejos —el BookingService con sus cinco colaboradores— desde piezas simples, sin repetir setup.

Cinco: yield para setup y teardown (lección 6). Algunas piezas del andamio hay que desmontarlas: un archivo temporal que borrar, una conexión que cerrar. Una fixture con yield hace las dos cosas —lo de antes del yield es setup, lo de después es teardown— y pytest corre el teardown aunque el test falle. Es cómo el sistema de andamios se desmonta solo y deja la obra limpia.

Seis: fixtures que fabrican y autouse (lección 7). A veces un test no necesita una sala sino tres, o una configurada de un modo particular. Entonces la fixture no devuelve un objeto: devuelve una función que fabrica objetos a pedido —la fixture-factory, un adelanto del módulo 7—. Y autouse es el opuesto: una fixture que corre para todos los tests sin que la pidan. Potente y peligrosa; la lección te da el criterio para usarla bien.

Seis piezas: ladrillo, ubicación, scope, composición, limpieza, fabricación. Juntas son la columna vertebral, y sobre ella se apoyan los módulos que siguen —la organización en capas del módulo 3, los marcadores del 4, las utilidades del 5— porque todos usan las fixtures que aquí diseñas.

Lo que este módulo NO es

Conviene marcar la frontera desde ya, porque las fixtures rozan temas que tienen su propia casa y no quiero que los mezcles.

No es sobre marcadores ni configuración. Vas a ver fixtures en conftest.py, pero no vas a tocar pytest.ini ni pyproject.toml ni vas a escribir un @pytest.mark.slow. La configuración del framework —dónde registrar marcadores, cómo poner addopts, cómo seleccionar subconjuntos con -m— es el módulo 4. Aquí conftest.py es solo el hogar de las fixtures.

No es sobre builders de datos a fondo. En la lección 7 vas a ver una fixture-factory que fabrica Booking a pedido, y es una probadita deliciosa. Pero el diseño serio de datos de test —builders con valores por defecto sensatos, el patrón Object Mother, cómo modelar datos que no duelen de mantener— es el módulo 7 de esta guía y el corazón de la guía hermana test-doubles-and-test-data-guide. Aquí la factory se presenta como una pieza del framework, no como el tema.

No es sobre organizar la suite en carpetas. Vas a ver tests/unit/ y tests/integration/ para ilustrar la jerarquía de conftest.py, pero cómo decidir esa estructura —por capa, por feature, cómo comunica la intención— es el módulo 3. Aquí las carpetas son el decorado que hace visible la jerarquía; su diseño se estudia allá.

Si te descubres queriendo registrar un marcador, diseñar un builder elaborado o discutir la estructura de carpetas, buena señal: reconociste un tema real. Anótalo y sigue; tiene su módulo. Aquí construimos la columna, nada más y nada menos.

Errores comunes

Creer que una fixture es "solo una función de ayuda con un decorador" (de subestimación). Qué pasa: alguien ve @pytest.fixture encima de una función que devuelve un objeto y concluye que es azúcar sintáctica sobre una función normal, así que la usa sin pensar en scope ni en composición. Por qué pasa: en su forma más simple, una fixture se parece a una función de ayuda. Cómo detectarlo: si nunca has pensado en el scope de tus fixtures ni las has compuesto unas sobre otras, las estás usando como funciones de ayuda, no como arquitectura. Cómo corregirlo: recuerda las tres cosas que una fixture hace y una función de ayuda no —te da una instancia fresca por test (aislamiento), se descubre sin importar (vía conftest.py), y se compone declarando otras fixtures como parámetros—. Esas tres son la razón de que las fixtures sean la columna vertebral y no un atajo. Este módulo entero es sobre aprovecharlas.

Saltar directo a componer sin dominar la pieza suelta (de correr antes de caminar). Qué pasa: alguien lee que la lección 5 (componer) es "el corazón" y quiere ir directo a armar el booking_service compuesto, saltándose la fixture básica y el conftest.py. Por qué pasa: la composición es lo vistoso y lo que se ve "de arquitecto". Cómo detectarlo: si intentas componer fixtures y te pierdes en errores de fixture not found o de scope, probablemente te saltaste los cimientos. Cómo corregirlo: sigue el orden del módulo. Una columna vertebral compuesta se apoya en piezas sueltas bien entendidas (lección 2), colocadas en la jerarquía correcta (lección 3), con el scope pensado (lección 4). Componer sin eso es apilar sin cimientos. Cada lección es un peldaño; súbelos en orden.

Confundir "menos código en el test" con "el objetivo" (de optimizar la métrica equivocada). Qué pasa: alguien mete todo en fixtures con tal de que los tests queden cortos, y termina con una maraña de fixtures que se llaman entre sí donde nadie entiende qué escenario tiene un test cuando lo lee. Por qué pasa: el "antes y después" de este módulo hace ver el código corto como la meta. Cómo detectarlo: si para entender qué prueba un test tienes que abrir tres conftest.py y seguir un grafo de fixtures, la columna se volvió laberinto. Cómo corregirlo: el objetivo no es "tests cortos", es "una suite que crece sin dolor y sigue siendo legible". Un buen diseño de fixtures esconde el andamio repetitivo pero deja visible lo que cada test afirma. La lección 5 y el mini-proyecto insisten en ese equilibrio: reutilizar sin ocultar la intención. Menos código es una consecuencia, no la meta.

Ejercicios

Ejercicio 1 — Cuenta el andamio. Vuelve al bloque "ANTES" del ejemplo trabajado. Cuenta cuántas líneas de cada función de test son andamio (armar el escenario) y cuántas son la prueba en sí (la acción y el assert). Luego di, con esa proporción, por qué la suite duele al crecer.

Ver solución

En cada test del bloque "ANTES", el desglose es así: ocho líneas de andamio —crear room, member, calendar, payments, emails, repo, clock, y construir el service— más las dos líneas de start/end, contra dos líneas de prueba real: la llamada a book y el assert. Es decir, alrededor del 80% de cada test es andamio repetido y solo el 20% es lo que el test de verdad afirma.

Por qué eso duele al crecer: ese 80% es casi idéntico entre los doscientos tests, así que cualquier cambio en la forma de armar el escenario —un colaborador nuevo en BookingService, una tarifa distinta, una firma que cambia— hay que aplicarlo en doscientos lugares, y basta que se te escape uno para tener un test que arma el escenario viejo. La suite no crece de forma lineal: cada test nuevo añade otro sitio que mantener, y el andamio duplicado es deuda que se paga en cada cambio. La columna de fixtures colapsa ese 80% a una sola definición central, y entonces el cambio se hace en un lugar.

Ejercicio 2 — Ubica cada tema. Para cada tarea, di si la resuelve este módulo (M2, fixtures como arquitectura) o si pertenece a otro módulo, y cuál: (a) "quiero marcar unos tests como slow y correr solo los rápidos en cada guardado"; (b) "quiero que booking_service se arme una sola vez por archivo de tests para que la suite corra más rápido"; (c) "quiero un builder que cree un Booking con valores por defecto sensatos y me deje cambiar solo el que me importa"; (d) "quiero que la fixture calendar esté disponible en todos los tests sin importarla en cada archivo".

Ver solución
  • (a) es del módulo 4 (marcadores y configuración). Marcar tests con @pytest.mark.slow, registrar el marcador y seleccionar subconjuntos con -m es el tema de config del framework. Aquí no tocamos marcadores.
  • (b) es de este módulo, lección 4 (el scope). "Armarse una sola vez por archivo" es exactamente scope="module", una decisión de arquitectura entre aislamiento y velocidad. Es M2.
  • (c) es del módulo 7 (datos y builders a fondo) y de la guía test-doubles-and-test-data-guide. Un builder con defaults sensatos y sobrescritura selectiva es diseño de datos de test. En M2 solo asomas la fixture-factory (lección 7) como pieza del framework; el builder serio se diseña allá.
  • (d) es de este módulo, lección 3 (conftest.py y su jerarquía). "Disponible en todos los tests sin importarla" es la definición de una fixture en el conftest.py de la raíz. Es M2.

El punto del ejercicio: dos de las cuatro tareas (b y d) son columna vertebral de fixtures —scope y ubicación— y dos (a y c) rozan las fixtures pero tienen su propia casa. Reconocer la frontera te ahorra meter en M2 cosas que se enseñan mejor, y más adelante, en otro lado.

Ejercicio 3 — Predice el ahorro. La suite de Reservo tiene 200 tests que instancian BookingService. Hoy cada uno lo construye a mano con cinco colaboradores. Mañana BookingService.__init__ pasa a pedir un sexto colaborador (un metrics). Estima cuántos lugares hay que tocar (a) con la suite de hoy, sin columna de fixtures, y (b) con una columna de fixtures ya montada. Explica la diferencia en una frase.

Ver solución
  • (a) Sin columna de fixtures: 200 lugares (uno por test), más el riesgo real de que se escapen algunos. Cada test tiene su propia línea BookingService(calendar, clock, payments, emails, repo) que ahora debe pasar a BookingService(calendar, clock, payments, emails, repo, metrics), y cada uno además debe crear el nuevo colaborador metrics. Doscientas ediciones mecánicas, y basta olvidar una para dejar un test roto o —peor— un test que compila pero arma el servicio con el colaborador viejo.
  • (b) Con columna de fixtures: 1 lugar. El BookingService se construye en una sola fixture booking_service dentro del conftest.py. Añades ahí la creación de metrics y lo pasas al constructor. Los 200 tests que piden booking_service reciben el servicio nuevo sin que abras ninguno.

La diferencia en una frase: una columna de fixtures convierte un cambio de "doscientas ediciones y reza que no se te escape ninguna" en un cambio de "una edición en un lugar". Ese es, medido en trabajo real de mantenimiento, el retorno de invertir en la arquitectura de fixtures, y es la razón de que este módulo exista antes que todos los demás.

Resumen y siguiente paso

En esta lección enmarcaste el módulo. Viste que las fixtures no son un atajo cómodo sino la columna vertebral de un framework de tests: la capa que sostiene a todas las demás y que decide, más que cualquier otra, cuánto duele hacer crecer la suite. Con la analogía del sistema de andamios de una obra entendiste el valor de fondo —definir el escenario una vez, que cada test lo pida montado, y cambiarlo en un solo lugar el día que haga falta—, y con el "antes y después" de la suite de Reservo lo viste en código: ocho líneas de andamio por test que se colapsan en unos nombres de parámetro, y un cambio de "doscientas ediciones" que se vuelve "una edición".

Tienes también el plano de las seis piezas que el módulo construye —la fixture reutilizable, la jerarquía de conftest.py, el scope, la composición, yield, y la factory con autouse— y la frontera clara con los módulos vecinos: los marcadores y la config son el módulo 4, los builders de datos a fondo son el módulo 7 y la guía de dobles, y la estructura de carpetas es el módulo 3.

Antes de avanzar deberías poder: explicar con tus palabras por qué las fixtures son la primera capa de un framework y no un lujo; señalar en un test cuánto es andamio repetido y cuánto es la prueba real; y ubicar una tarea de testing en su módulo correcto (fixture, marcador, builder, estructura).

Lo que sigue es dejar el mapa y poner el primer ladrillo. En la lección 2 vas a construir tu primera pieza de la columna: una fixture de verdad. Vas a ver qué es por dentro, cómo un test la pide por su nombre, y —lo que la separa de una simple constante compartida— la garantía de que cada test recibe una instancia fresca. Ahí el andamio deja de ser una metáfora y se vuelve una función con @pytest.fixture que corre en tu terminal.

Recursos

  • Cómo usar fixtures en pytest — la guía oficial del mecanismo que este módulo convierte en arquitectura. No hace falta leerla entera ahora; es la referencia que vas a consultar lección tras lección. Está en inglés, y el vocabulario que aprenderás aquí (fixture, scope, conftest, teardown) es el suyo.
  • Referencia de fixtures — la explicación de fondo de qué son las fixtures y por qué existen en pytest. Léela si quieres el porqué del diseño antes de empezar a construir; si no, lo reconstruimos pieza por pieza.
  • pytest --setup-show — la bandera que muestra el andamio montándose y desmontándose por debajo, la que usaste en el ejemplo trabajado para ver el grafo de booking_service. Es la herramienta con la que vas a ver la columna vertebral en cada lección de este módulo.