Módulo 6: Puertas de calidad — cobertura y umbrales que rompen el build

7. El 100% como fetiche que empuja tautologías

Descripción

Esta lección es el lado oscuro de todo lo anterior, y la prueba de que una puerta mal entendida no es neutra: hace daño activo. Ya viste que la cobertura es una condición necesaria pero no suficiente de la calidad (lección 6). Aquí verás qué pasa cuando un equipo olvida esa distinción y trata la cobertura como una meta a maximizar cueste lo que cueste —el fetiche del 100%—. El incentivo se pervierte de una forma concreta y demostrable: en vez de escribir tests que verifican que el código hace lo correcto, la gente escribe tests tautológicos que solo ejecutan el código para subir el número. La puerta se cumple, el porcentaje sube a verde, y sin embargo el código no está más protegido que antes —a veces, ni un poco—.

Al terminar vas a haber visto la trampa ejecutada de verdad, no descrita. Un test tautológico —uno que llama a una función de Reservo y solo comprueba que el resultado "no es nulo"— sube la cobertura de esa función y pasa en verde. Pero cuando rompemos la función a propósito —le hacemos devolver texto basura—, ese test tautológico sigue pasando (exit 0), sin notar nada, mientras un test real que afirma el contenido atrapa el bug (exit 1). Es el "verde que no verifica" de la guía de fundamentos de testing, ahora convertido en el producto directo de una puerta de cobertura mal usada. Vas a llevarte la lección que corona el módulo: la cobertura mide qué se ejecutó, no qué se verificó, y perseguir el número por sí mismo produce tests que suben la cobertura y no atrapan bugs —lo peor de los dos mundos, porque además dan una falsa sensación de seguridad—.

Conexión con el módulo: esta lección es la contracara exacta de la 3. Allá, la puerta roja te empujó a escribir un test que verifica (subió la cobertura y protegió el código) —apagar el fuego—. Aquí, el fetiche del umbral empuja a escribir un test que solo ejecuta (sube la cobertura y no protege nada) —arrancarle la batería al detector, pero peor, porque el detector ahora parece encendido—. Cierra el arco del módulo: la puerta es una herramienta, y como toda herramienta, su valor depende de cómo se use. Bien usada (lecciones 3-6), atrapa erosión real; fetichizada, produce el teatro que esta lección desnuda. Es también la conexión más fuerte con la guía de fundamentos: el "verde que no verifica" que aprendiste allá es, en CI, el subproducto de una puerta convertida en fetiche.

La escuela que enseña para el examen, no para aprender

Imagina dos escuelas cuyo desempeño se mide con un único número: el porcentaje de alumnos que aprueban un examen estandarizado. La primera escuela toma el número como lo que es —una señal imperfecta de aprendizaje— y enseña de verdad: los alumnos entienden, y como entienden, aprueban. El número sube porque el aprendizaje subió. La medición y lo que mide se mueven juntas.

La segunda escuela fetichiza el número. La meta deja de ser "que aprendan" y se vuelve "que el porcentaje suba", cueste lo que cueste. ¿Cómo se sube un porcentaje de aprobados sin enseñar? Enseñando para el examen: se memorizan las respuestas de los exámenes viejos, se entrenan los trucos para adivinar, se practica el formato hasta el cansancio. Los alumnos aprueban —el número sube, la meta se cumple— pero no aprendieron nada; solo aprendieron a pasar ese examen. Y aquí está lo perverso: la segunda escuela se ve, en el número, igual de buena que la primera. El 95% de aprobados no distingue "aprendieron" de "memorizaron el truco". El número se cumplió y la sustancia se perdió, y peor, el número ya no sirve para distinguir una escuela de la otra.

Esto tiene un nombre: la ley de Goodhart —"cuando una medida se vuelve un objetivo, deja de ser una buena medida"—. Una métrica funciona como señal mientras nadie la manipule directamente; en el momento en que se vuelve la meta, la gente encuentra formas de moverla sin mover lo que se suponía que medía, y la métrica se rompe. La cobertura es exactamente así. Como señal —"¿hay código sin probar?"— es útil. Como meta —"lleguemos al 100%"— se vuelve manipulable: se puede subir escribiendo tests que ejecutan sin verificar, igual que se sube un porcentaje de aprobados enseñando para el examen. El número se cumple; la sustancia —tests que atrapan bugs— se pierde. Esta lección es esa ley, demostrada con código de Reservo.

