Módulo 5: Coverage y Edge Cases

Boundary Value Analysis y Property-Based Testing con Hypothesis

Boundary Value Analysis y Property-Based Testing con Hypothesis

Descripción de la cápsula

Sabes medir coverage y usar prompts para descubrir edge cases. Pero hay dos técnicas que multiplican tu efectividad: boundary value analysis (sistemático, predecible) y property-based testing con Hypothesis (aleatorio, descubre lo que no imaginabas). Esta cápsula te enseña ambas: cuándo usar valores límite explícitos, cuándo delegar en propiedades matemáticas, y cómo combinar todo con Claude Code.

Al final tendrás un arsenal de tres enfoques complementarios: boundary testing para límites conocidos, property testing para propiedades invariantes, y prompts para que Claude Code piense por ti en escenarios difíciles.


Boundary Value Analysis

Los bugs se concentran en los límites

Una observación empírica en testing: la mayoría de los bugs aparecen en los límites, no en el "medio" del dominio. Si una función acepta edad entre 0 y 120, el bug está casi seguro en -1, 0, 1, 119, 120 o 121 — no en 50, 30 u 80.

Tipo de boundaryEjemploPor qué falla el código
Mínimo0Off-by-one: if age < 0 vs if age <= 0
Máximo120Lo mismo al otro extremo
Cero0División, índices, lógica especial
Uno más/menos-1, 121Justo fuera del rango válido
Vacío/lleno[], [1], lista muy largaLongitud extrema

Regla práctica: testea los límites, no el centro

Para una función que valida edad 0–120:

✅ Testea: -1, 0, 1, 119, 120, 121
❌ No pierdas tiempo con: 50, 30, 80 — valores del medio rara vez revelan bugs.

Para una función sobre listas:

✅ Testea: [], [1], [1, 2], lista muy larga (1000+ elementos)
❌ No priorices: [1, 2, 3, 4, 5] — longitud arbitraria intermedia casi nunca es el problema.

Ejemplo: validate_age

def validate_age(age: int) -> bool:
    """Retorna True si age está en [0, 120]."""
    return 0 <= age <= 120

Un test naif probaría solo validate_age(25) y pasaría. El boundary test cubre todos los puntos críticos:

import pytest


def validate_age(age: int) -> bool:
    """Retorna True si age está en [0, 120]."""
    return 0 <= age <= 120


@pytest.mark.parametrize("age,expected", [
    (-1, False),    # Debajo del mínimo
    (0, True),      # Límite mínimo
    (1, True),      # Justo arriba del mínimo
    (119, True),    # Justo debajo del máximo
    (120, True),    # Límite máximo
    (121, False),   # Arriba del máximo
])
def test_validate_age_boundaries(age, expected):
    assert validate_age(age) == expected

Ejecuta:

pytest test_validate_age.py -v

Por qué los boundaries revelan bugs: un ejemplo real

Imagina que un developer escribió por error if age < 1 en vez de if age < 0, pensando que "edad negativa es imposible, no hace falta probarlo." O que usó if age <= 119 cuando el límite debería ser 120. Un test con validate_age(25) pasaría. Pero un boundary test con 0 o 120 fallaría de inmediato. Esa es la potencia: los límites son donde el código toma decisiones distintas, y una decisión equivocada ahí propaga el error a todo el dominio.

Un bug real: off-by-one en límites

Imagina que alguien implementó validate_age con un error común:

def validate_age(age: int) -> bool:
    return 0 < age < 120  # Bug: usa < en lugar de <=

Un test con validate_age(25) pasaría. Pero el boundary test con (0, True) fallaría: la función retorna False para edad 0 cuando debería aceptarla. Ese es exactamente el tipo de bug que boundary testing captura. Sin boundaries explícitos, el bug llegaría a producción y usuarios con edad 0 o 120 serían rechazados incorrectamente.


Patrones de boundary testing

Estructura parametrizada

@pytest.mark.parametrize es ideal para boundaries: un test, muchos valores.

@pytest.mark.parametrize("value,expected", [
    (min_value - 1, False),   # Debajo del rango
    (min_value, True),        # Límite inferior
    (min_value + 1, True),    # Justo dentro
    (max_value - 1, True),    # Justo dentro
    (max_value, True),        # Límite superior
    (max_value + 1, False),   # Arriba del rango
])
def test_boundaries(value, expected):
    assert function_under_test(value) == expected

Listas: vacío, uno, muchos

