Una guía de codificación para pruebas basadas en propiedades utilizando hipótesis con diseño de pruebas con estado, diferenciales y metamórficas

En este tutorial, exploramos las pruebas basadas en propiedades utilizando Hipótesis y creamos un proceso de pruebas riguroso que va mucho más allá de las pruebas unitarias tradicionales. Implementamos invariantes, pruebas diferenciales, pruebas metamórficas, exploración dirigida y pruebas con estado para validar tanto la corrección funcional como las garantías de comportamiento de nuestros sistemas. En lugar de crear manualmente casos extremos, permitimos que Hypothesis genere entradas estructuradas, reduzca las fallas a contraejemplos mínimos y descubra sistemáticamente errores ocultos. Además, demostramos cómo las prácticas de prueba modernas se pueden integrar directamente en flujos de trabajo experimentales y basados ​​en la investigación.

importar sys, textwrap, subproceso, os, re, math!{sys.executable} -m pip -q instalar hipótesis pytest test_code = r”’ importar re, matemáticas importar pytest desde hipótesis importar (dado, asumir, ejemplo, configuración, nota, objetivo, HealthCheck, Fase) desde hipótesis importar estrategias como st desde hipótesis.stateful importar RuleBasedStateMachine, regla, invariante, inicializar, condición previa def abrazadera(x: int, lo: int, hola: int) -> int: si x < lo: regresar lo si x > hola: regresar hola regresar x def normalize_whitespace(s: str) -> str: regresar ” “.join(s.split()) def is_sorted_non_decreasing(xs): devolver todo(xs[i] <= xs[i+1] para i en rango(len(xs)-1)) def merge_sorted(a, b): i = j = 0 out = [] mientras que i < len(a) y j < len(b): si a[i] <= segundo[j]: out.append(a[i]); i += 1 más: out.append(b[j]); j += 1 salida.extender(a[i:]) fuera.extender(b[j:]) devolver def merge_sorted_reference(a, b): devolver ordenado(lista(a) + lista(b))

Configuramos el entorno instalando Hypothesis y pytest e importando todos los módulos necesarios. Comenzamos a construir el conjunto de pruebas completo definiendo funciones de utilidad principales como abrazadera, normalize_whitespace y merge_sorted. Establecemos la base funcional que nuestras pruebas basadas en propiedades validarán rigurosamente en fragmentos posteriores.