Ley de Goodhart: cuando una medida se vuelve un objetivo, deja de ser una buena medida. La cobertura como señal ("¿hay código sin probar?") es útil; como meta a maximizar ("lleguemos al 100%") se vuelve manipulable —se sube escribiendo tests que ejecutan sin verificar, como una escuela que enseña para el examen—. El número se cumple; la protección se pierde.

Qué es un test tautológico

Una tautología es una afirmación que es verdadera por su propia forma, sin decir nada del mundo: "mañana lloverá o no lloverá" es cierto pase lo que pase, y por eso no informa nada. Un test tautológico es el equivalente en código: un test cuyo assert es verdadero casi pase lo que pase, de modo que ejecuta el código —y por lo tanto sube la cobertura— pero no verifica que el código haga lo correcto —y por lo tanto no atrapa bugs—.

El patrón más común es afirmar algo trivialmente cierto sobre el resultado. Reservo tiene una función que arma el texto del correo de confirmación:

# reservo/notifications.py
def booking_confirmed_message(booking: Booking) -> str:
    """Cuerpo del correo cuando una reserva se confirma."""
    return (
        f"Your booking {booking.id} for {booking.room.name} is confirmed. "
        f"Total: {booking.price_cents} cents."
    )

Un test tautológico para esta función se ve así —y es tentador, porque es corto, pasa siempre, y sube la cobertura—:

# tautologico: ejecuta la funcion pero no afirma nada del contenido
def test_confirmed_message_tautological():
    msg = booking_confirmed_message(b)
    assert msg is not None       # ¿cuando seria None? Casi nunca.
    assert isinstance(msg, str)  # una funcion que arma un f-string SIEMPRE da str

Mira los assert. msg is not None: la función construye un f-string y lo devuelve; jamás devuelve None, así que esa afirmación es cierta pase lo que pase con el contenido. isinstance(msg, str): un f-string siempre es un str, así que también es cierta siempre. El test ejecuta booking_confirmed_message entera —la cubre—, pero no comprueba nada de lo que la función debe hacer: no verifica que el mensaje mencione la reserva, ni la sala, ni el precio. Es la tautología: cierto por su forma, ciego al mundo. Y cumple exactamente su propósito perverso —subir la cobertura— sin cumplir el propósito real de un test —verificar—.

La demo: el test tautológico sube la cobertura

Comprobemos primero que el test tautológico de verdad sube la cobertura, que es lo que lo hace tentador para quien persigue el número. Corremos solo ese test, midiendo la cobertura de booking_confirmed_message, de verdad en Python 3.14.0:

python -m pytest test_notifications_tautological.py --cov=reservo.notifications --cov-report=term-missing

Qué esperar (salida real):

Name                       Stmts   Miss  Cover   Missing
--------------------------------------------------------
reservo/notifications.py       7      3    57%   16-21
1 passed in 0.01s

El test pasa (1 passed) y cubre booking_confirmed_message: la cobertura de notifications.py sube a 57% —las líneas de la función confirmada, ejecutadas; las que faltan, 16-21, son de la otra función que este test no toca—. Para quien persigue el porcentaje, misión cumplida: escribió un test de tres líneas, pasó, y la cobertura subió. Si su meta es "que el número suba", este test es un éxito. Guarda ese "éxito" en mente, porque ahora viene la prueba de que es un fraude.

La demo: el test tautológico no atrapa el bug

La pregunta que separa un test de verdad de uno tautológico es la del módulo de fundamentos: ¿el test se pone rojo cuando el código se rompe? Un test que no muerde cuando el código está mal no protege nada, por más cobertura que aporte. Comprobémoslo rompiendo booking_confirmed_message a propósito: le hacemos devolver texto basura —un mensaje totalmente equivocado que jamás debería llegarle a un cliente—:

# reservo/notifications.py, ROTO a proposito
def booking_confirmed_message(booking: Booking) -> str:
    return "WRONG: totally broken confirmation text"

Este código está roto de la peor forma: le manda al cliente un texto sin sentido, sin su reserva, sin su sala, sin su precio. Un test que valga algo debería atraparlo. Corremos el test tautológico contra este código roto:

python -m pytest test_notifications_tautological.py

Qué esperar (salida real):

1 passed in 0.01s

Y el exit code:

python -m pytest test_notifications_tautological.py > /dev/null 2>&1; echo "exit code: $?"
exit code: 0

