Módulo 1: Del script suelto al framework — por qué la arquitectura

7. El retorno de invertir en arquitectura

Descripción

Al terminar esta lección vas a saber decidir cuánto invertir en la arquitectura de tu suite y cuándo, que es una habilidad tan importante como saber construir las fixtures. Porque hasta aquí la guía te ha mostrado el dolor de no tener framework, y podrías salir con la conclusión ingenua de "entonces siempre, cuanto más framework, mejor". Es falsa. Construir el framework también cuesta —tiempo, indirección, una fixture que hay que entender antes de leer un test— y construir de más es su propio error: fixtures que nadie usa, factories para objetos que aparecen una vez, abstracciones que resuelven problemas que no tienes. La sabiduría no es "arquitectura sí/no", es dónde en la curva de tu suite estás y qué inversión corresponde a ese punto.

Esta lección traza esa curva: por qué la misma fixture que a diez tests parece sobre-ingeniería, a mil es lo único que mantiene la suite viva. Te da la regla operativa para invertir en el momento justo —el tercer duplicado— y el reverso, las señales de que sobre-invertiste. Y te muestra, ejecutado, cómo se cobra la inversión: en un framework, agregar el test número siete cuesta una línea, y un cambio de modelo que sin arquitectura tocaría cientos de lugares se hace en uno solo, con la suite entera en verde.

Conexión con el módulo: esta es la lección que convierte todo el dolor de las anteriores en una decisión económica. La 2 y la 6 midieron el costo de no tener framework; esta pone del otro lado el costo de tenerlo y te enseña a comparar. Es la última lección conceptual del módulo: después de ella, el mini-proyecto (lección 8) te pone a hacer la inversión más pequeña y de mayor retorno que existe —extraer tu primera fixture compartida— y a medir su pago con tus manos. Y de aquí en adelante, cada capa que construyas en los módulos 2 a 7 la vas a evaluar con la brújula de esta lección: ¿este framework resuelve un dolor que tengo, o es andamiaje que admiro pero no necesito?

Analogía: comprar la máquina cuando el volumen la paga

Un taller de costura que hace tres prendas al mes cose los ojales a mano. Es lento por prenda —quince minutos cada ojal— pero funciona, y comprar una ojaladora industrial de cuarenta mil pesos para tres prendas al mes sería una locura: la máquina se pagaría en veinte años. A ese volumen, "hacerlo a mano" es la decisión correcta, no la floja.

El mismo taller, cuando empieza a producir trescientas prendas al mes, cose ojales a mano durante días enteros. Ahora la ojaladora de cuarenta mil se paga en dos semanas: lo que costaba quince minutos por prenda pasa a costar segundos, multiplicado por trescientas. A ese volumen, seguir cosiendo a mano deja de ser humildad artesanal y se vuelve un error económico —estás quemando días de trabajo para ahorrarte una inversión que se pagaría sola—.

Fíjate en las dos moralejas, porque las dos importan. La primera: la misma decisión (comprar la máquina) es equivocada a bajo volumen y correcta a alto volumen —no hay una respuesta universal, hay un punto de cruce—. La segunda: el error existe en las dos direcciones —comprar la máquina para tres prendas es sobre-invertir; coser a mano trescientas es sub-invertir—. La arquitectura de tu suite es la ojaladora. La fixture compartida, el marcador, la factory: cada una es una máquina que cuesta montar y que paga por volumen. Tu trabajo no es "montar todas las máquinas siempre"; es reconocer cuándo tu volumen cruzó el punto en que la máquina se paga.

La curva de costo: dos líneas que se cruzan

Pon en tu cabeza dos líneas, en un eje donde la horizontal es "tamaño de la suite" (de 10 a 1000 tests) y la vertical es "costo total de mantenerla".

La suite sin arquitectura empieza barata: a diez tests, copiar el setup no cuesta casi nada, y no hay fixtures que entender. Pero su costo crece rápido —recuerda la lección 6: es O(N × C), el número de tests por la frecuencia de cambios—. Cada test nuevo copia el setup; cada cambio de modelo toca todos los lugares. La línea sube con pendiente creciente.

