Yeison Daza
4 min de lectura

Los tests necesitan ganarse su lugar en mi codebase

Durante las últimas semanas se ha hablado bastante sobre tests. Ahora que la IA puede escribirlos, es fácil terminar con una suite que parece saludable: muchos casos, buen coverage y checks verdes en CI.

Pero eso no significa necesariamente que nos esté protegiendo de errores.

Un test puede ser solo un eco de la implementación. Puede comprobar detalles de cómo está escrito el código, sin comprobar el comportamiento que debería mantenerse. Si cambia la implementación, cambiamos el test con ella. Y si introducimos un bug, quizás todos siguen pasando.

He visto dos respuestas a esto:

  1. Eliminar todos los tests del codebase.
  2. Dejar de escribir unit tests y reemplazarlos por E2E tests.

Ambas tienen pros y contras. Pero mi conclusión ha sido otra:

Los tests necesitan ganarse su lugar en mi codebase.

Un test debería dar confianza

Para mí, un buen test es uno que me da confianza cuando el código cambia.

Me debería ayudar a detectar errores cuando alguien modifica una parte del sistema sin tener todo el contexto. No importa si ese alguien es una persona, una IA o yo mismo dentro de seis meses.

El código cambia. Refactorizamos, añadimos funcionalidades y corregimos bugs. Los tests deberían demostrar que pueden detectar los cambios que rompen comportamientos importantes.

No deberían existir solo porque pasan en CI, porque mantienen el coverage o porque alguna vez fueron escritos.

Retar a los tests

Por eso implementé mutation testing en mi proyecto.

La idea es sencilla: introducir cambios deliberados en el código y luego ejecutar los tests. Por ejemplo, cambiar una condición, invertir una comparación, modificar un valor retornado o eliminar una llamada.

Si ningún test falla después de ese cambio, la mutación sobrevive.

Una mutación superviviente no significa automáticamente que haya un problema. Puede ser un cambio estético o una parte del código sin impacto real. Pero sí es una señal útil: probablemente hay un comportamiento que nuestros tests no están verificando.

En vez de preguntarnos cuántas líneas cubrimos, podemos hacer una pregunta más útil:

Si este código cambiara de una forma incorrecta, ¿nuestros tests se darían cuenta?

Hacerlo a escala

Mutation testing puede ser muy costoso. Mutar todos los archivos y ejecutar todos los tests después de cada mutación, en cada PR, no es viable. Sería demasiado lento y terminaría bloqueando el flujo de desarrollo.

Mi enfoque es usarlo como un proceso adicional en CI:

  1. Lee los cambios del PR.
  2. Mapea esos cambios a los test cases relacionados.
  3. Muta únicamente el código afectado.
  4. Ejecuta los tests relevantes.

Así el proceso toma aproximadamente cinco minutos en CI.

También tomé una decisión importante: no bloquea a nadie. El PR puede mergearse y el equipo puede continuar trabajando. Cuando el mutation testing termina, publica un reporte directamente en el PR con las mutaciones que sobrevivieron.

No quiero convertir otra métrica en una barrera para entregar código. Quiero obtener señal sobre dónde nuestra suite tiene puntos ciegos.

Cerrar los gaps

Encontrar mutaciones supervivientes es solo la primera parte. Lo útil ocurre después.

Tengo una tarea programada que, cada mañana, revisa los reportes generados el día anterior. Para cada mutación decide si representa un gap real o si se trata de un cambio irrelevante. Cuando es un gap real, ajusta los tests para cubrir ese comportamiento.

El resultado ha sido cerrar alrededor de 100 gaps en los test cases cada día. Y casi todos los días el proceso encuentra bugs reales en el codebase.

No porque los tests fueran inútiles, sino porque todavía no estaban protegiendo todo lo que creíamos que protegían.

Un proceso continuo

La idea no es que los tests sean un artefacto pasivo que corre en CI, ni que existan para aumentar una métrica.

Su trabajo es aumentar nuestra confianza: que, si alguien modifica el código que escribimos, puedan detectar ese cambio y evitar que introduzcamos errores.

Con mutation testing, los tests dejan de ser algo que escribimos y olvidamos. Se enfrentan continuamente a cambios deliberados, revelan sus puntos ciegos y mejoran con el tiempo.

No busco un mutation score perfecto, ni bloquear a todo el equipo con otra validación lenta. Busco un proceso que rete a los tests cada vez que el código cambia y que, día a día, haga más difícil que un error llegue a producción.

Los tests necesitan ganarse su lugar en mi codebase.