1 passed, exit code 0. El test tautológico pasa sobre el código roto. Piénsalo: booking_confirmed_message ahora devuelve "WRONG: totally broken confirmation text", y el test dice que todo está bien. ¿Por qué? Porque sus assert siguen siendo ciertos: "WRONG..." no es None (msg is not None ✓) y es un str (isinstance(msg, str) ✓). El texto basura cumple las dos tautologías igual que el texto correcto. El test cubrió la función, pasó en verde, subió la cobertura —y no notó que la función está catastróficamente rota—. Es el "verde que no verifica" en estado puro: un verde que no significa nada, porque el rojo era imposible.

La demo: un test real sí lo atrapa

Contrastemos con un test que verifica de verdad —uno que afirma el contenido que la función debe producir—:

# test real: afirma que el mensaje menciona la reserva, la sala y el precio
def test_confirmed_message_names_the_booking_and_price():
    msg = booking_confirmed_message(b)
    assert "bk-1" in msg     # la reserva
    assert "Focus" in msg    # la sala
    assert "7500" in msg     # el precio

Estos assert no son tautológicos: afirman algo concreto sobre el mundo —que el mensaje contiene el id de la reserva, el nombre de la sala y el precio—. Si la función devuelve el texto correcto, pasan; si devuelve otra cosa, fallan. Corramos este test contra el mismo código roto (booking_confirmed_message devolviendo basura):

python -m pytest test_notifications_real.py

Qué esperar (salida real):

>       assert "bk-1" in msg
E       AssertionError: assert 'bk-1' in 'WRONG: totally broken confirmation text'
tautdemo/test_notifications_real.py:15: AssertionError

FAILED test_notifications_real.py::test_confirmed_message_names_the_booking_and_price
================================ 1 failed in 0.03s =============================

Y el exit code:

python -m pytest test_notifications_real.py > /dev/null 2>&1; echo "exit code: $?"
exit code: 1

1 failed, exit code 1. El test real atrapa el bug: assert 'bk-1' in 'WRONG: totally broken confirmation text' falla, porque el texto basura no contiene el id de la reserva. Rojo. Este test verifica, así que muerde cuando el código está mal.

Pon las dos demos lado a lado, porque en el contraste está toda la lección:

Test tautológicoTest real
¿Cubre la función?Sí (sube la cobertura)Sí (sube la cobertura)
¿Pasa con el código correcto?
¿Pasa con el código roto?Sí (exit 0) — no nota nadaNo (exit 1) — atrapa el bug
¿Protege el código?No

Los dos suben la cobertura por igual —una puerta de cobertura no los distingue—. Pero uno protege y el otro no. La puerta ve el número idéntico; la realidad es opuesta. Ese es exactamente el hueco por donde el fetiche del 100% mete el fraude: si tu meta es el número, los dos tests "valen lo mismo"; si tu meta es atrapar bugs, uno vale y el otro es peor que nada, porque da falsa confianza.

Por qué el fetiche del 100% empuja justo a esto

Conecta el mecanismo. Cuando el umbral es honesto (lección 6) —un piso o trinquete en tu nivel real—, la puerta te empuja a probar el código sin probar: escribes tests para las funciones que ningún test toca, y como esas funciones hacen algo, tus tests naturalmente verifican ese algo. El incentivo apunta a la verificación.

Cuando el umbral se vuelve fetiche —"hay que llegar al 100%"—, el incentivo se tuerce. Los últimos puntos de cobertura suelen ser los más difíciles y menos valiosos: ramas de error raras, líneas defensivas, código que casi nunca corre. Probar eso de verdad es trabajoso. Pero el fetiche no pide verificación, pide el número. Y la forma más rápida de mover el número sin hacer el trabajo real es el test tautológico: llamar a la función, afirmar algo trivial, cubrir la línea, subir el porcentaje. El fetiche premia exactamente el atajo que no verifica. Cuanto más alto e inflexible el umbral, más fuerte el empujón hacia la tautología —porque cerrar el último 5% con tests de verdad es caro, y con tautologías es gratis—.

Por eso el 100% es un fetiche particularmente tóxico. No solo es difícil de alcanzar con tests reales; es que el esfuerzo de alcanzarlo se canaliza hacia los tests falsos, porque son la vía barata de cumplir la meta. Un equipo con la puerta en 100% termina, con frecuencia, con más tautologías que un equipo con la puerta en 85% honesto —el primero pagó el último 15% con fraude; el segundo no tuvo que fingir—. La meta más ambiciosa produjo tests peores. Es la ley de Goodhart cobrando su precio: la métrica, vuelta objetivo extremo, dejó de medir lo que importaba y empezó a premiar su falsificación.