La suite con arquitectura empieza más cara: montar la primera fixture cuesta cinco minutos que la versión copy-paste no paga, y hay una indirección (el conftest.py) que un lector nuevo debe conocer. Pero su costo crece lento —O(C), casi plano—: cada test nuevo reutiliza las fixtures sin agregar setup, y cada cambio de modelo se hace en un lugar. La línea sube con pendiente casi horizontal.

Dos líneas: una que empieza abajo y sube empinada, otra que empieza un poco más arriba y sube plana. Se cruzan. A la izquierda del cruce —suites pequeñas—, la sin-arquitectura es más barata: por eso montar un framework para diez tests es sobre-ingeniería genuina. A la derecha del cruce —suites que crecen—, la con-arquitectura es dramáticamente más barata, y la distancia entre las líneas se abre sin límite. Toda la habilidad de esta lección es estimar dónde está el cruce para tu caso y no quedarte del lado equivocado: ni montar la máquina antes del cruce (sobre-inversión), ni seguir a mano mucho después (sub-inversión, la deuda de la lección 6).

La regla operativa: el tercer duplicado

"Estima el cruce" es correcto pero difícil de aplicar en el momento. Por suerte hay una regla práctica, heredada de la ingeniería de software y afinada para tests, que acierta casi siempre:

La primera vez que escribes un bloque de setup, escríbelo. La segunda vez que lo necesitas, cópialo —sí, cópialo— y aguanta la incomodidad. La tercera vez, extráelo a una fixture compartida.

¿Por qué el tercero y no el segundo? Porque con dos ocurrencias todavía no sabes cuál es la parte estable del patrón y cuál varía. Extraer al segundo te hace adivinar la abstracción, y adivinar temprano produce fixtures con demasiados parámetros o con supuestos que el tercer caso rompe. Al tercer uso ya ves el patrón: qué se repite idéntico (va a la fixture) y qué cambia entre casos (se queda como parámetro o en el test). Extraer al tercero es extraer con evidencia, no con corazonada. Y el costo de haber copiado dos veces es trivial —dos bloques— comparado con el costo de una abstracción equivocada que hay que deshacer.

La regla también te protege del error opuesto. Si un bloque de setup aparece una sola vez, la regla te dice explícitamente: no lo extraigas. Una fixture usada por un solo test no comparte nada; solo agrega una indirección que obliga al lector a saltar de archivo para entender un test que habría leído de corrido. Esa es sobre-inversión, y la regla del tercer duplicado la previene tan bien como previene la sub-inversión.

Ejemplo trabajado: cómo se cobra la inversión

Veamos el retorno en concreto, con una suite de precios de Reservo ya montada sobre fixtures. El conftest.py tiene el mundo compartido:

# conftest.py
import pytest

from reservo.models import Room, Member


@pytest.fixture
def focus_room():
    return Room(id="r1", name="Focus", capacity=1, hourly_cents=2500)


@pytest.fixture
def basic_member():
    return Member(id="m1", name="Ana", tier="basic")


@pytest.fixture
def pro_member():
    return Member(id="m2", name="Bruno", tier="pro")

Con esa inversión hecha, mira lo que cuesta cada test nuevo. No hay setup: cada test es una línea que pide su mundo y afirma su ancla.

# test_pricing.py
from reservo.pricing import price_cents


def test_basic_1h(focus_room, basic_member):
    assert price_cents(focus_room, basic_member, 1) == 2500


def test_basic_3h(focus_room, basic_member):
    assert price_cents(focus_room, basic_member, 3) == 7500


def test_pro_1h(focus_room, pro_member):
    assert price_cents(focus_room, pro_member, 1) == 2000


def test_pro_3h(focus_room, pro_member):
    assert price_cents(focus_room, pro_member, 3) == 6000


def test_basic_5h(focus_room, basic_member):
    assert price_cents(focus_room, basic_member, 5) == 12500


def test_pro_5h(focus_room, pro_member):
    assert price_cents(focus_room, pro_member, 5) == 10000

Qué esperar. Con pytest test_pricing.py -q:

......                                                                   [100%]
6 passed in 0.01s

Retorno 1: el costo marginal de un test nuevo es una línea. Quiero cubrir dos casos más —basic 2 h (5000) y pro 2 h (4000)—. En la suite copy-paste, cada uno serían ocho líneas (construir sala, miembro, calendario, afirmar). Aquí son dos líneas cada uno:

