Ir al contenido
angel.library

Property-based testing y mutation testing

Los ejemplos concretos siguen siendo esenciales, pero no siempre descubren combinaciones inesperadas. El property-based testing genera muchas entradas y comprueba una propiedad que debe cumplirse para todas. El mutation testing modifica temporalmente el código para comprobar si la suite detecta el defecto. Son técnicas diferentes y complementarias.

Ventana de terminal
pnpm add -D fast-check

Una propiedad o invariante describe algo que siempre debe ser cierto. En una función de ordenamiento, la salida conserva la longitud, contiene los mismos elementos y queda ordenada; no necesitas predecir cada arreglo generado.

import fc from "fast-check"
import { describe, expect, it } from "vitest"
describe("sortNumbers", () => {
it("conserva los elementos y produce orden ascendente", () => {
fc.assert(
fc.property(fc.array(fc.integer()), (input) => {
const output = sortNumbers(input)
expect(output).toHaveLength(input.length)
expect([...output].sort((a, b) => a - b)).toEqual(output)
expect([...output].sort((a, b) => a - b)).toEqual(
[...input].sort((a, b) => a - b)
)
})
)
})
})

fast-check intenta reducir —shrink— una entrada que falla hasta obtener un contraejemplo pequeño. Si el error aparece con un arreglo enorme, podría terminar reportando algo tan simple como [0, -1], que es más fácil de diagnosticar.

El arbitrary es el generador de valores. Debe representar entradas válidas o inválidas según la propiedad, no ruido sin relación con el dominio.

const userArbitrary = fc.record({
id: fc.uuid(),
age: fc.integer({ min: 0, max: 120 }),
email: fc.emailAddress()
})

Cuando una propiedad falla, conserva el seed y el path reportados para reproducirla. Después añade un ejemplo concreto si el contraejemplo documenta una regla importante.

  • ida y vuelta: decode(encode(value)) conserva el valor;
  • idempotencia: normalizar dos veces produce lo mismo que una;
  • límites: el resultado nunca excede el rango permitido;
  • conservación: una transformación no pierde elementos;
  • equivalencia: una implementación nueva coincide con una referencia confiable.

No repitas la implementación dentro de la propiedad: si calculas el resultado con el mismo algoritmo, ambos pueden compartir el mismo error. Combina propiedades con ejemplos explícitos que documenten casos de negocio.

  • “no lanza error” puede aceptar resultados incorrectos;
  • comparar dos implementaciones copiadas comparte el mismo defecto;
  • generar cualquier string para un dominio muy específico desperdicia casos;
  • una propiedad sin límites puede crear entradas irreales y lentas;
  • afirmar solamente tipos o longitudes puede omitir semántica.

Empieza con un oráculo independiente: una ley matemática, una ida y vuelta, un modelo sencillo o una invariante del negocio.

Una herramienta de mutación cambia operadores como > por >=, elimina una condición o sustituye un valor. Si todos los tests siguen verdes, el mutante “sobrevivió”: esa parte del comportamiento no está protegida por una aserción capaz de detectar el cambio.

ResultadoSignificado
Mutante eliminadoal menos una prueba detectó el defecto
Mutante sobrevivientefalta una prueba o una aserción útil
Mutante equivalenteel cambio no altera comportamiento observable
Timeout/errorrevisar costo, aislamiento o configuración

La puntuación de mutación orienta, no es una meta absoluta. Úsala en lógica crítica y módulos estables; ejecutarla sobre todo el repositorio puede ser costoso. Primero revisa sobrevivientes con impacto real: permisos, cálculos, validación y estados.

  1. Ejecuta primero la suite normal y elimina flakiness.
  2. Limita mutación a módulos críticos o modificados.
  3. Revisa sobrevivientes por impacto, no solo por cantidad.
  4. Añade una aserción si existe un comportamiento observable faltante.
  5. Marca mutantes equivalentes con justificación.
  6. Ejecuta el conjunto amplio de forma programada si es costoso.

La herramienta puede generar timeouts porque cada mutante vuelve a ejecutar pruebas. Excluir código generado o trivial es razonable; excluir lógica difícil porque baja el score elimina el valor de la técnica.

Usa generación de propiedades en parsers, serialización, cálculos y algoritmos con un dominio grande. Usa mutación cuando una suite tiene buena cobertura de líneas, pero no sabes si sus aserciones son sensibles. Para UI y flujos completos, las técnicas convencionales y E2E suelen comunicar mejor la intención.