@pytest.mark.parametrize("items", [
    [],           # Vacío
    [1],          # Un elemento
    [1, 2],       # Dos elementos (algunas implementaciones fallan aquí)
    list(range(10000)),  # Muchos (stack, memoria, O(n²) oculto)
])
def test_list_operations(items):
    result = process_list(items)
    assert result is not None  # o la propiedad que corresponda

Strings: vacío, un carácter, Unicode, muy largo

@pytest.mark.parametrize("s", [
    "",
    "a",
    "ñ",
    "🔥",           # Unicode
    "a" * 1_000_000,
])
def test_string_handling(s):
    result = normalize(s)
    assert isinstance(result, str)

Un bug real: off-by-one en los límites

Imagina que alguien implementa validate_age así:

def validate_age(age: int) -> bool:
    """Retorna True si age está en [0, 120]."""
    if age < 0:
        return False
    if age > 120:  # Bug: debería ser >= 121 o mantener > 120
        return False
    return True

Esa versión es correcta. Pero esta variación tiene un bug típico:

def validate_age_buggy(age: int) -> bool:
    """Versión con bug: usa < en vez de <= en el máximo."""
    if age < 0:
        return False
    if age >= 120:   # Bug: 120 debería ser válido, pero lo rechaza
        return False
    return True

En realidad el bug estaría al otro lado: age > 120 deja pasar 120, pero age >= 120 rechaza 120. El boundary test con 120 lo detectaría. Sin boundary tests, validate_age(25) pasa y crees que todo está bien. En producción, usuarios con 120 años (o datos mal formateados) fallarían.

La lección: Un solo test con valor "normal" (25, 50, 80) no habría encontrado el bug. Los seis valores de boundary sí.


Introducción a property-based testing

De ejemplos a propiedades

En los tests típicos defines ejemplos concretos:

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0

En property-based testing defines propiedades invariantes que deben cumplirse para cualquier input válido:

  • "Para cualquier entero n, abs(n) >= 0"
  • "Para cualquier string s, reverse(reverse(s)) == s"
  • "Para cualquier lista lst, len(sorted(lst)) == len(lst)"

El framework genera cientos (o miles) de inputs aleatorios y verifica que la propiedad se cumpla. Encuentra edge cases que nunca hubieras escrito a mano.

Hipótesis matemática → testing

La idea viene de QuickCheck (Haskell): si puedes enunciar una propiedad matemática, el ordenador puede buscar contraejemplos. Hypothesis es la implementación más usada en Python.


Hypothesis: fundamentos

Instalación

pip install hypothesis

O con Poetry/uv:

poetry add --group dev hypothesis
uv add hypothesis --dev

Primer ejemplo: valor absoluto

from hypothesis import given
from hypothesis import strategies as st


@given(st.integers())
def test_absolute_value_always_positive(n):
    result = abs(n)
    assert result >= 0

Hypothesis generará muchos enteros (positivos, negativos, cero, muy grandes) y verificará que abs(n) >= 0 siempre se cumple.

Segundo ejemplo: reverse dos veces

def reverse(s: str) -> str:
    return s[::-1]


@given(st.text())
def test_reverse_twice_returns_original(s):
    assert reverse(reverse(s)) == s

La propiedad: invertir dos veces devuelve el original. Hypothesis prueba con strings vacíos, Unicode, emojis, etc.

Tercer ejemplo: sort preserva longitud

@given(st.lists(st.integers()))
def test_sort_preserves_length(lst):
    assert len(sorted(lst)) == len(lst)

La propiedad: ordenar no cambia el número de elementos.

Por qué property-based testing encuentra bugs que tú no ves

Con tests por ejemplo, eliges 3-5 inputs. Con property-based testing, Hypothesis puede generar 100+ en una sola ejecución. Y lo hace de forma inteligente: tiende a probar valores "interesantes" (0, -1, strings vacíos, listas muy largas) más a menudo que valores aleatorios puros. Eso significa que en pocas ejecuciones ya estás cubriendo muchos edge cases que un humano no habría priorizado.

Un ejemplo real: una función que parsea fechas como "YYYY-MM-DD". Un test por ejemplo probaría "2024-01-15". Pero Hypothesis podría generar "2024-02-30" (fecha inválida) o "0000-00-00" — y descubrir que tu parser no valida correctamente.


Estrategias comunes de Hypothesis

Tipos básicos

st.integers()           # Cualquier int (incluye negativos, cero, grandes)
st.floats()             # Floats (incluye NaN, Inf por defecto)
st.text()               # Strings Unicode
st.booleans()           # True/False
st.none()               # None