def test_basic_2h(focus_room, basic_member):
    assert price_cents(focus_room, basic_member, 2) == 5000


def test_pro_2h(focus_room, pro_member):
    assert price_cents(focus_room, pro_member, 2) == 4000

Qué esperar. Con los dos tests agregados, pytest test_pricing.py -q:

........                                                                 [100%]
8 passed in 0.01s

Ocho tests, y los dos nuevos costaron una línea de setup cada uno —cero, en realidad: pidieron fixtures que ya existían—. Ese es el primer dividendo de la inversión: una vez montado el framework, la suite crece casi gratis. Cada test que agregas hereda el mundo, en vez de reconstruirlo.

Retorno 2: el cambio de modelo se hace en un lugar. Ahora el golpe que en la lección 2 rompía seis tests y en la 6 rompía doscientos: Room gana owner_id. Con la suite copy-paste, serían ocho ediciones (una por test). Con el framework, la sala Focus se construye en un solo lugar —la fixture focus_room—, así que el arreglo es una edición ahí:

@pytest.fixture
def focus_room():
    return Room(id="r1", name="Focus", capacity=1, hourly_cents=2500, owner_id="o1")

Qué esperar. Con el modelo cambiado y la fixture arreglada en ese único lugar, pytest test_pricing.py -q:

........                                                                 [100%]
8 passed in 0.01s

Ocho tests en verde, y no tocamos ninguno de los ocho: solo la fixture. Ahí está la inversión cobrándose. La misma mudanza de Room que sin arquitectura era ocho (o doscientas) ediciones frágiles, con arquitectura es una edición en el lugar obvio. Multiplica esos dos retornos —tests que crecen gratis, cambios que se hacen en un punto— por la vida de un proyecto y entiendes por qué, pasado el cruce, la línea con arquitectura no solo es más barata: hace posible una suite que sin ella se habría vuelto ingobernable.

El otro lado: cuándo NO invertir

Para que la lección sea honesta, hay que decir con la misma fuerza cuándo la arquitectura es un error. Sobre-invertir tiene sus propios síntomas, y reconocerlos te evita convertirte en la persona que monta la ojaladora para tres prendas:

  • Fixtures que un solo test usa. Si una fixture tiene un único consumidor, no comparte nada: es una función auxiliar disfrazada de arquitectura que solo agrega el costo de saltar de archivo. Deja el setup en el test.
  • Factories para objetos que aparecen una vez. Una factory se justifica cuando muchos tests fabrican variaciones del mismo objeto. Para un objeto único, es construcción con pasos de más.
  • Marcadores que nunca filtran. Un marcador que ningún -m selecciona no ordena nada; es una etiqueta muerta que ensucia la config.
  • Abstracción antes del tercer uso. Extraer una fixture al primer o segundo uso es adivinar la forma del patrón antes de tener evidencia; suele producir la abstracción equivocada, que cuesta más deshacer que el copy-paste que evitó.
  • Framework para una suite que no va a crecer. Un script de test de un solo uso, una prueba exploratoria que borrarás mañana: no merece infraestructura. La arquitectura es una apuesta a que la suite vivirá y crecerá; si no lo hará, no apuestes.

La prueba mental: antes de construir cualquier pieza del framework, pregúntate "¿qué dolor concreto y presente cura esto?". Si puedes nombrar el dolor —"tengo el setup de Focus copiado en veinte tests y Room cambia seguido"—, invierte. Si la respuesta es "es buena práctica" o "por si acaso crece", no inviertas todavía: estás admirando una máquina que tu volumen aún no paga.

Errores comunes

Sobre-arquitecturar por miedo al dolor de la lección 2. Qué pasa: alguien sale asustado de ver 200 failed y monta fixtures, factories y marcadores para su suite de quince tests, "para no sufrir eso". Termina con más framework que suite, y con abstracciones que nadie necesita. Por qué pasa: el miedo confunde "esto puede doler algún día" con "me duele ahora". Cómo detectarlo: cuenta los consumidores de cada pieza; si tienes fixtures con un usuario o marcadores que no filtran, sobre-invertiste. Cómo corregirlo: aplica el tercer duplicado. La arquitectura es respuesta a un dolor presente, no seguro contra uno hipotético.