Cómo defenderse: verificar, no solo ejecutar

La defensa no es abandonar la cobertura —sigue siendo una señal útil—; es usarla bien y complementarla:

  • Umbral honesto, no fetiche. Piso o trinquete en tu nivel real (lección 6), no 100% aspiracional. Quita el incentivo a las tautologías de raíz: si no persigues el último punto a toda costa, no necesitas falsificarlo.
  • Juzga los tests por si muerden, no por lo que cubren. La pregunta de fundamentos: ¿este test se pone rojo cuando rompo el código? Un test que no muerde no vale, cubra lo que cubra. La técnica de romper el código a propósito (mutación manual) que usaste en las demos es cómo se comprueba.
  • Revisa los assert, no solo la existencia del test. En la revisión de código, un assert msg is not None sobre una función que arma un string debería levantar la ceja: ¿qué está verificando de verdad? Un test sin un assert sustancioso es sospechoso de tautología.
  • La cobertura señala dónde faltan tests; tú decides si el test que escribes verifica. Usa el reporte term-missing para encontrar el código sin probar (buen uso), y escribe para esas líneas tests que afirmen comportamiento real —no tautologías para tapar el hueco en el número—.

En una frase: la cobertura mide qué se ejecutó, no qué se verificó, así que la protección real no viene del porcentaje sino de que cada test afirme algo que se rompería si el código estuviera mal. Una puerta de cobertura honesta te dice dónde mirar; que lo que escribas ahí verifique de verdad es tu responsabilidad, y ninguna puerta la puede sustituir.

Errores comunes

Escribir tests para el número, no para el bug. Qué pasa: alguien ve la puerta roja o el reporte con una función sin cubrir y escribe el test más corto que la cubra —assert result is not None—, sin preguntarse qué debería verificar. La cobertura sube, el bug queda sin red. Por qué pasa: cubrir la línea es el objetivo aparente (lo que la puerta pide), y verificar es más trabajo. Cómo detectarlo: por cada test, pregúntate "si rompo la función que prueba, ¿este test se pone rojo?". Si no, es tautológico. Cómo corregirlo: escribe el assert que afirme el comportamiento real —lo que el código debe hacer, no que "devuelve algo"—. La cobertura es un efecto secundario de un buen test, no su meta.

Perseguir el 100% como si fuera un logro. Qué pasa: un equipo pone la meta en 100% y celebra alcanzarla, sin notar que el último tramo se cerró con tautologías. Por qué pasa: 100% es redondo, satisfactorio, y suena a excelencia. Cómo detectarlo: mira los tests que cubren las ramas más raras; si son assert x is not None o assert True, el 100% es de cartón. Cómo corregirlo: acepta que el número correcto casi nunca es 100 —las últimas ramas suelen costar más de lo que valen probarlas de verdad—, y prefiere un 85% honesto de tests que muerden a un 100% inflado de tautologías. La ley de Goodhart: en el momento en que el 100% es la meta, deja de significar "bien probado".

Confiar en la cobertura como si midiera calidad de los tests. Qué pasa: se lee "92% de cobertura" como "92% bien probado", sin distinguir tests que verifican de tests que solo ejecutan. Por qué pasa: la cobertura es el único número a mano, y es tentador dejar que signifique más de lo que significa. Cómo detectarlo: la demo de esta lección —dos tests con la misma cobertura, uno protege y el otro no—. Si tu confianza viene del porcentaje y no de haber visto tus tests morder, es confianza prestada. Cómo corregirlo: recuerda que la cobertura mide ejecución, no verificación. La calidad de los tests se comprueba rompiendo el código y viendo cuáles se ponen rojos, no leyendo un porcentaje.

Ejercicios

Ejercicio 1 — Detecta la tautología. Para cada test de Reservo, di si es tautológico (ejecuta sin verificar) o real (verifica), y explica cómo lo sabes. (a) assert price_cents(focus, pro, 3) == 6000. (b) assert price_cents(focus, pro, 3) is not None. (c) assert isinstance(refund_cents(booking, 6000, now), int). (d) assert refund_cents(booking, 6000, now) == 6000.

