Módulo 4: Marcadores y configuración: pytest.ini y markers
8. Mini-proyecto: marca y configura la suite de Reservo
Descripción
Al terminar esta lección vas a haber juntado todo el módulo en una entrega real: tomar la suite de Reservo del módulo 3 —trece tests en capas, sin marcar y sin configuración central— y construirle la capa de configuración completa. Vas a diseñar el vocabulario de marcadores, pegarlos a los tests (con pytestmark a nivel de módulo donde toca), registrarlos en pyproject.toml, activar --strict-markers, y configurar addopts = "-m smoke" para que la suite corra la de humo por defecto y la lenta a demanda. El entregable es doble: la suite marcada y el contrato de configuración que la gobierna, verificado con las expresiones -m que dominaste en la lección 7.
Esto importa porque es la primera vez en el módulo que haces el recorrido entero sin que nadie te dé las piezas ya puestas. Cada lección anterior te mostró un ingrediente aislado —qué es un marcador, cómo registrarlo, qué es addopts—; aquí los ensamblas en el orden correcto sobre una suite cruda, que es exactamente lo que harás en un proyecto real cuando heredes una suite en capas y te toque darle su capa de marcadores y config. Y hay un criterio de "terminado" honesto que este proyecto entrena: la capa de configuración no está lista cuando la suite pasa —una suite sin marcar también pasa—, sino cuando pytest a secas corre solo la de humo, cuando pytest -m slow corre solo los lentos, cuando un typo de marcador rompe la colección, y cuando el header prueba que el contrato se cargó. Construir bien es que cada uno de esos comportamientos ocurra y lo verifiques ejecutando.
Conexión con el módulo: esta lección es la síntesis. Cada paso activa una anterior: diseñar el vocabulario es la lección 3; pegar con pytestmark es la lección 3; registrar y --strict-markers es la lección 4; escribir el contrato (testpaths, markers) es la lección 5; fijar addopts = "-m smoke" es la lección 6; verificar con expresiones -m es la lección 7. Y cierra el módulo con una pieza entregable. Recuerda la frontera: aquí el contrato es único —un comportamiento para todo el equipo—; hacerlo variar por entorno (local contra CI) es el módulo 7. Lo que entregas es la base fija sobre la que esa variación se montará después.
El taller al que le montas el tablero de mandos
Piénsalo así, cerrando la metáfora del módulo 3. En aquel mini-proyecto reformaste el taller: montaste la caja con compartimentos —tests/unit/, tests/integration/—, para que las herramientas tuvieran su lugar. El taller quedó navegable, pero mudo: para correr "solo lo rápido" o "solo lo crítico" tenías que saber a mano qué carpeta o qué archivo pedir, y cada persona lo hacía a su manera.
Este mini-proyecto le monta al taller su tablero de mandos. Es el panel en la pared con botones rotulados: un botón "chequeo rápido" que corre la suite de humo, un botón "prueba completa" que corre todo, un botón "flujos lentos" que corre solo los pesados. Detrás de cada botón hay un marcador y una expresión -m; el tablero es la configuración que los conecta y los rotula para que cualquiera —no solo quien montó el taller— pueda operar la suite sin saber la mecánica interna. Marcar los tests es cablear los botones; escribir el contrato es rotularlos y fijar cuál se aprieta por defecto. Al final, pytest a secas es apretar el botón "chequeo rápido", y el taller dejó de ser mudo: te dice cómo se opera, y se opera igual para todos.
El punto de partida: la suite en capas, sin marcar
Esta es la suite que recibes —la del módulo 3, crecida a trece tests, en dos capas, con un conftest.py por carpeta pero sin un solo marcador y sin config central—. El árbol:
reservo/ # el dominio (models, pricing, refunds, calendar, service)
tests/
├── conftest.py # dominio: focus, ana, bruno, monday_9am
├── unit/
│ ├── test_overlaps.py # 2 tests: overlaps() puro
│ ├── test_pricing.py # 3 tests: price_cents() puro
│ └── test_refund.py # 3 tests: refund_cents() puro
└── integration/
├── conftest.py # pesadas: calendar, service
├── test_booking_flow.py # 3 tests: BookingService + Calendar
└── test_cancel_flow.py # 2 tests: BookingService + Calendar + refund
El estado de partida, corriendo la suite tal cual:
python3 -m pytest -q
Qué esperar. En mi máquina (Python 3.14.0, pytest 9.1.1):
............. [100%]
13 passed in 1.24s
Trece verdes en 1.24s. Este es el "antes": una suite que pasa pero muda —no puedes cortarla por criticidad ni por costo, no hay red contra typos de marcador (porque no hay marcadores), y no hay un archivo que diga cómo se corre—. El proyecto es convertir esto en una suite con tablero de mandos, en cinco pasos.
Paso 1: diseña el vocabulario
Antes de tocar código, decide qué etiquetas merece la suite —lección 3, criterio del bibliotecario: corto, ortogonal, cada una con su pytest -m—. Para Reservo, tres:
smoke— el camino crítico feliz, el puñado que corres antes de desplegar. Comando:pytest -m smoke. Dimensión: criticidad.slow— el test cuyo cuerpo tarda, el que quieres poder excluir al desarrollar. Comando:pytest -m "not slow". Dimensión: costo.integration— varias piezas reales juntas (BookingService+Calendar). Comando:pytest -m "integration and smoke"y demás combinaciones. Dimensión: naturaleza.
Tres dimensiones ortogonales, cada una con un comando real. No agregamos important (subjetivo), ni fast (redundante con not slow), ni unit (duplica la carpeta sin ganar combinabilidad) —los rechazos del ejercicio 3 de la lección 3—.
Paso 2: pega los marcadores
Ahora cablea los botones. Dónde va cada etiqueta:
smoke con decorador, en los cuatro críticos. Es selectivo dentro de sus archivos (no todos los tests de precio son de humo), así que va con decorador, uno por uno. En tests/unit/test_pricing.py:
import pytest
from reservo.pricing import price_cents
@pytest.mark.smoke
def test_basic_member_pays_hourly_rate_times_hours(focus, ana):
# 2500 * 3 = 7500 centavos.
assert price_cents(focus, ana, 3) == 7500
@pytest.mark.smoke
def test_pro_member_gets_twenty_percent_off(focus, bruno):
# 7500 - 20% = 6000 centavos.
assert price_cents(focus, bruno, 3) == 6000
def test_zero_hours_costs_nothing(focus, ana):
# Caso de borde, NO camino critico: sin smoke.
assert price_cents(focus, ana, 0) == 0
El tercer test, test_zero_hours_costs_nothing, no lleva smoke a propósito: es un caso de borde, no el camino crítico. Un marcador vale por lo que excluye. Igual, en tests/unit/test_refund.py marcas test_full_refund_at_72h con smoke (el reembolso total es crítico) y dejas los otros dos sin marcar. Y el cuarto smoke es de integración: test_booking_a_room_charges_the_pro_price.
integration con pytestmark, a nivel de módulo. Los dos archivos de tests/integration/ tienen todos sus tests de integración, así que la etiqueta va con pytestmark —una línea que marca el archivo entero y que los tests nuevos heredan solos—. En tests/integration/test_booking_flow.py:
"""Tests de integracion del flujo de reserva: BookingService + Calendar."""
import time
from datetime import timedelta
import pytest
from reservo.service import SlotTakenError
# Todo test de este modulo es de integracion (marca a nivel de modulo).
pytestmark = pytest.mark.integration
@pytest.mark.smoke
def test_booking_a_room_charges_the_pro_price(service, focus, bruno, monday_9am):
booking = service.book(focus, bruno, monday_9am, monday_9am + timedelta(hours=3))
assert booking.price_cents == 6000
assert booking.status == "confirmed"
def test_room_is_unavailable_after_it_is_booked(service, calendar, focus, ana, monday_9am):
start, end = monday_9am, monday_9am + timedelta(hours=3)
service.book(focus, ana, start, end)
assert calendar.is_available(focus.id, start, end) is False
@pytest.mark.slow
def test_double_booking_the_same_slot_is_rejected(service, focus, ana, bruno, monday_9am):
time.sleep(0.4) # simula un flujo lento de integracion
start, end = monday_9am, monday_9am + timedelta(hours=3)
service.book(focus, ana, start, end)
with pytest.raises(SlotTakenError):
service.book(focus, bruno, start, end)
Fíjate en la acumulación: test_booking_a_room_charges_the_pro_price lleva integration (heredado del pytestmark) y smoke (decorador propio); test_double_booking_the_same_slot_is_rejected lleva integration y slow. Un test puede tener varias etiquetas, y eso es lo que hace posibles las combinaciones (integration and smoke). El otro archivo de integración, test_cancel_flow.py, lleva el mismo pytestmark = pytest.mark.integration, y sus dos tests de cancelación llevan además @pytest.mark.slow (su cuerpo tarda).
El reparto final de las etiquetas: 4 smoke (dos de precio, uno de reembolso, uno de reserva), 3 slow (uno de reserva, dos de cancelación), 5 integration (los cinco de la carpeta, por pytestmark).
Paso 3: registra los marcadores y activa strict
Con las etiquetas puestas, la suite ya funciona pero corre con el PytestUnknownMarkWarning en cada test —y sin red contra typos—. Ciérralo con el contrato: crea pyproject.toml en la raíz, registra los tres marcadores, y (en el paso 4) activa --strict-markers. Empecemos con el registro y las opciones base de la lección 5:
# pyproject.toml — el contrato de Reservo (paso 3)
[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
"smoke: el camino critico feliz; se corre antes de desplegar.",
"slow: el test tarda; se excluye con -m 'not slow' al desarrollar.",
"integration: arma varias piezas reales de Reservo juntas.",
]
Con esto, los warnings desaparecen (los tres marcadores son conocidos) y testpaths fija que pytest a secas empiece por tests/. Verifícalo con pytest --markers, que ahora lista tu vocabulario documentado.
Paso 4: fija los defaults con addopts
Ahora el tablero de mandos: qué corre pytest a secas, y qué garantías van siempre. Agrega addopts con --strict-markers (la red de typos, lección 4), -ra (resumen útil, lección 6) y -m smoke (la suite de humo por defecto, lección 6):
# pyproject.toml — el contrato completo de Reservo
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers -m smoke"
markers = [
"smoke: el camino critico feliz; se corre antes de desplegar.",
"slow: el test tarda; se excluye con -m 'not slow' al desarrollar.",
"integration: arma varias piezas reales de Reservo juntas.",
]
Este es el contrato final: siete líneas que definen cómo corre la suite para todo el equipo. pytest a secas corre la de humo con la red estricta puesta; cualquier otra selección se pide en la línea de comandos, y ese -m gana sobre el -m smoke del contrato (lección 6).
Ejemplo trabajado: el tablero de mandos, botón por botón
Verifiquemos cada botón del tablero. Primero, el botón por defecto —pytest a secas, que debe correr la suite de humo—:
python3 -m pytest
Qué esperar:
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0
rootdir: /path/to/reservo
configfile: pyproject.toml
testpaths: tests
collected 13 items / 9 deselected / 4 selected
...
======================= 4 passed, 9 deselected in 0.02s ========================
Lee el header como recibo (lección 5): configfile: pyproject.toml prueba que el contrato se cargó, testpaths: tests que la clave se aplicó, y collected 13 items / 9 deselected / 4 selected que el -m smoke de addopts filtró la suite de humo sin que teclearas ningún -m. Cuatro tests críticos, en 0.02s: el botón "chequeo rápido" funciona, y es lo más simple del mundo de apretar —pytest—.
Segundo botón —los lentos a demanda, pytest -m slow—:
python3 -m pytest -m slow
Qué esperar:
======================= 3 passed, 10 deselected in 1.22s =======================
Tres tests lentos, en 1.22s (corrieron de verdad, por eso el segundo largo). Tu -m slow venció al -m smoke de addopts —el último gana—. El botón "flujos lentos" funciona.
Tercer botón —la suite completa, que con un -m smoke por defecto hay que pedir explícito—:
python3 -m pytest -m ""
Qué esperar:
============================== 13 passed in 1.22s ==============================
Trece verdes: la suite entera. El -m "" (expresión vacía) no filtra nada, así que sobrescribe el -m smoke de addopts y corre todo. Este es el botón "prueba completa", y es el que hay que documentar, porque no es obvio que con un default de humo se corra todo con -m "". Y el botón del desarrollo —pytest -m "not slow", todo menos lo lento—:
python3 -m pytest -m "not slow"
Qué esperar:
======================= 10 passed, 3 deselected in 0.01s =======================
Diez tests en 0.01s: la red rápida que corres a cada rato mientras programas. Cuatro botones, cuatro comportamientos, todos verificados ejecutando.
La prueba de que la red está puesta
Falta comprobar la garantía silenciosa: que un typo de marcador rompe la colección, sin que nadie teclee --strict-markers (viene de addopts). Simula el dedazo —cambia un @pytest.mark.smoke por @pytest.mark.smoek— y corre pytest a secas:
python3 -m pytest
Qué esperar:
collected 10 items / 1 error
==================================== ERRORS ====================================
_________________ ERROR collecting tests/unit/test_pricing.py __________________
'smoek' not found in `markers` configuration option
=========================== short test summary info ============================
ERROR tests/unit/test_pricing.py - Failed: 'smoek' not found in `markers` con...
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
=============================== 1 error in 0.06s ===============================
El typo detuvo la colección con 'smoek' not found in markers configuration option, y ni siquiera tecleaste --strict-markers —lo llevaba puesto el contrato—. Esta es la prueba de que el tablero incluye su fusible: el mismo smoek que en el módulo sin config habría tirado un test crítico de la suite de humo en silencio, aquí es una pared que no deja avanzar. Arreglas smoek → smoke y la suite vuelve a correr.
Tu entrega
El proyecto te pide dos entregables, y cada uno ejercita una mitad del módulo:
- La suite marcada. Toma la suite de Reservo en capas (la de esta lección, o la que armaste en el mini-proyecto del módulo 3 crecida a trece tests) y pégale el vocabulario de tres marcadores:
smokecon decorador en los cuatro críticos,slowcon decorador en los tres que tardan,integrationconpytestmarken los dos archivos de la capa. El reparto correcto es 4 smoke, 3 slow, 5 integration, con acumulación donde toca (el flujo de reserva esintegrationysmoke; los de cancelación sonintegrationyslow). - El contrato de configuración. Escribe el
pyproject.tomlcon[tool.pytest.ini_options]:testpaths = ["tests"], los tres marcadores registrados enmarkers, yaddopts = "-ra --strict-markers -m smoke"para que la suite corra la de humo por defecto, con la red estricta puesta y el resumen útil.
Y la verificación, que es la mitad del entregable —una capa de config sin verificar no está terminada—. Captura estas cinco corridas:
pytest→4 passed, 9 deselected(el default es la suite de humo; header conconfigfileytestpaths).pytest -m slow→3 passed, 10 deselecteden ~1.2s (los lentos a demanda; el-mde la línea de comandos gana).pytest -m ""→13 passed(la suite completa, sobrescribiendo el default).pytest -m "not slow"→10 passed, 3 deselecteden ~0.01s (la red rápida del desarrollo).- Un
smoekde prueba →ERROR ... 'smoek' not found(la red estricta, sin teclear--strict-markers).
Un criterio de "terminado" honesto, en el espíritu de la guía: la capa de config está lista no cuando la suite pasa —la suite sin marcar también pasaba, 13 passed—, sino cuando el tablero de mandos responde: pytest a secas corre la de humo, cada botón -m corta lo que dice, el header prueba que el contrato se cargó, y un typo de marcador rompe la colección en vez de esconder un test. Si esas cinco corridas dan lo esperado, montaste el tablero.
Errores comunes
Registrar los marcadores pero olvidar --strict-markers (de red a medias). Qué pasa: alguien completa el paso 3 (registrar) y da por terminado el contrato, saltándose el --strict-markers de addopts. La suite corre sin warnings y parece lista, pero el hueco del typo sigue abierto —un smoek volvería a caer en silencio—. Por qué pasa: registrar silencia los warnings, y "ya no veo warnings" se confunde con "ya estoy protegido" (el primer error común de la lección 4). Cómo detectarlo: mete un smoek de prueba y corre pytest; si te da passed en vez de un ERROR ... not found, te falta --strict-markers en addopts. Cómo corregirlo: el contrato necesita las dos piezas —markers = [...] y --strict-markers en addopts—. Registrar es la mitad; strict es la que cierra el hueco.
Marcar smoke de más y vaciar su significado (de exceso). Qué pasa: al pegar smoke, alguien lo pone en los tres tests de precio (incluido test_zero_hours_costs_nothing) y en los dos de reembolso "para no dejar críticos afuera", y termina con siete u ocho smoke. Ahora pytest a secas —que corre la de humo— ejecuta media suite, y "humo" ya no significa "el puñado crítico". Por qué pasa: da miedo dejar un test sin la etiqueta, y marcar de más se siente seguro. Cómo detectarlo: si pytest (con -m smoke por default) corre más de cuatro o cinco tests, la suite de humo se infló y perdió su filo. Cómo corregirlo: smoke es el camino crítico feliz, no "todo lo importante". Cuatro tests en Reservo: precio basic, precio pro, reembolso total, flujo de reserva. Los casos de borde (test_zero_hours_costs_nothing, test_half_refund_at_36h) no van; un marcador vale por lo que excluye.
Entregar sin documentar el default de selección (de tablero sin rótulos). Qué pasa: el contrato queda con addopts = "-m smoke" funcionando, pero nadie escribe en ningún lado que pytest corre solo la de humo ni cómo correr todo. El próximo que llega teclea pytest, ve 4 passed, y cree que la suite tiene cuatro tests —o que su test nuevo no corre (el default sorpresa de la lección 6)—. Por qué pasa: el -m smoke de addopts es invisible en el comando, así que actúa por detrás. Cómo detectarlo: si alguien pregunta "¿por qué mi test no aparece?" o "¿por qué solo corren cuatro?", el default no está documentado. Cómo corregirlo: rotula el tablero. Un comentario en el pyproject.toml y una nota en el README: "pytest corre la suite de humo; usa pytest -m '' para la suite completa, pytest -m slow para los lentos". Un tablero de mandos sin rótulos confunde a quien no lo cableó.
Ejercicios
Ejercicio 1 — Predice el tablero completo. Con el contrato final (addopts = "-ra --strict-markers -m smoke", y la suite marcada 4 smoke / 3 slow / 5 integration), predice cuántos tests corren y la línea de conteo de cada comando. (a) pytest. (b) pytest -m integration. (c) pytest -m "integration and smoke". (d) pytest tests/unit. (e) pytest -m "".
Ver solución
- (a)
pytest→ 4 corren. El-m smokedeaddoptsaplica; corren los cuatro de humo.4 passed, 9 deselected. - (b)
pytest -m integration→ 5 corren. Tu-m integrationgana sobre el-m smokedel contrato; corren los cinco de la carpeta de integración (marcados porpytestmark).5 passed, 8 deselecteden ~1.2s (tres de ellos son lentos). - (c)
pytest -m "integration and smoke"→ 1 corre. La intersección: solotest_booking_a_room_charges_the_pro_pricees integration y smoke.1 passed, 12 deselected. - (d)
pytest tests/unit→ 3 corren. Aquí no tecleaste un-m, así que el-m smokedeaddoptssigue vigente; pasaste una ruta, que se suma al filtro. Pytest coleccionatests/unity de ahí selecciona los smoke: los tres unitarios de humo (basic, pro, full_refund).3 passed. (Punto sutil de la lección 6: una ruta no borra el-mdeaddopts; solo un-mnuevo lo hace.) - (e)
pytest -m ""→ 13 corren. La expresión vacía no filtra nada y sobrescribe el-m smoke; corre la suite entera.13 passeden ~1.2s.
La clave está en (d): pasar una ruta no es pasar un -m, así que el default de selección sigue actuando sobre esa ruta. Combinar ruta y default es un caso real (correr los smoke de una carpeta) que sorprende si no recuerdas cómo se compone addopts.
Ejercicio 2 — Encuentra el marcado mal hecho. Un compañero entrega la suite marcada, pero al verificar salen conteos raros: pytest -m smoke da 7, y pytest -m "integration and slow" da 0 (esperabas 3). Sin ver su código, ¿qué dos errores de marcado explican cada anomalía, y cómo los confirmarías?
Ver solución
-m smokeda 7 en vez de 4 → marcósmokede más. Probablemente puso@pytest.mark.smokeen tests que no son camino crítico —los casos de borde (test_zero_hours_costs_nothing,test_half_refund_at_36h,test_no_refund_at_12h) o los de overlaps—, inflando la suite de humo. Cómo confirmarlo:pytest -m smoke --collect-only -qlista los siete; comparas contra los cuatro críticos correctos (basic, pro, full_refund_72h, booking_charges_pro) y ves cuáles sobran. Cura: quitarsmokede los que no son camino crítico feliz.-m "integration and slow"da 0 en vez de 3 → losslowno heredaronintegration. Los tres tests lentos viven en la carpeta de integración, pero si su archivo no tienepytestmark = pytest.mark.integration(o el compañero marcóslowcon decorador pero olvidó elpytestmarkdel módulo), entonces sonslowpero nointegration, así que la intersección es vacía. Cómo confirmarlo:pytest -m integration --collect-only -q—si da menos de 5, falta elpytestmarken algún archivo de integración; sipytest -m slowda 3 perointegration and slowda 0, los slow existen pero sin la etiquetaintegration. Cura: agregarpytestmark = pytest.mark.integrationa nivel de módulo en los dos archivos detests/integration/.
La técnica general: cuando un conteo no cuadra, --collect-only -q te da la lista de lo seleccionado, y comparándola con lo esperado ves exactamente qué test está mal etiquetado. Los conteos son el síntoma; la lista es el diagnóstico.
Ejercicio 3 — Cierra el módulo: extiende el contrato con un requisito nuevo. El equipo de Reservo decide dos cosas: (1) que en CI se corra todo menos lo lento (para que el pipeline sea rápido), y (2) que cualquier warning rompa la corrida (rigor). Responde: (a) ¿El requisito (1) va en el pyproject.toml de este módulo, o en otro lado? Justifica con la frontera del módulo. (b) Escribe la línea que agregarías al contrato para el requisito (2). (c) Con esa línea, ¿qué pasaría si alguien deja un @pytest.mark.wip sin registrar? (d) Reflexiona: ¿qué te dio esta capa de configuración que la suite marcada "en crudo" del módulo 3 no daba?
Ver solución
- (a) El requisito (1) NO va en este
pyproject.toml; es config por entorno, que es el módulo 7. "Correr distinto en CI que en local" es exactamente la variación por entorno que la frontera del módulo deja fuera —aquí el contrato es único, un comportamiento igual para todos—. Poner-m "not slow"como default rompería el chequeo de humo local; lo correcto es que CI invoquepytest -m "not slow"en su configuración de pipeline (o, en el módulo 7, una opción--envque cambie el comportamiento). El contrato de este módulo fija el default base; el entorno lo sobrescribe desde afuera. - (b) La línea es
filterwarningsen modo error (lección 5):filterwarnings = ["error"] - (c) La colección fallaría con un error. Con
filterwarnings = ["error"], elPytestUnknownMarkWarningde@pytest.mark.wip(no registrado) se convierte en error y detiene la corrida —un segundo camino al mismo rigor que--strict-markers, pero por la vía de los warnings—. Elwipno registrado rompería igual que un typo, obligando a registrarlo o quitarlo. - (d) Qué te dio la capa de config: un tablero de mandos operable por cualquiera. La suite marcada en crudo del módulo 3 (bueno, sin marcar aún) pasaba, pero era muda y frágil: para cortarla había que saber los comandos a mano, cada persona corría distinto, y un typo de marcador pasaba en silencio. La capa de config le dio (1) cortes con nombre —
pytestes la de humo,-m slowlos lentos, sin recordar rutas—; (2) un comportamiento único para todos —el contrato en un archivo, no en la memoria de cada quien—; (3) una red contra typos —--strict-markersconvierte el dedazo silencioso en una pared—; y (4) documentación viva —pytest --markersy el header prueban qué existe y qué se cargó—. Ese es el salto del módulo: de "trece tests que pasan" a "una suite con un contrato que la gobierna", operable por cualquiera, protegida contra el error humano, e igual para todo el equipo.
Lo que cerraste es la capa de configuración del framework: marcadores como segundo eje, y un contrato central que los registra, los protege, y fija cómo corre la suite. Es una de las cuatro capas de un framework de tests —junto a fixtures (módulo 2), estructura (módulo 3) y las que vienen—, y la que convierte una suite ordenada en una suite gobernada.
Resumen y siguiente paso
En este mini-proyecto juntaste todo el módulo en una entrega real: tomaste la suite de Reservo en capas —trece tests, sin marcar y sin config— y le montaste su tablero de mandos en cinco pasos. Diseñaste el vocabulario (tres marcadores ortogonales, cada uno con su comando), pegaste las etiquetas (smoke con decorador en los cuatro críticos, slow en los tres que tardan, integration con pytestmark en los dos archivos de la capa: 4/3/5 con acumulación donde toca), registraste los marcadores en pyproject.toml, activaste --strict-markers, y fijaste addopts = "-ra --strict-markers -m smoke". Y lo verificaste ejecutando cada botón del tablero: pytest corre la de humo (4 passed, con el header probando configfile y testpaths), pytest -m slow los lentos (3 passed en 1.2s), pytest -m "" la suite completa (13 passed), pytest -m "not slow" la red rápida (10 passed en 0.01s), y un smoek de prueba rompe la colección ('smoek' not found) sin teclear --strict-markers, porque el contrato lo lleva puesto. El taller dejó de ser mudo: te dice cómo se opera, y se opera igual para todos.
Con esto cerraste la capa de configuración del framework: marcadores como segundo eje de categorización, y el archivo central como el contrato que los registra, los protege con la revisión estricta, y fija cómo corre la suite. Ya sabes pasar de una suite ordenada en carpetas a una suite gobernada por un contrato.
Lo que sigue en la guía es el módulo 5: la biblioteca de utilidades compartidas y el harness. Hasta aquí diste estructura (módulo 3) y control (módulo 4) a la suite, pero los tests siguen repitiendo lógica entre sí —cada uno arma su setup, cada uno escribe sus mismas aserciones—. El módulo 5 ataca esa duplicación: una biblioteca de utilidades compartidas —helpers, aserciones personalizadas como assert_refund(...), el harness de setup/teardown— tratada como un módulo del framework, no como copy-paste. Vas a ver el equilibrio delicado de DRY sin magia escondida: reutilizar la lógica común sin ocultar lo que cada test verifica. Los cimientos —fixtures, estructura, marcadores y config— ya los tienes; lo que viene es la capa de código compartido que los tests usan para no repetirse.
Recursos
- pytest — How to mark test functions with attributes — la referencia completa de marcadores, registro,
--strict-markersy selección con-m; el mapa de todo lo que ensamblaste en este proyecto. - pytest — Configuration file formats — dónde vive el contrato (
pyproject.tomlcon[tool.pytest.ini_options]) y cómo pytest lo encuentra; el archivo que escribiste en los pasos 3 y 4. - pytest — How to manage options: addopts — la referencia de
addoptsy cómo se compone con la línea de comandos; la base del tablero de mandos que fijaste con-m smoke.