Con restricciones

st.integers(min_value=0, max_value=100)
st.floats(min_value=0.01, max_value=10000, allow_nan=False)
st.text(min_size=1, max_size=50)
st.text(alphabet=st.characters(whitelist_categories=("L", "N")))  # Solo letras y números

Estructuras compuestas

st.lists(st.integers())                    # Lista de enteros
st.lists(st.integers(), min_size=1)        # Al menos un elemento
st.dictionaries(st.text(), st.integers())  # Dict[str, int]
st.tuples(st.integers(), st.text())        # Tupla (int, str)
st.one_of(st.none(), st.integers())        # None o int
st.sampled_from(["a", "b", "c"])           # Uno de estos valores

Ejemplo combinado

@given(
    st.lists(st.floats(min_value=0.01, max_value=1000, allow_nan=False))
)
def test_sum_of_non_negative_floats(prices):
    total = sum(prices)
    assert total >= 0

Propiedades matemáticas típicas

Cuando no sabes qué inputs concretos probar, piensa en propiedades invariantes:

PropiedadEjemploCuándo usar
Idempotenciaf(f(x)) == f(x)Normalización, sanitización
Conmutatividadf(a, b) == f(b, a)Suma, unión de conjuntos
Invertibilidaddecode(encode(x)) == xSerialización, compresión
Preservación de estructuralen(sort(lst)) == len(lst)Ordenamiento, filtrado
Cota inferior/superiormin <= clamp(x, min, max) <= maxFunciones de restricción
No negatividadabs(x) >= 0Valores absolutos, distancias

Si tu función implementa una de estas ideas, Hypothesis puede verificarla automáticamente frente a muchos inputs.

Reproducir un fallo de Hypothesis

Cuando Hypothesis encuentra un contraejemplo, guarda la semilla para reproducirlo. Si el test falla, verás algo como:

hypothesis.invalid.Example: @given(...)
Falsifying example: test_foo(x=0, y=-1)

Para reproducir ese caso específico en futuras ejecuciones, usa @reproduce_failure:

from hypothesis import assume, given, reproduce_failure
from hypothesis import strategies as st