def safe_parse_int(s: str): t = s.strip() si re.fullmatch(r”[+-]?\d+”, t) es Ninguno: return (False, “not_an_int”) if len(t.lstrip(“+-“)) > 2000: return (False, “too_big”) try: return (True, int
elif t[0] == “-“: signo = -1 t = t[1:]
si no es t o cualquiera(ch < "0" o ch > “9” para ch en t): devolver (Falso, “not_an_int”) si len

Implementamos lógica de análisis y definimos estrategias estructuradas que generan entradas de prueba significativas y restringidas. Creamos estrategias compuestas como int_like_strings para controlar con precisión el espacio de entrada para la validación de propiedades. Preparamos generadores de listas ordenadas y estrategias de límites que permiten pruebas basadas en diferenciales e invariantes.

@settings(max_examples=300, suprimir_health_check=[HealthCheck.too_slow]) @given(x=st.integers(-50_000, 50_000), b=bounds) def test_clamp_within_bounds(x, b): lo, hola = b y = abrazadera(x, lo, hola) afirmar lo <= y <= hola @settings(max_examples=300, suprimir_salud_check=[HealthCheck.too_slow]) @given(x=st.integers(-50_000, 50_000), b=bounds) def test_clamp_idempotent(x, b): lo, hola = b y = abrazadera(x, lo, hola) afirmar abrazadera(y, lo, hola) == y @settings(max_examples=250) @given(s=st.text()) @example(" a\t\tb \nc ") def test_normalize_whitespace_is_idempotent(s): t = normalize_whitespace(s) afirmar normalize_whitespace

Definimos pruebas de propiedades centrales que validan la corrección y la idempotencia en múltiples funciones. Utilizamos decoradores de hipótesis para explorar automáticamente casos extremos y verificar garantías de comportamiento, como restricciones de límites y normalización determinista. También implementamos pruebas diferenciales para garantizar que nuestra implementación de fusión coincida con una referencia confiable.

@settings(max_examples=250, fecha límite=200, suprimir_health_check=[HealthCheck.too_slow]) @given(s=int_like_strings()) def test_two_parsers_agree_on_int_like_strings(s): ok1, v1 = safe_parse_int(s) ok2, v2 = safe_parse_int_alt(s) afirmar ok1 y ok2 afirmar v1 == v2 @settings(max_examples=250) @given(s=st.text(min_size=0, max_size=200)) def test_safe_parse_int_rejects_non_ints(s): t = s.strip() m = re.fullmatch(r”[+-]?\d+”, t) ok, val = safe_parse_int(s) si m es Ninguno: afirmar ok es Falso de lo contrario: si len(t.lstrip(“+-“)) > 2000: afirmar ok es Falso y val == “too_big” más: afirmar ok es Verdadero e isinstance(val, int) def varianza(xs): si len(xs) < 2: devuelve 0.0 mu = suma(xs) / len(xs) return suma((x - mu) ** 2 para x en xs) / (len(xs) - 1) @settings(max_examples=250, fases=[Phase.generate, Phase.shrink]) @given(xs=st.lists(st.integers(-1000, 1000), min_size=0, max_size=80)) def test_statistics_sanity(xs): target(varianza(xs)) if len(xs) == 0: afirmar varianza(xs) == 0.0 elif len(xs) == 1: afirmar varianza(xs) == 0.0 más: v = varianza(xs) afirmar v >= 0.0 k = 7 afirmar math.isclose(varianza([x + k for x in xs]), v, rel_tol=1e-12, abs_tol=1e-12)

Ampliamos nuestra validación para analizar la solidez y la corrección estadística mediante exploración dirigida. Verificamos que dos analizadores de enteros independientes acuerden entradas estructuradas y apliquen reglas de rechazo en cadenas no válidas. Además, implementamos pruebas metamórficas validando invariantes de varianza bajo transformación.

banco de clase: def __init__(self): self.balance = 0 self.ledger = []

def deposit(self, amt: int): si amt <= 0: aumentar ValueError("el depósito debe ser positivo") self.balance += amt self.ledger.append(("dep", amt)) def retirar(self, amt: int): si amt <= 0: aumentar ValueError("el retiro debe ser positivo") if amt > self.balance: aumentar ValueError(“fondos insuficientes”) self.balance -= amt self.ledger.append((“wd”, amt)) def replay_balance(self): bal = 0 para typ, amt in self.ledger: bal += amt if typ == “dep” else -amt return bal class BankMachine(RuleBasedStateMachine): def __init__(self): super().__init__() self.bank = Bank() @initialize() def init(self): afirmar self.bank.balance == 0 afirmar self.bank.replay_balance() == 0 @rule(amt=st.integers(min_value=1, max_value=10_000)) def deposit(self, amt): self.bank.deposit(amt) @precondition(lambda self: self.bank.balance > 0) @rule(amt=st.integers(min_value=1, max_value=10_000)) def retirar(self, amt): asumir(amt <= self.bank.balance) self.bank.withdraw(amt) @invariant() def balance_never_negative(self): afirmar self.bank.balance >= 0 @invariant() def ledger_replay_matches_balance(self): afirmar self.bank.replay_balance() == self.bank.balance TestBankMachine = BankMachine.TestCase ”’ ruta = “/tmp/test_hypothesis_advanced.py” con open(ruta, “w”, codificación=”utf-8″) como f: f.write(test_code) print(“Versión de hipótesis:”, __import__(“hipótesis”).__versión__) print(“\nEjecutando pytest en:”, ruta, “\n”) res = subprocess.run([sys.executable, “-m”, “pytest”, “-q”, path]capture_output=True, text=True) print(res.stdout) if res.returncode != 0: print(res.stderr) if res.returncode == 0: print(“\nTodas las pruebas de hipótesis aprobadas.”) elif res.returncode == 5: print(“\nPytest no recopiló pruebas.”) else: print(“\nAlgunas pruebas fallaron.”)

Implementamos un sistema con estado utilizando la máquina de estado basada en reglas de Hypothesis para simular una cuenta bancaria. Definimos reglas, condiciones previas e invariantes para garantizar la coherencia del saldo y la integridad del libro mayor en secuencias de operación arbitrarias. Luego ejecutamos todo el conjunto de pruebas a través de pytest, lo que permite a Hypothesis descubrir automáticamente contraejemplos y verificar la corrección del sistema.

En conclusión, creamos un marco de prueba integral basado en propiedades que valida funciones puras, lógica de análisis, comportamiento estadístico e incluso sistemas con estado con invariantes. Aprovechamos las capacidades cada vez más reducidas, de búsqueda dirigida y de pruebas de máquinas de estado de Hypothesis para pasar de las pruebas basadas en ejemplos a la verificación basada en el comportamiento. Nos permite razonar sobre la corrección en un nivel más alto de abstracción mientras mantiene sólidas garantías para los casos extremos y la coherencia del sistema.

Consulte el cuaderno de codificación completo aquí. Además, no dude en seguirnos en Twitter y no olvide unirse a nuestro SubReddit de más de 130.000 ML y suscribirse a nuestro boletín. ¡Esperar! estas en telegrama? Ahora también puedes unirte a nosotros en Telegram.

¿Necesita asociarse con nosotros para promocionar su repositorio de GitHub O su página principal de Hugging O su lanzamiento de producto O seminario web, etc.? Conéctate con nosotros