Ver solución
  • (a) Real. Afirma el valor exacto (6000, el número-ancla del descuento pro). Si el descuento se rompe (da 5625 con el 25%), el test se pone rojo. Verifica.
  • (b) Tautológico. price_cents siempre devuelve un entero (nunca None), así que is not None es cierto pase lo que pase con el valor. Si el precio se rompe y da 5625, el test pasa igual —5625 tampoco es None—. Ejecuta, no verifica.
  • (c) Tautológico (casi). refund_cents siempre devuelve un int por su construcción, así que isinstance(..., int) es cierto siempre. Si el reembolso da 3000 cuando debería dar 6000, el test pasa —3000 también es int—. Comprueba el tipo, no el valor, y el bug está en el valor. (Comprobar el tipo puede tener sentido en algunos contextos, pero como única aserción sobre una función de dinero, es tautológico respecto a lo que importa.)
  • (d) Real. Afirma el valor exacto del reembolso (6000, el ancla del reembolso completo). Si la política de reembolso se rompe, se pone rojo. Verifica.

La regla para detectarlas: pregúntate "¿qué valores del resultado harían fallar este assert?". Si la respuesta es "casi ninguno" (como en is not None o isinstance sobre una función que siempre da ese tipo), es tautológico —cubre pero no muerde—. Si hay valores concretos que lo harían fallar (todo lo que no sea 6000), verifica.

Ejercicio 2 — Convierte la tautología en verificación. Un compañero escribió este test para refund_reason de Reservo (que devuelve "full refund", "half refund" o "no refund"). Cubre la función y pasa, pero es tautológico. Reescríbelo para que verifique de verdad, y explica qué bug atraparía el tuyo que el suyo no.

def test_refund_reason():
    reason = refund_reason(booking, datetime(2026, 1, 1, 9))  # 72h antes
    assert reason is not None
    assert len(reason) > 0
Ver solución

El test del compañero es tautológico: refund_reason siempre devuelve un string no vacío (una de las tres frases), así que is not None y len(reason) > 0 son ciertos pase lo que pase. Si la función devolviera la frase equivocada —"no refund" cuando debería ser "full refund"—, el test pasaría igual, porque "no refund" tampoco es None ni vacío. Cubre la línea, no atrapa el bug.

La versión que verifica afirma qué frase corresponde a cada tramo:

def test_refund_reason_matches_the_tier():
    # 72h antes (>= 48h) -> full refund
    assert "full refund" in refund_reason(booking, datetime(2026, 1, 1, 9))
    # 36h antes (24-48h) -> half refund
    assert "half refund" in refund_reason(booking, datetime(2026, 1, 2, 21))
    # 9h antes (< 24h) -> no refund
    assert "no refund" in refund_reason(booking, datetime(2026, 1, 4, 0))

Qué bug atrapa el mío que el suyo no: cualquier error en qué tramo devuelve qué frase. Si alguien invierte una condición y refund_reason devuelve "no refund" para una cancelación de 72 h (que debería ser "full refund"), mi test falla (assert "full refund" in "no refund..." es falso) y el suyo pasa (la frase equivocada sigue siendo no-nula y no-vacía). El suyo verifica que la función devuelve algo; el mío verifica que devuelve lo correcto —que es lo que a un cliente le importa, porque la frase le dice si recupera su dinero—. La diferencia: el mío tiene valores del resultado que lo harían fallar; el suyo no.

Ejercicio 3 — El diálogo del 100%. Tu líder dice: "Subamos la puerta de cobertura de Reservo a 100%. Más cobertura es más calidad, y 100% es la meta." Usando lo del módulo, respóndele: ¿por qué 100% como puerta puede producir peor código de test que un umbral más bajo? Da el argumento y un ejemplo concreto de Reservo.

Ver solución

El argumento, en tres pasos:

  1. Cobertura no es calidad (necesaria, no suficiente). El 100% garantiza que todas las líneas se ejecutan en los tests, no que los tests verifiquen algo. Un 100% de cobertura es compatible con tests que no atrapan ningún bug —lo demostramos: un assert msg is not None cubre al 100% y pasa aunque la función devuelva basura—.

  2. El 100% como meta empuja a tautologías (ley de Goodhart). Los últimos puntos de cobertura son los más caros de probar de verdad (ramas de error raras, líneas defensivas). Perseguir el 100% a toda costa canaliza el esfuerzo hacia la vía barata de cerrar esos puntos: tests tautológicos que ejecutan sin verificar. La meta extrema premia justo el atajo que no protege.

  3. Resultado: peor código de test que con un umbral honesto. Un equipo con la puerta en 100% suele terminar con más tautologías que uno con la puerta en 91% honesto (el nivel real de Reservo, lección 6). El primero pagó el último 9% con fraude; el segundo no tuvo que fingir. La meta más ambiciosa produjo tests peores.

