Pruebas de mutación en C++: cuando el 100% de cobertura de código es una mentira
\n\n¿Está orgulloso de su 95% de code coverage? Quizás no debería. La cobertura de código es como una cerradura que se revisa simplemente tirando de la manija. Y las pruebas de mutación son un ladrón que realmente intenta abrir la caja fuerte. En este artículo, analizaremos cómo las pruebas de mutación en C++ transforman sus pruebas de una formalidad en un arma real. Y sí, no vamos a copiar y pegar definiciones aburridas de Wikipedia.
\n\nImagine: escribió una función, la cubrió con pruebas, el CI está en verde. Y luego un error llega a producción. ¿Le suena familiar? La mayoría de las veces, el problema no está en el código, sino en las pruebas que verifican lo que no deben. Mutation testing para C++ resuelve exactamente este problema: muestra qué tan peligrosas son realmente sus pruebas para los errores.
\n\n¿Qué son las pruebas de mutación y por qué las necesita un desarrollador de C++?
\n\nLas pruebas de mutación (o mutation testing) son una metodología para evaluar la calidad de las pruebas mediante la introducción de pequeños cambios (mutaciones) en el código fuente. Si después de la mutación las pruebas aún pasan, significa que son débiles. El mutante sobrevivió. Si las pruebas fallan, el mutante ha sido eliminado, la prueba funciona.
\n\nPara C++, esto es especialmente relevante porque:
\n- \n
- El código C++ a menudo contiene lógica compleja con punteros, plantillas y RAII. \n
- Las métricas de cobertura estándar (line coverage, branch coverage) son fáciles de engañar. \n
- El costo de un error en C++ (especialmente en embedded, finanzas, motores de juegos) puede ser catastrófico. \n
Un ejemplo simple: tiene una función para verificar la edad:
\n\nbool is_adult(int age) {\n return age >= 18;\n}\n\nPrueba:
\n\nTEST(AdultTest, Positive) {\n EXPECT_TRUE(is_adult(20));\n}\n\nCobertura: 100% de líneas. Pero la mutación age >= 18 → age > 18 no será detectada, porque 20 aún pasa. El mutante sobrevivió. Y si hubiera tenido una prueba para is_adult(18) y is_adult(17), habría atrapado el error. Las pruebas de mutación en C++ le obligan a escribir estas pruebas de límite.
Operadores de mutación principales en C++
\n\nLas herramientas de pruebas de mutación aplican un conjunto de operadores (mutators). Estos son los más comunes para C++:
\n\n- \n
- Reemplazo de operadores aritméticos:
+→-,*→/\n - Reemplazo de operadores lógicos:
&&→||,==→!=\n - Cambio de condiciones límite:
>=→>,<→<=\n - Eliminación de expresiones: la llamada a función se reemplaza por una constante \n
- Cambio del valor de retorno:
return true→return false\n - Mutación de bucles: cambio de la condición de salida o del paso \n
Ejemplo con código real:
\n\nint factorial(int n) {\n if (n <= 1) return 1;\n return n * factorial(n - 1);\n}\n\nMutación #1: n <= 1 → n < 1. Si la prueba llama a factorial(1) y espera 1, pero la mutación elimina el caso base, la prueba fallará solo si verifica factorial(0) o factorial(1) por separado.
Mutación #2: return n * factorial(n - 1) → return n + factorial(n - 1). Si las pruebas solo son para números positivos, es posible que no detecten la sustitución de multiplicación por suma.
Estos son exactamente los casos que revela el mutation testing en C++.
\n\nHerramientas para pruebas de mutación en C++
\n\nNo todas las herramientas son igualmente útiles. Estos son los tres principales actores en el mercado de C++:
\n\n1. Mull
\nProyecto moderno y activo. Funciona basado en LLVM pass. Soporta C++17/20, se integra con Google Test, Catch2, doctest