La vez que confié más en mi intuición que en los tests y lo pagué caro

Este artículo puede incluir enlaces de afiliado. Si compras a través de ellos, me llevo una pequeña comisión sin coste extra para ti. Más información.

Hay un momento muy concreto en el que sabes que has metido la pata. No cuando te llega el aviso del sistema. No cuando un compañero te escribe por Slack con un mensaje de tres palabras y un punto final. Sino antes. Justo cuando haces el despliegue y piensas «seguro que funciona» por segunda vez en diez segundos, como para convencerte.

Ese momento lo viví hace un par de años. Y lo que aprendí de él no tiene que ver con los tests en sí, sino con algo más incómodo: la diferencia entre entender el código que escribes y entender el sistema en el que vive ese código.

El contexto: un cambio «obvio»

Estaba trabajando en un módulo de facturación. Nada del otro mundo, un servicio que calculaba importes según una serie de reglas de negocio que llevaban años ahí. El cambio que me pedían era pequeño: ajustar cómo se aplicaba un redondeo en casos con descuentos combinados.

Tenía tests. Buenos tests, además. De los que cubrían los casos principales, los casos borde, situaciones que en teoría nunca pasan pero que alguien tuvo la cabeza fría de contemplar tiempo atrás. Y yo los miré, entendí lo que hacían, y decidí que para este cambio concreto no necesitaba añadir ninguno nuevo ni tocar los existentes. Porque «era obvio». Porque «lo entendía». Porque llevaba suficiente tiempo en el proyecto como para saber cómo funcionaba aquello.

Pasé los tests existentes. Verde. Hice el despliegue. Y al día siguiente, producción empezó a generar facturas con importes incorrectos en un porcentaje pequeño pero nada despreciable de casos.

Lo que falló (y por qué no lo vi)

El error era sutil. El redondeo que yo había modificado interactuaba con otro proceso que se ejecutaba de forma asíncrona y que acumulaba valores antes de presentarlos. Yo había probado mi lógica de forma aislada. Funcionaba perfectamente de forma aislada. Pero en el sistema real, ese valor pasaba por otra capa que yo no había tenido en cuenta porque, sinceramente, ni recordaba que existía.

No era ignorancia del lenguaje. No era un fallo de sintaxis ni un despiste tonto. Era que yo había entendido el código que toqué, pero no había entendido el sistema completo en el que ese código opera. Y esa distinción es importante. Es, de hecho, la distinción que separa cometer este tipo de errores de no cometerlos.

developer staring at glowing monitors late at night, surrounded by code reflections, dark cinematic atmosphere
Imagen generada con IA

Si hubiera escrito un test de integración que simulara el flujo completo, lo habría pillado. No porque el test fuera mágico, sino porque el acto de escribirlo me habría obligado a pensar en ese flujo. A recorrerlo. A preguntarme qué más toca este cambio.

«El testing no puede establecer la ausencia de bugs, solo su presencia.»

— Edsger W. Dijkstra

Cito a Dijkstra no para ponerme filosófico, sino porque esa frase captura exactamente lo que me pasó desde el otro lado: mis tests no fallaron porque no había un bug en lo que probaban. El bug estaba en lo que no probaban. Decidí que mi intuición cubría ese hueco. Y no lo cubría.

La trampa del «lo entiendo»

Cuanto más tiempo llevas en un proyecto, más peligrosa se vuelve esta trampa. Paradójicamente. Porque acumulas contexto real, experiencia con el código base, intuiciones que muchas veces son correctas. Y eso genera una confianza que en su mayor parte es legítima, pero que en algún momento empieza a sustituir pasos que no deberían saltarse.

Escribir un test no es solo una red de seguridad. Es también un ejercicio de especificación. Te obliga a articular exactamente qué debe hacer el código, bajo qué condiciones, con qué entradas y qué salidas. Cuando lo saltas, a veces también saltas ese ejercicio mental. Y puedes hacerlo sin notarlo, porque la solución «te parece clara».

La intuición tiene un scope

Lo que aprendí es que la intuición técnica es válida dentro de un scope. Puedo confiar en mi intuición sobre si una estructura de datos tiene sentido, sobre si un método está haciendo demasiado, sobre si un nombre es claro o confuso. Son cosas que vivo en el código directamente.

Pero el comportamiento emergente de un sistema distribuido, con procesos asíncronos, con estados que se acumulan en capas que no siempre están visibles desde donde estás trabajando… eso está fuera del scope de la intuición. Ahí necesitas herramientas que te fuercen a recorrer el camino completo. Los tests de integración son exactamente eso.

// Ejemplo simplificado: un test de integración que cubre el flujo completo
// en lugar de testear la lógica de redondeo en aislamiento

@Test
void deberia_calcular_importe_final_correctamente_con_descuento_combinado() {
    // Arrange
    Factura factura = new Factura();
    factura.agregarLinea(new LineaFactura("Servicio A", 100.00, 0.10));
    factura.agregarLinea(new LineaFactura("Servicio B", 50.00, 0.15));
    factura.aplicarDescuentoGlobal(0.05);

    AcumuladorImportes acumulador = new AcumuladorImportes();

    // Act
    double importeFinal = acumulador.procesar(factura); // el flujo real, no el método aislado

    // Assert
    assertEquals(138.04, importeFinal, 0.001);
}

Parece un detalle menor: testear el método de redondeo directamente versus testear el flujo que lo incluye. Pero ese detalle es el que marca la diferencia entre detectar el error antes o después del despliegue.

Lo que cambié después

No cambié nada dramático. No me volví un fanático del TDD ni empecé a escribir tests para todo lo que se me ocurre. Lo que cambié es más pequeño y más concreto: cuando tengo la tentación de no escribir un test porque «es obvio», me pregunto si lo que me parece obvio es el código o el sistema. Si la respuesta es el código, es probable que necesite un test.

También empecé a tratarme peor a mí mismo en ese momento de confianza previa al despliegue. No de forma masoquista, sino de forma útil. Si noto que me estoy convenciendo de algo en lugar de verificarlo, suele ser señal de que algo falla en mi razonamiento, no en el código.

Kika tiene una habilidad que envidio a veces: cuando algo le parece raro, se queda mirándolo hasta que lo entiende del todo. No da el salto hasta que está segura. Yo di el salto. Y aprendí que el tiempo que «ahorro» saltándome un test no compensa ni de lejos el tiempo que cuesta reparar lo que rompes en producción, más el tiempo que pasas intentando entender por qué no lo viste venir.

La intuición es una herramienta útil. Pero tiene límites. Y conocer sus límites es, probablemente, la parte más difícil de cualquier oficio.


/-Foto: macbook pro turned on displaying music por Joshua Woroniecki en Unsplash.

¿Quieres más?

Recibe los nuevos artículos sobre gatos, escritura y código. Sin frecuencia fija, sin paja de IA, sin spam.

Suscribirme

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio