La paradoja de la cobertura del 100% — o por qué sus pruebas podrían estar mintiendo
\n\nEscribió código perfecto en Rust. El compilador está contento, clippy calla, las pruebas pasan en verde, y el informe de cobertura proclama con orgullo: el 100% de las líneas están cubiertas. ¿Puede relajarse? No. Las pruebas de mutación en Rust son precisamente la técnica que le mostrará que sus pruebas, posiblemente, están protegiendo el aire, no la lógica.
\n\nImagine: accidentalmente cambia + por - en una función crítica. ¿Las pruebas siguen en verde? Felicidades, ha encontrado un mutante — un defecto que vive en su código, pero que las pruebas no detectan. En este artículo, analizaremos cómo las pruebas mutacionales en Rust ayudan a encontrar estos puntos ciegos, y lo implementaremos en un proyecto real.
¿Qué son las pruebas de mutación (y por qué Rust es ideal para ellas)?
\n\nLas pruebas de mutación (o pruebas mutacionales) es un método para evaluar la calidad de las pruebas, en el que se introducen pequeños cambios artificiales (mutaciones) en el código fuente, y luego se ejecutan las pruebas. Si las pruebas no fallan con el código mutado, significa que el mutante ha sobrevivido. Cuantos más mutantes sobrevivan, peores son sus pruebas.
\n\nRust es un lenguaje que por sí mismo detecta clases enteras de errores en la etapa de compilación: condiciones de carrera, punteros nulos, use-after-free. Pero las pruebas de mutación en Rust se centran precisamente en los errores lógicos — lo que el compilador no ve. Y esto hace de Rust una plataforma ideal: ya está protegido contra el 70% de los errores típicos, y las pruebas mutacionales eliminan el 30% restante.
\n\n¿Cómo se ve una mutación en el código Rust?
\n\nLas mutaciones típicas incluyen:
\n- \n
- Cambio de operadores:
a + b→a - b\n - Inversión de condiciones:
if x > 5→if x < 5\n - Reemplazo de constantes:
let n = 42→let n = 0\n - Eliminación de llamadas a funciones:
process(data)→ llamada vacía \n - Cambio de límites de iteración:
0..n→0..=n\n
Si después de tal mutación al menos una prueba no falla — significa que perdió el tiempo escribiendo esa prueba. No está verificando lo que debería.
\n\nHerramientas para pruebas de mutación en Rust: cargo-mutants
\n\nHoy en día, la herramienta principal para las pruebas mutacionales en Rust es cargo-mutants. Es madura, se mantiene activamente y proporciona informes legibles. La instalación es sencilla:
cargo install cargo-mutants\n\n\nDespués de eso, simplemente ejecute:
\n\ncargo mutants\n\n\nY la herramienta encontrará todas las mutaciones en su código, ejecutará las pruebas y generará un informe HTML con indicación de colores: verde — mutantes eliminados (las pruebas funcionaron), rojo — sobrevivientes (problema).
\n\nAlternativas en el radar
\n- \n
- mutagen — más antiguo, pero aún funcional. Menos mutaciones, más rápido en proyectos pequeños. \n
- cargo-insta — no es exactamente pruebas de mutación, pero está cerca: ayuda a verificar visualmente los cambios en las instantáneas de prueba. \n
Para trabajo serio, recomiendo cargo-mutants — ofrece el mejor equilibrio entre velocidad y profundidad de análisis.
Ejemplo práctico: probando una calculadora de descuentos
\n\nSupongamos que tenemos un módulo para calcular descuentos en una tienda en línea en Rust:
\n\n// src/discount.rs\n\npub fn calculate_discount(price: f64, customer_age: u32) -> f64 {\n let mut discount = 0.0;\n\n // 10% de descuento para jubilados (mayores de 65)\n if customer_age > 65 {\n discount += 0.1;\n }\n\n // 5% de descuento adicional por compras superiores a 1000 rublos\n if price > 1000.0 {\n discount += 0.05;\n }\n\n // Descuento máximo no superior al 30%\n if discount > 0.30 {\n discount = 0.30;\n