Sub-invertir esperando "el momento adecuado" que nunca llega. Qué pasa: "cuando escale, arquitecturamos". La suite escala, pero migrar mil tests copy-paste a fixtures es tan caro que nunca se prioriza, y la deuda se vuelve permanente. Por qué pasa: el retrofit es visible y caro; la deuda es invisible y difusa. Cómo detectarlo: si tu plan de arquitectura es siempre futuro, ya estás sub-invirtiendo. Cómo corregirlo: invierte incrementalmente, al tercer duplicado, cada vez —así nunca acumulas mil lugares que migrar de golpe—. La arquitectura barata es la que se construye poco a poco, cuando cada patrón madura, no la que se pospone para un gran rediseño.

Medir el retorno solo en líneas ahorradas. Qué pasa: alguien evalúa si una fixture "vale la pena" contando cuántas líneas de setup borra, y concluye que una fixture que ahorra dos líneas por test no compensa. Por qué pasa: el ahorro de escritura es el retorno más visible, pero es el menor. Cómo detectarlo: si tu cálculo de retorno ignora el costo de los cambios futuros y la confianza, estás midiendo mal. Cómo corregirlo: el retorno grande de una fixture no es escribir menos hoy; es cambiar en un lugar cuando el modelo evoluciona (lección 2) y proteger la señal de la suite (lección 6). Una fixture que ahorra dos líneas por test pero convierte doscientas ediciones futuras en una vale muchísimo, aunque el ahorro de escritura parezca chico.

Ejercicios

Ejercicio 1 — Aplica el tercer duplicado. Para cada situación, di si extraerías una fixture/helper ahora, esperarías, o no lo harías nunca, y por qué. (a) Acabas de escribir el primer test que construye un Calendar con tres reservas. (b) Es la segunda vez que copias ese bloque de tres reservas. (c) Es la cuarta vez. (d) Un test único, exploratorio, que vas a borrar cuando entiendas un bug. (e) Un bloque de setup que aparece en 40 tests desde el día uno de una suite nueva.

Ver solución
  • (a) Primera vez: no extraigas. Escríbelo en el test. Todavía no sabes si el patrón se repetirá ni cuál es su parte estable.
  • (b) Segunda vez: cópialo y aguanta. Dos ocurrencias no bastan para ver la forma del patrón; extraer ahora es adivinar la abstracción. Copia y sigue.
  • (c) Cuarta vez: extrae ya. Pasaste el tercer duplicado con holgura; el patrón está claro y el copy-paste ya duele. Va a una fixture (o factory si cada uso varía).
  • (d) Test único y desechable: nunca. No merece infraestructura; lo vas a borrar. Arquitecturar algo efímero es puro costo sin retorno.
  • (e) 40 usos desde el día uno: extrae de inmediato. Aquí el "tercer duplicado" ya se cumplió cuarenta veces antes de empezar. No hay que esperar a copiarlo cuarenta veces para saber que se repite; el patrón es evidente. La regla del tercero es un piso, no un techo: si ves cuarenta usos de entrada, inviertes de entrada.

Ejercicio 2 — Ubica el cruce. Dos suites de Reservo: la suite A tiene 8 tests y su modelo casi nunca cambia (es una librería estable); la suite B tiene 600 tests y su modelo cambia cada dos semanas (es un producto en evolución). Para cada una: (a) ¿de qué lado del cruce está? (b) ¿qué inversión en arquitectura corresponde? (c) ¿qué error sería más probable cometer en cada una?

Ver solución
  • Suite A (8 tests, modelo estable). (a) A la izquierda del cruce: pocos tests, poca frecuencia de cambio, o sea N y C bajos. (b) Inversión mínima: quizá una o dos fixtures si hay setup repetido tres veces, pero nada de factories elaboradas ni marcadores. Copy-paste moderado es aceptable aquí. (c) El error probable es sobre-invertir: montar un framework completo para ocho tests que casi no cambian es la ojaladora para tres prendas.
  • Suite B (600 tests, modelo volátil). (a) A la derecha del cruce, muy lejos: N alto y C alto, o sea el multiplicador N × C es enorme. (b) Inversión fuerte y temprana: fixtures para todo el setup compartido, factories para las variaciones, marcadores para correr subconjuntos, config por entorno. (c) El error probable es sub-invertir: dejar setup copiado en una suite que cambia cada dos semanas es acumular la deuda de la lección 6, con rojos masivos rutinarios y erosión de confianza.

