Introducción: ¿Los tests pasan, pero los bugs persisten?
\n\nImagine la situación: ha escrito un módulo en Go, lo ha cubierto con tests unitarios al 95% según go test -cover, todos los tests están en verde. Está seguro de que el código funciona perfectamente. Pero en producción aparece un bug. ¿Le suena?
El problema es que la métrica de cobertura de código (code coverage) es solo la cantidad de líneas que se ejecutaron. No indica qué es lo que realmente verifican los tests. Se puede tener un 100% de cobertura, pero nunca comprobar que una función devuelve el valor correcto, y no solo que no falla con un pánico.
\n\nAquí es donde entran en escena las pruebas de mutación (mutation testing) en Go: una técnica que introduce errores artificialmente en su código y comprueba si los tests pueden detectarlos. Si los tests no fallan con el código mutado, significa que sus tests son inútiles.
\n\nEn este artículo, analizaremos qué son las pruebas de mutación en Go, cómo configurarlas y por qué son imprescindibles para proyectos donde la fiabilidad es crítica. ¡Vamos allá!
\n\n¿Qué son las pruebas de mutación?
\n\nLas pruebas de mutación son un método para evaluar la calidad de los tests, en el que se introducen pequeños cambios (mutaciones) en el programa que simulan errores típicos de un desarrollador. Luego se ejecutan los tests. Si al menos un test falla, la mutación está "muerta" y el test funciona. Si todos los tests pasan, la mutación "sobrevive", y eso es una bandera roja: tiene un agujero en sus tests.
\n\nEn Go, las pruebas de mutación a menudo se denominan mutation testing golang, y son especialmente útiles para verificar la lógica de negocio, validadores, analizadores sintácticos (parsers) y otros componentes críticos.
\n\nEjemplo de mutación
\n\nSupongamos que tiene una función simple:
\n\nfunc IsEven(n int) bool {\n return n%2 == 0\n}\n\nUna mutación típica es reemplazar == por !=:
func IsEven(n int) bool {\n return n%2 != 0\n}\n\nSi su test solo verifica que IsEven(2) no falla, pero no comprueba que el resultado sea true, entonces la mutación sobrevivirá. El test pasará, aunque la función ahora funcione exactamente al revés.
¿Por qué la simple cobertura de código no es suficiente?
\n\nMuchos desarrolladores en Go se acostumbran a confiar en go test -cover. Es una métrica útil, pero engañosa. Consideremos un ejemplo:
func Divide(a, b int) (int, error) {\n if b == 0 {\n return 0, errors.New("division by zero")\n }\n return a / b, nil\n}\n\nTest:
\n\nfunc TestDivide(t *testing.T) {\n result, err := Divide(10, 2)\n if err != nil {\n t.Fatal(err)\n }\n if result != 5 {\n t.Errorf("expected 5, got %d", result)\n }\n}\n\n¿Cobertura? 100%: se ejecutaron todas las líneas. Pero, ¿qué pasa si alguien accidentalmente reemplaza a / b por a * b? El test no lo notará, porque 10 * 2 = 20, y esperábamos 5. ¿Fallará el test? No, si no verificamos el valor exacto.
Las pruebas de mutación en Go detectarán inmediatamente este problema, creando un mutante con la multiplicación y comprobando si el test falla.
\n\nHerramientas para pruebas de mutación en Go
\n\nLa herramienta principal es go-mutesting. Es una biblioteca popular que crea mutaciones automáticamente y ejecuta los tests. Instalación:
\n\ngo get github.com/zimmski/go-mutesting/cmd/go-mutesting\n\nTambién existen alternativas, como mutesting de la empresa "Go Test It", pero go-mutesting es el estándar de facto.
Cómo ejecutarlo
\n\nEjecución básica:
\n\ngo-mutesting ./...\n\nEste comando encontrará todos los archivos _test.go en el proyecto y comprobará qué mutaciones sobreviven. El resultado se muestra en la consola con un informe detallado.
Ejemplo práctico: pruebas de mutación en Go
\n\nCreemos un paquete simple calculator con funciones de suma y