Módulo 3: Organizar la suite: capas y estructura de carpetas
2. Por qué importa la forma de la suite
Descripción
Al terminar esta lección vas a poder nombrar, uno por uno, los costos concretos de una suite sin estructura —no como una queja vaga de "está desordenada", sino como problemas específicos que se sienten en el día a día y que la estructura de carpetas resuelve—. En la lección anterior viste la suite plana de Reservo y su contraste con la suite en capas. Aquí vamos a quedarnos en el "antes" y examinarlo con lupa: qué exactamente no puedes hacer cuando doce tests viven apilados en un solo directorio, y por qué ese costo no es una molestia fija que pagas una vez, sino una deuda que crece con cada archivo que agregas.
Esto importa porque la estructura es de esas inversiones que parecen innecesarias hasta el día en que son urgentes, y para entonces cuesta el triple hacerlas. Con diez tests, el directorio plano funciona: los abres todos, los corres todos, y punto. El problema no aparece de golpe; se acumula. Un día quieres correr solo los tests rápidos mientras desarrollas —para no esperar los lentos cada vez— y descubres que no tienes cómo pedírselo a la carpeta. Otro día buscas el test que cubre el reembolso y tienes que abrir cinco archivos para encontrarlo. Otro día llega alguien nuevo, abre tests/ esperando entender el sistema, y solo ve una lista de nombres. Cada uno de esos días es barato por sí solo; sumados a lo largo de un año y multiplicados por todo el equipo, son la diferencia entre una suite que ayuda y una que estorba. Esta lección hace visible esa deuda antes de que la pagues.
Conexión con el módulo: esta lección es el diagnóstico que justifica todo lo que sigue. Nombra tres costos del directorio plano —no puedes correr por partes, no encuentras dónde vive un test, no ves la forma del sistema— y cada uno anticipa una solución del módulo. Correr por partes es la lección 5 (capas físicas que corres por separado). Encontrar dónde vive un test es la lección 3 (agrupar por capa o por feature) y la 7 (la estructura como mapa). Ver la forma del sistema es la lección 7 (la estructura como documentación). Aquí solo diagnosticamos; el tratamiento viene después. Y recuerda la frontera: seguimos hablando de carpetas, no de marcadores —esos son el módulo 4—.
La caja de herramientas volcada en un balde
Piénsalo así. Un carpintero tiene sus herramientas. Puede guardarlas de dos maneras. La primera: todas en un balde. Martillos, destornilladores, brocas, clavos, la cinta métrica, las lijas —todo adentro, revuelto—. Cuando el balde tiene cinco cosas, buscar el martillo es trivial: metes la mano y ahí está. El balde funciona. Pero el taller crece, llegan herramientas nuevas, y un día el balde tiene ochenta cosas. Ahora buscar el destornillador de estrella chico es vaciar medio balde en el piso. Y si te pidieran "tráeme solo las brocas", tendrías que sacar todo y separar a mano, porque en el balde no hay "sección de brocas": hay un revoltijo.
La segunda manera: una caja con compartimentos y una tabla de pared con siluetas. Los martillos en su gancho, las brocas en su cajón, los destornilladores ordenados por tamaño. Buscar el destornillador de estrella chico es ir a su lugar. "Tráeme solo las brocas" es abrir un cajón. Y cuando llega una herramienta nueva, hay un lugar obvio donde va —o si no lo hay, esa ausencia te dice que necesitas un compartimento nuevo, lo cual también es información útil—. La tabla de pared, además, le dice a cualquiera que entre al taller qué herramientas hay y cómo trabaja este carpintero, sin que él tenga que explicar nada.
Las dos guardan las mismas herramientas. La diferencia es lo que cuesta usarlas: encontrar una, sacar un subconjunto, y entender el taller de un vistazo. El balde escala pésimo —cada herramienta nueva empeora la búsqueda de todas las demás—; la caja con compartimentos escala bien —cada herramienta nueva va a su lugar y no estorba a las otras—. Una suite de tests en un directorio plano es el balde. Este módulo construye la caja con compartimentos. Y esta lección mide, con la suite de Reservo, exactamente cuánto cuesta el balde.
Los tres costos del directorio plano
Volvamos a la suite plana de Reservo: los doce tests de la lección 1, apilados en un solo directorio tests/. Vamos a examinar tres costos, del más operativo al más humano.
Costo 1: no puedes correr solo un subconjunto
Este es el que muerde primero, porque lo pagas cada vez que desarrollas. Mientras trabajas en el código de precios, quieres correr solo los tests de lógica pura —rápidos, aislados— una y otra vez, sin arrastrar en cada corrida los tests de integración que arman el BookingService completo. En una suite en capas, eso es un comando: pytest tests/unit. En el balde plano, no hay una carpeta "lo rápido", así que la única forma de correr un subconjunto es nombrar los archivos uno por uno:
python3 -m pytest tests/test_overlaps.py tests/test_pricing.py tests/test_refund.py
Qué esperar. En mi máquina (Python 3.14.0, pytest 9.1.1):
....... [100%]
7 passed in 0.01s
Funciona —corre los 7 tests rápidos—, pero mira lo que tuviste que escribir: los tres nombres de archivo, a mano. Y aquí está el problema que no se ve en el resultado verde: esa lista es frágil. Mañana agregas tests/test_availability.py, otro test rápido. Si olvidas sumarlo a la lista —y lo vas a olvidar—, tu comando de "correr lo rápido" silenciosamente deja de correrlo. No hay error, no hay aviso: simplemente ese test deja de ejecutarse en tu ciclo de desarrollo, y te enteras cuando ya rompiste algo. La carpeta plana no tiene forma de decir "todos los rápidos"; solo tiene archivos sueltos que tú tienes que recordar enumerar. Compáralo con la versión en capas, donde el mismo subconjunto es una carpeta:
python3 -m pytest tests/unit
....... [100%]
7 passed in 0.01s
Mismo resultado, 7 tests, pero ahora tests/unit es el subconjunto: cualquier archivo nuevo que pongas ahí entra automáticamente, y ninguno que no debería entrar se cuela. La carpeta hace el trabajo que en el balde hacías a mano y mal.
Costo 2: no encuentras dónde vive un test
El segundo costo lo pagas cada vez que buscas algo. Supón que un compañero reporta que el reembolso a 36 horas devuelve mal. Quieres abrir el test que cubre ese caso. En el balde plano, ¿cuál de los cinco archivos es? test_pricing.py suena a precios, no a reembolsos... ¿o el reembolso está ahí porque "también es dinero"? ¿O en test_cancel_flow.py, porque cancelar dispara el reembolso? ¿O hay un test_refund.py? En un directorio plano de cinco archivos todavía lo resuelves abriendo un par. Pero la suite plana de un proyecto real no tiene cinco archivos: tiene cuarenta, con nombres que se acumularon sin plan —test_pricing.py, test_pricing2.py, test_new_pricing.py, test_pricing_fixed.py—, y encontrar el test correcto es la expedición al galpón.
La estructura resuelve esto dándole a cada test un lugar predecible. Si la suite está por feature, el reembolso vive en tests/pricing/ (o tests/refunds/); vas ahí y está. Si está por capa, sabes que refund_cents es lógica pura, así que vive en tests/unit/, y ahí lo buscas. En los dos casos, la forma de la suite te dice dónde mirar antes de abrir un solo archivo. El balde no te dice nada: cada búsqueda empieza de cero.
Costo 3: no ves la forma del sistema
El tercer costo es el más silencioso y el más caro a largo plazo. Llega alguien nuevo al equipo —o eres tú mismo, volviendo al proyecto seis meses después—. Abres tests/ para entender cómo funciona Reservo, porque los tests son la mejor documentación viva que hay: dicen qué hace el sistema y qué reglas cumple. En el balde plano, lo que ves es esto:
tests/
├── conftest.py
├── test_booking_flow.py
├── test_cancel_flow.py
├── test_overlaps.py
├── test_pricing.py
└── test_refund.py
Una lista de nombres. Te dice que hay algo de reservas, algo de precios, algo de cancelaciones —los nombres ayudan—, pero no te dice cómo se relacionan, cuáles son el núcleo lógico y cuáles la maquinaria que los ata, ni por dónde empezar a leer. La forma es plana, así que no comunica jerarquía ni capas: todo parece igual de importante y del mismo tipo. Compáralo con lo que ve el recién llegado en la suite en capas:
tests/
├── conftest.py
├── integration/
│ ├── conftest.py
│ ├── test_booking_flow.py
│ └── test_cancel_flow.py
└── unit/
├── conftest.py
├── test_overlaps.py
├── test_pricing.py
└── test_refund.py
Esta forma enseña. En diez segundos, sin abrir un archivo, el recién llegado aprende: Reservo se prueba en dos capas; hay un núcleo de lógica pura (unit/: solape, precio, reembolso) y una capa de piezas que colaboran (integration/: los flujos de reservar y cancelar). La estructura le contó la arquitectura del sistema. Ese es el costo escondido del balde: no es solo incómodo para ti hoy, es que no le enseña nada a nadie, y una suite que no enseña desperdicia la mejor documentación que un proyecto puede tener.
Ejemplo trabajado: la lista contra el árbol, medido
Los tres costos tienen una raíz común: el directorio plano no tiene jerarquía, y sin jerarquía no hay ni subconjuntos, ni lugares predecibles, ni forma que enseñe. Podemos verlo directamente con --collect-only, que dibuja la jerarquía —o su ausencia—. Primero, el balde:
python3 -m pytest tests --collect-only -q
Qué esperar, sobre la suite plana (uso -q para ver solo la lista, sin el árbol indentado):
tests/test_booking_flow.py::test_booking_a_room_charges_the_pro_price
tests/test_booking_flow.py::test_room_is_unavailable_after_it_is_booked
tests/test_booking_flow.py::test_double_booking_the_same_slot_is_rejected
tests/test_cancel_flow.py::test_cancelling_72h_ahead_refunds_in_full
tests/test_cancel_flow.py::test_cancelling_frees_the_room
tests/test_overlaps.py::test_touching_intervals_do_not_overlap
tests/test_overlaps.py::test_nested_interval_overlaps
tests/test_pricing.py::test_basic_member_pays_hourly_rate_times_hours
tests/test_pricing.py::test_pro_member_gets_twenty_percent_off
tests/test_refund.py::test_full_refund_at_72h
tests/test_refund.py::test_half_refund_at_36h
tests/test_refund.py::test_no_refund_at_12h
12 tests collected in 0.00s
Doce líneas, todas con el mismo prefijo tests/. No hay agrupación: los tests de integración (test_booking_flow, test_cancel_flow) y los unitarios (test_overlaps, test_pricing, test_refund) se intercalan sin distinción. Para "ver lo rápido" tendrías que leer los nombres uno por uno y clasificar en tu cabeza. Ahora el árbol indentado de la misma suite plana, que hace la falta de jerarquía todavía más evidente:
python3 -m pytest tests --collect-only
<Dir tests>
<Module test_booking_flow.py>
<Function test_booking_a_room_charges_the_pro_price>
<Function test_room_is_unavailable_after_it_is_booked>
<Function test_double_booking_the_same_slot_is_rejected>
<Module test_cancel_flow.py>
<Function test_cancelling_72h_ahead_refunds_in_full>
<Function test_cancelling_frees_the_room>
<Module test_overlaps.py>
<Function test_touching_intervals_do_not_overlap>
<Function test_nested_interval_overlaps>
<Module test_pricing.py>
<Function test_basic_member_pays_hourly_rate_times_hours>
<Function test_pro_member_gets_twenty_percent_off>
<Module test_refund.py>
<Function test_full_refund_at_72h>
<Function test_half_refund_at_36h>
<Function test_no_refund_at_12h>
El árbol tiene exactamente dos niveles: <Dir tests> y sus cinco <Module>. Es un abanico plano. No hay un <Dir> intermedio que agrupe, porque en el disco no hay subcarpetas. La profundidad del árbol es la medida de la organización: dos niveles es un balde; tres o más es una caja con compartimentos. El módulo entero se trata de ganar ese tercer nivel con criterio.
Por qué el costo crece (y no es lineal)
Un detalle que conviene entender bien, porque es lo que hace urgente la estructura: el costo del balde no es fijo, y ni siquiera crece parejo. Crece más rápido que la suite.
Pensemos en el costo de "encontrar un test". Con 5 archivos, en el peor caso abres 5. Con 40 archivos, en el peor caso abres 40. Pero además, con más archivos hay más nombres parecidos, más duplicación acumulada (test_pricing2.py), y más probabilidad de que el test que buscas esté en un lugar contraintuitivo. Cada archivo nuevo no solo suma su propio peso: empeora la búsqueda de todos los demás, porque agranda el revoltijo en el que todos viven. Es el balde: la herramienta número ochenta no solo es difícil de encontrar ella misma; hace más difícil encontrar las otras setenta y nueve.
La estructura rompe esa curva. En una suite en capas, agregar un test de reembolso a tests/unit/test_refund.py no toca en nada la capacidad de encontrar los tests de integración: viven en otra rama del árbol, en otro cajón. Cada compartimento acota el revoltijo a su propio contenido. Por eso la inversión en estructura, que a los diez tests parece exagerada, es exactamente lo que mantiene la suite navegable cuando llega a mil: no elimina el crecimiento, pero lo vuelve local. Un archivo nuevo pesa solo lo suyo, no lo de todos.
Errores comunes
Esperar a que duela para estructurar (de oportunidad). Qué pasa: el equipo deja la suite plana "porque todavía se maneja" y planea reorganizar "cuando haga falta". Por qué pasa: con pocos tests el balde de verdad funciona, y la reorganización se siente como trabajo sin recompensa inmediata. Cómo detectarlo: el momento en que "hace falta" es justo el momento en que reorganizar cuesta más —cuarenta archivos revueltos, con fixtures mezcladas y dependencias tácitas—, así que si esperas al dolor, pagas la estructura en su versión más cara. Cómo corregirlo: la regla práctica es estructurar temprano y barato —dos carpetas, unit/ e integration/, desde que la suite pasa de un puñado de archivos—; es trivial hacerlo con diez tests y una pesadilla con doscientos.
Confundir "los tests pasan" con "la suite está bien" (de criterio). Qué pasa: alguien mira 12 passed y concluye que no hay nada que mejorar. Por qué pasa: el verde es la señal fuerte y satisfactoria de siempre, y es fácil creer que es la única que importa. Cómo detectarlo: los tres costos de esta lección —no correr por partes, no encontrar un test, no ver la forma— conviven perfectamente con 12 passed; el balde plano pasa todos sus tests. El verde mide si los tests son correctos, no si la suite es navegable. Cómo corregirlo: agrega a tu criterio de "suite sana" las preguntas de esta lección: ¿puedo correr solo una parte? ¿encuentro rápido dónde vive algo? ¿la forma le enseña el sistema a alguien nuevo? Si alguna es "no", hay trabajo aunque todo esté verde.
Resolver el subconjunto con nombres de archivo a mano (de método). Qué pasa: en vez de crear carpetas, alguien memoriza o guarda en un alias la lista de archivos "rápidos" y la pasa a mano a pytest. Por qué pasa: parece más rápido que reorganizar, y funciona el primer día. Cómo detectarlo: en cuanto agregas un archivo rápido nuevo y olvidas sumarlo a la lista, tu "correr lo rápido" deja de correrlo sin avisar —un test que existe pero que tu ciclo de desarrollo ya no ejecuta—. Cómo corregirlo: deja que la carpeta defina el subconjunto. pytest tests/unit incluye automáticamente todo lo que pongas ahí; no hay lista que mantener ni archivo que se te olvide. La estructura hace el trabajo que la lista a mano hace mal.
Ejercicios
Ejercicio 1 — Nombra el costo. Para cada situación, di cuál de los tres costos del directorio plano estás pagando —(1) no correr subconjuntos, (2) no encontrar un test, (3) no ver la forma— y cómo lo curaría la estructura. (a) Estás depurando el cálculo de precio y esperas ocho segundos en cada corrida porque los tests de integración corren junto con los de precio. (b) Un colega nuevo pregunta "¿por dónde empiezo a entender esto?" y le respondes "abre los archivos y ve leyendo". (c) Reportan un bug en el reembolso y abres tres archivos antes de dar con el test correcto.
Ver solución
- (a) Costo 1: no correr subconjuntos. Estás pagando el tiempo de los tests de integración en cada corrida porque no puedes aislar los de precio. La cura: una capa física (
tests/unit/) que corras sola conpytest tests/unit, dejando la integración fuera del ciclo rápido. - (b) Costo 3: no ver la forma. El recién llegado no puede aprender el sistema de la estructura porque la estructura es plana y no enseña nada; por eso lo mandas a leer archivos. La cura: una forma en capas (o por feature) que, con
--collect-onlyo un simplels, le muestre la arquitectura antes de abrir código. - (c) Costo 2: no encontrar un test. El reembolso no tiene un lugar predecible, así que la búsqueda es una expedición por varios archivos. La cura: un lugar obvio para cada test —
tests/unit/test_refund.pysi organizas por capa,tests/pricing/si por feature— de modo que sepas dónde mirar sin abrir nada.
La regla que estás aplicando: cada dolor cotidiano del balde plano corresponde a uno de los tres costos, y cada costo tiene una cura estructural concreta. Diagnosticar bien el costo te dice qué parte de la estructura necesitas.
Ejercicio 2 — Predice la fragilidad. En la suite plana, corres tus tests rápidos así: pytest tests/test_overlaps.py tests/test_pricing.py tests/test_refund.py. Mañana agregas tests/test_availability.py, otro test de lógica pura rápida, pero olvidas actualizar tu comando. ¿Qué pasa exactamente? ¿Te avisa pytest? Ahora describe qué habría pasado si la suite estuviera en capas y hubieras puesto el archivo en tests/unit/.
Ver solución
Con la lista a mano: tu comando pytest tests/test_overlaps.py tests/test_pricing.py tests/test_refund.py sigue corriendo 7 tests —los de siempre—, ignorando por completo test_availability.py. Pytest no te avisa: no hay error, no hay advertencia. Tú nombraste tres archivos, pytest corre esos tres, y hace exactamente lo que le pediste. El test nuevo existe en el disco, pero tu ciclo de desarrollo dejó de ejecutarlo, y no te enteras hasta que rompes la disponibilidad y ningún test rojo te lo dice —porque el que lo cubría nunca corrió—. Es un silencio peligroso: la ausencia de cobertura no genera ninguna señal.
Con la suite en capas y el archivo en tests/unit/: pytest tests/unit descubre automáticamente test_availability.py, porque le pediste "corre la carpeta unit", no "corre estos archivos". Sin tocar tu comando, el subconjunto rápido pasa a tener 9 tests en vez de 7. La carpeta es el subconjunto, así que cualquier archivo que caiga dentro entra solo. No hay lista que mantener ni olvido posible. Esa es la diferencia de fondo: nombrar archivos es enumerar; una carpeta es una regla ("todo lo que esté aquí"), y las reglas no se olvidan de las cosas nuevas.
Ejercicio 3 — Mide la profundidad. El texto dice que "la profundidad del árbol de --collect-only es la medida de la organización": dos niveles es un balde, tres o más es una caja con compartimentos. Cuenta la profundidad (cuántos niveles de <Dir>/<Module>/<Function>) en el árbol plano y en el árbol en capas de la lección 1. Luego responde: si organizaras Reservo por feature con tests/pricing/ y tests/booking/, ¿qué profundidad tendría? ¿Sería también una caja con compartimentos?
Ver solución
En el árbol plano, los niveles son: <Dir tests> → <Module ...> → <Function ...>. Tres tipos de nodo, pero un solo nivel de carpeta (tests). Como jerarquía de directorios es plana: no hay ninguna carpeta dentro de tests. Es el balde.
En el árbol en capas, los niveles son: <Dir tests> → <Dir unit>/<Dir integration> → <Module ...> → <Function ...>. Hay dos niveles de carpeta (tests y luego unit/integration). Ese segundo nivel de directorio es el compartimento; es lo que convierte el balde en caja.
Si organizaras por feature con tests/pricing/ y tests/booking/, tendrías la misma profundidad de directorios que la versión en capas: tests/ → pricing//booking/ → módulos → funciones. También sería una caja con compartimentos —igual de navegable—, solo que los compartimentos estarían rotulados por área del dominio (precio, reservas) en vez de por capa (rápido, lento). Las dos ganan el nivel de agrupación que el balde no tiene; lo que cambia es el criterio del rótulo. Cuál rótulo conviene —capa o feature— es exactamente la decisión de la lección 3.
Resumen y siguiente paso
En esta lección diagnosticaste, con nombre y apellido, los tres costos del directorio plano. Costo 1: no puedes correr un subconjunto sin nombrar archivos a mano, y esa lista es frágil —un archivo nuevo que olvidas agregar deja de correrse en silencio—. Costo 2: no encuentras dónde vive un test, porque el balde no le da a nada un lugar predecible. Costo 3: no ves la forma del sistema, porque una estructura plana no enseña arquitectura a quien llega nuevo. Viste la raíz común de los tres —la falta de jerarquía, medible como la profundidad del árbol de --collect-only— y por qué el costo crece más rápido que la suite: cada archivo nuevo empeora el revoltijo de todos, mientras que en una suite con compartimentos cada archivo pesa solo lo suyo.
La analogía que lo sostiene: el balde de herramientas contra la caja con compartimentos. Las dos guardan lo mismo; la diferencia es lo que cuesta encontrar una herramienta, sacar un subconjunto y entender el taller de un vistazo.
Antes de avanzar deberías poder: nombrar los tres costos del directorio plano y dar un ejemplo de cada uno; explicar por qué una lista de archivos a mano es más frágil que una carpeta; y medir la organización de una suite por la profundidad de su árbol de directorios.
Lo que sigue es la primera gran decisión de diseño: con qué criterio agrupas. En la lección 3 vas a ver los dos grandes esquemas —organizar por capa (tests/unit/, tests/integration/), que agrupa por cómo es el test, o por feature (tests/pricing/, tests/booking/), que agrupa por de qué trata—, el trade-off de cada uno ejecutado sobre Reservo, y por qué muchas suites terminan combinando los dos. Es elegir el rótulo de los compartimentos que esta lección demostró que necesitas.
Recursos
- pytest — How to invoke pytest — la referencia de cómo pedirle a pytest un archivo, varios archivos o una carpeta; la base de los subconjuntos que aquí corrimos a mano y que la estructura vuelve un solo comando.
- pytest — Good Integration Practices — las convenciones oficiales de estructura de proyecto; el estándar contra el que se mide el "balde plano" de esta lección.
- pytest — Collection of test files and directories — la referencia de las opciones que gobiernan qué colecciona pytest y desde dónde; útil para entender que el árbol de
--collect-onlyrefleja exactamente la de tus directorios (lo abrimos a fondo en la lección 6).