Propiedades y pruebas primero
Enuncia reglas que se cumplen para toda entrada, deja que una herramienta busque la entrada que las rompe y escribe la prueba que falla antes que el código.
AL TERMINAR, PODRÁS
- Enunciar una propiedad que debe cumplirse para toda entrada válida
- Explicar cómo las entradas generadas y la reducción llevan a un caso pequeño que falla
- Seguir el ciclo rojo, verde, refactor en un cambio pequeño
Descripción del diagrama: Propiedad → Generar; Generar → Falla con; Falla con → Reducida (reducir); Rojo → Verde; Verde → Refactor; Refactor → Rojo (siguiente comportamiento)
De los ejemplos a las propiedades
Una prueba (test) de ejemplo comprueba una entrada que tú elegiste.
Una propiedad enuncia una regla que debe cumplirse para toda entrada válida, y una prueba la comprueba con muchas. Hay formas que se repiten. Un viaje de ida y vuelta: decodificar lo que codificaste devuelve el original.
Un invariante: la salida de un ordenamiento está en orden y tiene los mismos elementos. Un límite: la espera de reintento de Relay siempre está entre uno y sesenta segundos. La coincidencia con una versión más simple: una función rápida devuelve lo mismo que una lenta y claramente correcta.
La idempotencia: normalizar dos veces da lo mismo que normalizar una. Escribir una propiedad te obliga a decir cuál es de verdad el contrato, algo que un ejemplo te deja saltar. Incluso sin biblioteca, un ciclo sobre entradas de un generador con semilla fija comprueba una propiedad y falla igual en cada ejecución.
import random
def test_delay_stays_within_bounds():
rng = random.Random(7)
for _ in range(1_000):
attempt = rng.randint(0, 10_000)
assert 1 <= retry_delay(attempt) <= 60
Deja que una herramienta busque, y luego reduce
Una biblioteca de pruebas basadas en propiedades (property-based testing), como Hypothesis, genera las entradas por ti.
Describes su forma con estrategias (strategies), como listas de strings, y la prueba se ejecuta por defecto con cien ejemplos generados, incluidos casos incómodos como listas vacías y números muy grandes. Cuando una entrada falla, la biblioteca la reduce (shrinking): sigue probando versiones más simples que todavía fallan y reporta la más simple que encuentra, así que una falla con una lista de cuarenta elementos se reporta con una de dos. En su forma más sencilla, reducir es quitar una parte de la entrada y quedarse con la versión más pequeña cada vez que sigue fallando.
Hypothesis guarda las entradas que fallaron y las prueba primero en la siguiente ejecución, y @example fija un caso que siempre quieres comprobar. Una propiedad que pasa sigue siendo una muestra, no una demostración, y una propiedad débil deja pasar código incorrecto: «el resultado no tiene repetidos» se cumple para una función que devuelve una lista vacía.
from hypothesis import given, strategies as st
@given(st.lists(st.text()))
def test_dedup_keeps_every_item_once(items):
result = stable_dedup(items)
assert len(result) == len(set(items))
assert set(result) == set(items)
Escribe la prueba primero
El desarrollo guiado por pruebas (TDD, test-driven development) convierte las pruebas en la forma de diseñar un cambio.
Lleva una lista corta de comportamientos que faltan. Rojo: escribe una prueba pequeña para el siguiente comportamiento, ejecútala y mira que falle por la razón que esperas; una prueba que nunca viste fallar quizá no pruebe nada. Verde: escribe el código más simple que la haga pasar, aunque quede torpe.
Refactor: mejora los nombres y la estructura, en el código y en las pruebas, mientras todas siguen en verde. Luego elige el siguiente comportamiento. Los pasos pequeños mantienen cada falla cerca de su causa, y las pruebas describen lo que necesita quien llama antes de que exista la implementación.
Las propiedades también encajan en el ciclo: cuando una propiedad encuentra una falla, agrega la entrada reducida como ejemplo explícito, corrige el código y conserva el ejemplo como prueba de regresión.
# Red: written before stable_dedup exists, so it fails.
def test_dedup_keeps_the_first_occurrence():
assert stable_dedup(["b", "a", "b"]) == ["b", "a"]
# Green: the simplest code that passes.
def stable_dedup(items):
seen, result = set(), []
for item in items:
if item not in seen:
seen.add(item)
result.append(item)
return result
ERROR FRECUENTE
Confiar en una propiedad que nunca viste fallar. Rompe el código a propósito, por ejemplo que devuelva una lista vacía, y confirma que al menos una propiedad lo detecta.
INTÉNTALO TÚ
Escribe dos propiedades para retry_delay de Relay y dos para stable_dedup. Luego rompe cada función a propósito de dos maneras distintas y comprueba que cada ruptura haga fallar al menos una propiedad.
Decide en el laboratorio.
El checkpoint de dos preguntas, tus notas y tu progreso guardado están en el laboratorio. Gratis y sin cuenta.
Abrir en el laboratorio ↗CONÉCTALO CON RELAY
Agrega pruebas de propiedades a Relay: retry_delay queda entre 1 y 60 para cualquier intento, y stable_dedup no pierde ni repite nada. Luego agrega el siguiente comportamiento de la cola con la prueba primero, y deja los pasos rojo, verde y refactor en tus commits.