@given(st.integers(), st.integers())
def test_something(a, b):
    assume(b != 0)
    assert (a // b) * b + (a % b) == a  # No siempre cierto en Python

# Si falla, Hypothesis te da algo como:
# @reproduce_failure('6.0.0', b'AAEBAA==')
# Añade ese decorador temporalmente para depurar

En la práctica, sueles copiar el ejemplo que falló y escribir un test unitario explícito para ese caso, y luego corregir el código o la propiedad.


Ejemplo real: carrito de compras

Supongamos una clase ShoppingCart simple:

"""Implementación simple de carrito de compras para property testing."""


class ShoppingCart:
    def __init__(self):
        self._items: list[tuple[str, float]] = []

    def add_item(self, name: str, price: float) -> None:
        if price < 0:
            raise ValueError("Price must be non-negative")
        self._items.append((name, price))

    def total(self) -> float:
        return sum(price for _, price in self._items)

    def item_count(self) -> int:
        return len(self._items)

Propiedad: el total es la suma de los precios

import pytest
from hypothesis import given
from hypothesis import strategies as st

from shopping_cart import ShoppingCart


@given(
    st.lists(
        st.floats(min_value=0.01, max_value=10000, allow_nan=False),
        min_size=0,
    )
)
def test_cart_total_equals_sum_of_prices(prices):
    cart = ShoppingCart()
    for i, price in enumerate(prices):
        cart.add_item(f"item_{i}", price)
    assert cart.total() == pytest.approx(sum(prices))

pytest.approx maneja errores de precisión flotante. La propiedad: "para cualquier lista de precios válidos, el total del carrito coincide con la suma".

Propiedad: el número de items es la longitud de la lista añadida

@given(
    st.lists(
        st.tuples(
            st.text(min_size=1),
            st.floats(min_value=0.01, max_value=10000, allow_nan=False),
        ),
        min_size=0,
    )
)
def test_cart_item_count_matches_added(items):
    cart = ShoppingCart()
    for name, price in items:
        cart.add_item(name, price)
    assert cart.item_count() == len(items)

Cuándo usar cada enfoque

EnfoqueCuándo usarlo
Boundary testingConoces los límites exactos: edad 0–120, string max 100 chars, lista no vacía
Property testingConoces propiedades invariantes pero no todos los inputs posibles
Edge case promptsQuieres que Claude Code piense en escenarios que no se te ocurren

Los tres se complementan: boundaries para límites conocidos, properties para invariantes matemáticas, prompts para exploración creativa.

Propiedades típicas que puedes verificar

Cuando no se te ocurre qué propiedad testear, estas plantillas ayudan:

PropiedadEjemploCuándo aplica
Idempotenciaf(f(x)) == f(x)Normalización, sanitización
Conmutatividadf(a, b) == f(b, a)Suma, unión de conjuntos
Asociatividadf(f(a, b), c) == f(a, f(b, c))Operaciones matemáticas
Invarianza de tamañolen(transform(lst)) == len(lst)Map, filter sin descartar
Orden preservadoSi a < b entonces f(a) <= f(b)Funciones monótonas
Round-tripdecode(encode(x)) == xSerialización, compresión

Configuración avanzada de Hypothesis

Reproducir un fallo

Cuando Hypothesis encuentra un contraejemplo, puede mostrar algo como:

Falsifying example: test_sort_preserves_length(lst=[0, 0, -1])

Para reproducir ese mismo ejemplo en la siguiente ejecución, usa @seed o guarda el ejemplo. Hypothesis usa una semilla interna; si el test falla, imprime la semilla al final. Puedes forzarla:

from hypothesis import seed, given

@seed(123456789)
@given(st.lists(st.integers()))
def test_sort_preserves_length(lst):
    assert len(sorted(lst)) == len(lst)

Más útil: cuando falla, Hypothesis suele imprimir algo como @seed(12345). Añade ese decorador temporalmente para depurar el mismo ejemplo una y otra vez.

Ajustar número de ejemplos y tiempo

Por defecto Hypothesis genera unos 100 ejemplos por test. Si tu función es costosa o quieres más confianza:

from hypothesis import settings, given
from hypothesis import strategies as st

@settings(max_examples=500)
@given(st.integers())
def test_more_examples(n):
    assert abs(n) >= 0

O reducir para tests muy lentos:

@settings(max_examples=20, deadline=500)  # 20 ejemplos, 500ms por ejemplo
@given(st.lists(st.integers(), max_size=100))
def test_expensive_operation(lst):
    ...

Claude Code + Hypothesis

Prompt para generar property-based tests

Puedes pedir a Claude Code que genere tests con Hypothesis:

Genera property-based tests con Hypothesis para esta función. Define las propiedades que siempre deben ser verdaderas, elige las estrategias adecuadas y asegúrate de que el código sea ejecutable desde los imports.

Ejemplo de prompt y resultado

Tu prompt:

Tengo esta función que normaliza precios (redondea a 2 decimales, rechaza negativos). Genera 3 property-based tests con Hypothesis que verifiquen propiedades invariantes.

Código bajo prueba:

def normalize_price(price: float) -> float:
    if price < 0:
        raise ValueError("Price cannot be negative")
    return round(price, 2)

Resultado típico de Claude Code:

from hypothesis import given
from hypothesis import strategies as st


@given(st.floats(min_value=0, allow_nan=False))
def test_normalize_price_non_negative(price):
    result = normalize_price(price)
    assert result >= 0


@given(st.floats(min_value=0, allow_nan=False))
def test_normalize_price_at_most_two_decimals(price):
    result = normalize_price(price)
    s = str(result)
    if "." in s:
        decimal_places = len(s.split(".")[1])
        assert decimal_places <= 2
    else:
        assert True


@given(st.floats(min_value=0, allow_nan=False))
def test_normalize_price_idempotent(price):
    first = normalize_price(price)
    second = normalize_price(first)
    assert first == second

Claude Code puede definir propiedades que tú no habrías considerado (por ejemplo, idempotencia) y elegir estrategias adecuadas.


Ejercicios

Ejercicio 1: Boundary tests para rango de porcentaje

Escribe tests parametrizados para una función validate_discount(percent: float) -> bool que retorna True si 0 <= percent <= 100. Cubre todos los boundaries: debajo, mínimo, justo arriba del mínimo, justo debajo del máximo, máximo, arriba.

Ver solución
import pytest


def validate_discount(percent: float) -> bool:
    return 0 <= percent <= 100


@pytest.mark.parametrize("percent,expected", [
    (-0.01, False),   # Debajo del mínimo
    (0.0, True),      # Mínimo
    (0.01, True),     # Justo arriba del mínimo
    (99.99, True),    # Justo debajo del máximo
    (100.0, True),    # Máximo
    (100.01, False),  # Arriba del máximo
])
def test_validate_discount_boundaries(percent, expected):
    assert validate_discount(percent) == expected

Ejercicio 2: Property test para suma

La función add(a, b) suma dos números. Escribe un property-based test con Hypothesis que verifique: para cualquier par de enteros (a, b), add(a, b) == add(b, a) (conmutatividad).

Ver solución
from hypothesis import given
from hypothesis import strategies as st


def add(a: int, b: int) -> int:
    return a + b


@given(st.integers(), st.integers())
def test_add_commutative(a, b):
    assert add(a, b) == add(b, a)

Ejercicio 3: Estrategia para lista no vacía

Necesitas una estrategia que genere listas de strings con al menos un elemento. Escribe la estrategia y un test de propiedad que verifique: "para cualquier lista no vacía de strings, la concatenación de sus elementos tiene longitud >= 1".

Ver solución
from hypothesis import given
from hypothesis import strategies as st


@given(st.lists(st.text(), min_size=1))
def test_concatenation_non_empty(lst):
    concat = "".join(lst)
    # Si la lista no está vacía, al menos un string puede aportar longitud
    # Pero un string puede ser "" — así que la propiedad más segura es:
    assert len(lst) >= 1
    # O si sabemos que usamos min_size=1 y text(min_size=1):
    # assert len(concat) >= 1

Versión más restrictiva (strings no vacíos):

@given(st.lists(st.text(min_size=1), min_size=1))
def test_concatenation_non_empty_strict(lst):
    concat = "".join(lst)
    assert len(concat) >= 1

Ejercicio 4: Carrito con precios que pueden ser cero

Modifica el test del carrito para permitir precios exactamente 0 (productos gratis). ¿Qué estrategia usarías? ¿Hay que ajustar la implementación de add_item?

Ver solución

Si add_item rechaza price < 0, entonces price == 0 es válido. Solo cambia la estrategia:

@given(
    st.lists(
        st.floats(min_value=0, max_value=10000, allow_nan=False),  # 0 incluido
        min_size=0,
    )
)
def test_cart_total_with_zero_prices(prices):
    cart = ShoppingCart()
    for i, price in enumerate(prices):
        cart.add_item(f"item_{i}", price)
    assert cart.total() == pytest.approx(sum(prices))

Si la implementación actual exige price > 0, tendrías que relajarla para aceptar 0 o usar min_value=0.01 en los tests para que pasen sin cambiar el código. El ejercicio asume que aceptas 0; en ese caso, min_value=0 y la implementación debe permitirlo.

Ejercicio 5: Boundary + property para clamp

Implementa clamp(value, low, high) que retorna value si está en [low, high], o low/high si está fuera. Escribe:

  1. Boundary tests con parametrize para los valores límite.
  2. Un property test con Hypothesis: "el resultado siempre está en [low, high]".
Ver solución
from hypothesis import assume, given
from hypothesis import strategies as st
import pytest


def clamp(value: float, low: float, high: float) -> float:
    if value < low:
        return low
    if value > high:
        return high
    return value


@pytest.mark.parametrize("value,low,high,expected", [
    (-1, 0, 10, 0),
    (0, 0, 10, 0),
    (1, 0, 10, 1),
    (9, 0, 10, 9),
    (10, 0, 10, 10),
    (11, 0, 10, 10),
])
def test_clamp_boundaries(value, low, high, expected):
    assert clamp(value, low, high) == expected


@given(
    value=st.floats(allow_nan=False),
    low=st.floats(allow_nan=False),
    high=st.floats(allow_nan=False),
)
def test_clamp_always_in_range(value, low, high):
    assume(low <= high)  # Hypothesis: assume para filtrar inputs inválidos
    result = clamp(value, low, high)
    assert low <= result <= high

Ejercicio 6: Prompt a Claude Code

Toma una función propia (o la de clamp del ejercicio anterior) y escribe un prompt para Claude Code pidiendo: (a) boundary tests parametrizados, (b) dos property-based tests con Hypothesis. Copia el prompt y describe qué propiedades sugerirías si Claude Code te pide ayuda.

Ver solución

Prompt ejemplo:

Tengo esta función:

def clamp(value: float, low: float, high: float) -> float:
    if value < low: return low
    if value > high: return high
    return value

Necesito:

  1. Boundary tests con pytest.mark.parametrize cubriendo value debajo de low, en low, entre low y high, en high, y arriba de high.
  2. Dos property-based tests con Hypothesis. Propiedades que quiero verificar: (a) el resultado siempre está en [low, high], (b) si value ya está en [low, high], el resultado es value.

Genera el código completo con imports.

Propiedades que podrías sugerir:

  • El resultado siempre está entre low y high (inclusivo).
  • Si low <= value <= high, entonces clamp(value, low, high) == value.
  • clamp es idempotente: clamp(clamp(v,l,h), l, h) == clamp(v,l,h).

Troubleshooting

Hypothesis encuentra un contraejemplo pero no sé cuál

Causa: Hypothesis guarda un ejemplo que falló y lo usa en la próxima ejecución, pero puede que no lo imprima claramente.

Solución: Ejecuta con más verbosidad: pytest -v -s. Hypothesis suele mostrar el ejemplo que falló. También puedes usar @given(...) con print temporal o hypothesis.settings(verbosity=2) para ver qué se genera.

Flaky o tests que a veces pasan y a veces no

Causa: La función bajo prueba o la propiedad tiene comportamiento no determinista, o la estrategia genera valores "problemáticos" (NaN, Inf, muy grandes) que no manejas.

Solución: Restringe las estrategias: allow_nan=False, min_value/max_value acotados. Usa hypothesis.assume() para filtrar inputs que no cumplen precondiciones (por ejemplo, assume(low <= high)).

DeadlineExceeded — el test tarda demasiado

Causa: Hypothesis ejecuta muchas iteraciones y tu función es lenta, o la estrategia genera estructuras muy grandes.

Solución: Limita el tamaño: st.lists(..., max_size=100), st.text(max_size=500). O desactiva el deadline: @settings(deadline=None) (solo para casos justificados).

Property test pasa pero un boundary explícito falla

Causa: La estrategia no está generando el boundary (por ejemplo, 0 o 121 en un rango muy amplio) con alta probabilidad en pocas ejecuciones.

Solución: No confíes solo en property testing para boundaries conocidos. Usa boundary tests explícitos con parametrize para los límites que importan.

ImportError o Hypothesis no está instalado

Causa: Hypothesis no está en el entorno de ejecución.

Solución: pip install hypothesis o poetry add hypothesis --group dev. Verifica que el entorno del IDE y la terminal sean el mismo (which python, poetry env info).


Conexión con el proyecto del módulo

El proyecto del módulo es Suite con 90%+ coverage. Boundary value analysis y property-based testing son herramientas opcionales pero muy recomendables:

  1. Coverage te dice dónde hay gaps — boundary tests te ayudan a cubrir ramas de condicionales en límites (if x < 0, if x > max, etc.).
  2. Property-based testing descubre inputs que nunca escribirías a mano y que podrían exponer bugs en funciones matemáticas, validadores o transformaciones.
  3. Claude Code puede generar ambos tipos de tests si le das el código y un prompt claro.

Flujo sugerido: mide coverage → identifica funciones con lógica en límites → pide boundary tests a Claude Code → añade 1–2 property tests para funciones con propiedades claras (sort, sum, normalización) → mide de nuevo.


Resumen

  • ✅ Los bugs se concentran en boundaries: mínimo, máximo, cero, uno más/menos, vacío/lleno.
  • ✅ Usa @pytest.mark.parametrize para boundary tests sistemáticos en límites conocidos.
  • ✅ Property-based testing con Hypothesis: defines propiedades invariantes y el framework genera muchos inputs.
  • ✅ Estrategias habituales: st.integers(), st.text(), st.lists(), st.floats() con min_value/max_value.
  • ✅ Boundary testing para límites conocidos; property testing para propiedades; prompts para exploración con Claude Code.
  • ✅ Puedes pedir a Claude Code que genere boundary y property tests dando el código y requisitos claros.

Próxima cápsula: Proyecto — Suite con 90%+ coverage. Aplicarás todo lo del módulo para llevar un código existente a ≥90% coverage.


Recursos Adicionales

  1. Hypothesis Documentation — Documentación oficial de Hypothesis
  2. Hypothesis Strategies — Catálogo de estrategias
  3. Property-Based Testing with Hypothesis — Artículo sobre property-based testing
  4. QuickCheck: The Original — Origen del enfoque (Haskell)
  5. Boundary Value Analysis — Software Testing Fundamentals — Introducción a boundary testing
  6. Test-Driven Development with Property-Based Testing — Propiedad + TDD

Módulo 5, Cápsula 05 — Testing with Claude Code Guide