Ejemplo concreto de Reservo: imagina la rama de error if hours <= 0: raise ValueError en price_cents. Probarla de verdad es escribir with pytest.raises(ValueError): price_cents(room, member, 0) —un test real—. Pero bajo la presión del 100%, alguien podría "cubrirla" con algo que ejecute la línea sin verificar la excepción correcta, solo para cerrar el número. Con un umbral honesto de 91, esa rama puede quedar sin cubrir sin drama, o cubrirse bien cuando toque; con la puerta en 100%, se cierra a las malas.

La propuesta alternativa: umbral honesto (91, trinquete) que impide retroceder sin exigir el fraude del último tramo, más la práctica de juzgar los tests por si muerden (romper el código y ver cuáles se ponen rojos). Eso da más protección real que un 100% de cartón. "Más cobertura es más calidad" es cierto solo mientras la cobertura sea señal; en el momento en que es meta extrema, deja de medir calidad y empieza a medir cuántas tautologías escribió el equipo para cumplirla.

Resumen y siguiente paso

En esta lección desnudaste el peor uso de una puerta: el 100% como fetiche. Cuando la cobertura deja de ser una señal y se vuelve una meta a maximizar, la ley de Goodhart cobra su precio —"cuando una medida se vuelve objetivo, deja de ser buena medida"—: el equipo, empujado a subir el número, escribe tests tautológicos que ejecutan el código sin verificarlo, como una escuela que enseña para el examen en vez de para aprender.

Lo viste ejecutado de verdad, no descrito. Un test tautológico —assert msg is not None— cubrió booking_confirmed_message y pasó en verde, subiendo la cobertura. Pero cuando rompiste la función (texto basura), ese test siguió pasando (exit 0), ciego al desastre, mientras un test real que afirma el contenido —assert "bk-1" in msgatrapó el bug (exit 1). Los dos daban la misma cobertura; solo uno protegía. Ese contraste es la lección entera: la cobertura mide qué se ejecutó, no qué se verificó, y perseguir el número por sí mismo produce lo peor —tests que suben la cobertura, no atrapan bugs, y encima dan falsa confianza—. Es el "verde que no verifica" de fundamentals, ahora como subproducto directo de una puerta fetichizada.

Antes de avanzar deberías poder: definir un test tautológico y detectarlo preguntando "¿qué valores harían fallar este assert?"; explicar por qué la cobertura no distingue un test que verifica de uno que solo ejecuta; articular por qué el 100% como meta empuja a tautologías (Goodhart); y convertir un assert x is not None en una verificación real del comportamiento.

Lo que sigue, en la lección 8, es el mini-proyecto que cierra el módulo: pones una puerta de cobertura al CI de Reservo de principio a fin, y —el requisito clave— haces que rompa el build cuando falta un test. Vas a juntar todo: el workflow YAML con el step de --cov-fail-under, el .coveragerc, la demostración local del ciclo rojo→verde (65.38% exit 1 → agregas el test → 91.03% exit 0), la puerta smoke como job aparte, y una nota de decisión que justifica —con todo lo de las lecciones 6 y 7— qué umbral merece Reservo y por qué no el 100%.

Recursos

  • Coverage.py: qué es y qué no es la cobertura — la documentación oficial insiste en que la cobertura mide líneas ejecutadas, no comportamiento verificado. La base técnica de toda la lección.
  • pytest.raises: verificar excepciones de verdad — cómo probar una rama de error verificando la excepción correcta, en vez de "cubrirla" con una tautología. La herramienta para hacer real el test de if hours <= 0: raise ValueError.
  • Ley de Goodhart (concepto) — "cuando una medida se vuelve un objetivo, deja de ser una buena medida". No es un concepto de pytest sino de medición en general, pero explica exactamente por qué la cobertura como meta se corrompe. Piénsalo cada vez que alguien proponga maximizar una métrica.
  • Cómo escribir aserciones útiles en pytest — la referencia de assert en pytest; la clave contra las tautologías es que cada aserción afirme algo que se rompería si el código estuviera mal. Un test vale por sus assert, no por las líneas que ejecuta.