Pruebas de mutación en C#: rompemos el código para mejorarlo

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Cuando las pruebas pasan, pero el código sigue «roto»

\n\n

Imagina la situación: escribiste pruebas unitarias para un proyecto en C#, dotnet test da luz verde, la cobertura de código (code coverage) es del 85%. El equipo está contento, el CI/CD se ejecuta perfectamente. Pero un mes después aparece un error en producción que, al parecer, las pruebas unitarias deberían haber detectado. ¿Te suena familiar?

\n\n

El problema es que la cobertura de código (code coverage) es una métrica engañosa. Muestra qué líneas de código fueron ejecutadas, pero no verifica la calidad de las propias comprobaciones. Puedes tener un 100% de cobertura, pero ni una sola aserción significativa. Aquí es donde entran en escena las pruebas de mutación en C#: una metodología que «rompe» tu código para comprobar qué tan bien tus pruebas pueden detectar errores.

\n\n

En este artículo analizaremos qué son las pruebas de mutación, cómo implementarlas en proyectos .NET con Stryker.NET, y por qué son imprescindibles para cualquier desarrollador serio de C#.

\n\n

¿Qué son las pruebas de mutación (Mutation Testing)?

\n\n

Las pruebas de mutación son un método para evaluar la calidad de las pruebas, basado en la introducción de pequeños cambios intencionales (mutaciones) en el código fuente del programa. Cada una de estas versiones modificadas del código se denomina mutante (mutant).

\n\n

La lógica es simple:

\n
    \n
  • Se toma tu código de producción.
  • \n
  • Se generan mutantes automáticamente — por ejemplo, el operador + se reemplaza por -, la condición if (x > 5) se cambia a if (x >= 5), y true por false.
  • \n
  • Se ejecutan tus pruebas unitarias contra cada mutante.
  • \n
  • Si la prueba falla — el mutante está «muerto» (killed). Buena prueba.
  • \n
  • Si la prueba pasa — el mutante «sobrevive» (survived). Mala señal: tu prueba no detectó el cambio de lógica.
  • \n
\n\n

El objetivo es lograr un alto porcentaje de mutantes muertos (normalmente 80%+). Esto es una garantía real de que tus pruebas verifican el comportamiento, y no solo «recorren» el código.

\n\n

¿Por qué la cobertura de código es insuficiente?

\n\n

Consideremos un ejemplo simple en C#. Supongamos que tenemos un método que verifica si un número es par:

\n\n
public class MathUtils\n{\n    public bool IsEven(int number)\n    {\n        return number % 2 == 0;\n    }\n}
\n\n

Y una prueba «ideal» con 100% de cobertura:

\n\n
[Test]\npublic void IsEven_ShouldReturnTrueForEvenNumber()\n{\n    var utils = new MathUtils();\n    var result = utils.IsEven(4);\n    Assert.IsTrue(result);\n}
\n\n

La prueba es verde, cobertura del 100%. Pero ¿qué pasa si alguien accidentalmente (o maliciosamente) cambia el método a return number % 2 == 1;? ¿Nuestra única prueba fallará? ¡No! Probará el número 4 (4 % 2 == 0, pero ahora el método devolverá false), la aserción fallará — el mutante será muerto. ¿Todo bien? Espera.

\n\n

¿Y si cambiamos el código a return number % 2 != 1;? Lógicamente es lo mismo, pero las pruebas de mutación crearán un mutante: return number % 2 != 0;. Nuestra prueba con el número 4 seguirá pasando (4 % 2 != 0 — false, el método devolverá false, pero la aserción espera true — la prueba fallará). Aquí todo está bien. Pero imagina que escribimos la prueba así:

\n\n
[Test]\npublic void IsEven_ShouldNotThrow()\n{\n    var utils = new MathUtils();\n    Assert.DoesNotThrow(() => utils.IsEven(4));\n}
\n\n

La cobertura sigue siendo del 100%, ¡pero la aserción no verifica el resultado! Ahora cualquier mutante sobrevivirá. Las pruebas de mutación mostrarán inmediatamente que tus pruebas son vacías.

\n\n

Herramientas para pruebas de mutación en C#

\n\n

La herramienta principal y prácticamente estándar para .NET es Stryker.NET. Es un framework de código abierto que se integra fácilmente en cualquier proyecto de C#. Soporta xUnit, NUnit y MSTest.

\n\n

Otras herramientas (menos populares):

\n
    \n
  • VisualMutator — un plugin obsoleto para Visual Studio.
  • \n
  • Nester
  • \n

Blogs

Book Recommendations