La lección: la misma cantidad de arquitectura es sobre-inversión en A y sub-inversión en B. No hay una respuesta correcta en abstracto; hay una respuesta correcta para el punto en la curva donde está tu suite, y ese punto lo fija el producto N × C.

Ejercicio 3 — Cuenta el retorno completo, no solo las líneas. Una fixture focus_room ahorra 1 línea de setup por test. La suite tiene 300 tests que la usan, y en el próximo año el modelo Room cambiará 5 veces. (a) ¿Cuántas líneas de escritura ahorra la fixture (el retorno visible)? (b) ¿Cuántas ediciones de mantenimiento futuro ahorra (el retorno grande)? (c) Si alguien argumenta "solo ahorra una línea por test, no vale la pena", ¿qué está ignorando?

Ver solución
  • (a) 299 líneas de escritura, aproximadamente. La sala se define una vez en la fixture en vez de 300 veces en los tests: ahorras las 300 construcciones menos la única definición. Es real, pero es el retorno menor.
  • (b) 1495 ediciones de mantenimiento (5 cambios × 299 lugares que no tienes que tocar). Sin la fixture, cada uno de los 5 cambios de Room obligaría a editar 300 líneas: 1500 ediciones frágiles en el año. Con la fixture, cada cambio es 1 edición: 5 en total. El ahorro es de ~1495 ediciones, y cada una evitada es también un error de omisión evitado.
  • (c) Ignora el retorno grande y el retorno invisible. "Una línea por test" mide solo la escritura inicial (retorno menor, (a)). No cuenta las ~1495 ediciones de mantenimiento que la fixture ahorra (b), ni la protección de la confianza —esos 5 cambios, sin fixture, producirían rojos masivos que erosionan la señal de la suite (lección 6)—. El retorno de una fixture casi nunca está en escribir menos hoy; está en cambiar en un lugar mañana y en mantener la suite creíble. Medir solo las líneas de hoy es como valuar la ojaladora por lo que ahorra en la primera prenda.

Resumen y siguiente paso

En esta lección hiciste las cuentas de la arquitectura. Aprendiste que el framework también cuesta y que sobre-invertir es un error tan real como sub-invertir —la ojaladora para tres prendas es tan equivocada como coser a mano trescientas—. Viste la curva de costo: dos líneas que se cruzan, la sin-arquitectura barata al principio pero de pendiente creciente (O(N × C)), la con-arquitectura un poco más cara al principio pero casi plana (O(C)); toda la habilidad es no quedarte del lado equivocado del cruce. Tienes la regla operativa —el tercer duplicado: escribe, copia, extrae— que acierta casi siempre y te protege en las dos direcciones. Y viste el retorno ejecutado: una vez montado el framework, cada test nuevo cuesta una línea (8 passed con dos tests agregados gratis) y un cambio de modelo se hace en un solo lugar (8 passed tocando solo la fixture). El retorno grande no es escribir menos hoy; es cambiar en un punto mañana y proteger la confianza en la suite.

Antes de avanzar deberías poder: dibujar la curva de costo con sus dos líneas y explicar el cruce; aplicar la regla del tercer duplicado en casos concretos; reconocer las señales de sobre-inversión (fixtures de un solo uso, marcadores que no filtran, abstracción antes del tercer uso); y calcular el retorno completo de una fixture —escritura, mantenimiento futuro y confianza—, no solo las líneas ahorradas.

Lo que sigue es dejar la teoría y hacer la inversión más pequeña y rentable de todas con tus manos. El mini-proyecto de la lección 8 te entrega la suite de Reservo con setup duplicado en seis tests —la misma que en la lección 2 se rompía en seis lugares— y te pide extraer su primera fixture compartida a un conftest.py, sin cambiar lo que cada test afirma. La correrás hasta ver 6 passed, le aplicarás el cambio de modelo que antes rompía seis lugares para arreglarlo en uno, y entregarás las dos salidas lado a lado: el "antes" y el "después" del módulo entero, medidos con pytest.

Recursos