Lo que valen los test case

Seguro que más de uno, tras leer esta entrada, decide buscarme y matarme, pero os lo tengo que contar, y supongo que por el título ya habéis adivinado de qué va la cosa: otra diatriba del RFOG.

Ya sabéis que yo no soy muy dado a las formalidades esas de seguir un método de desarrollo como Scrumm o Agile o como quieran llamarse, y que siempre he dicho que los test units no sirven para nada… al menos en mi trabajo.

Bueno, pues ahora le toca el turno a los “test case”… Según yo los entiendo, se trata de pasar una pieza de código más o menos importante y/o completa por una serie de test automatizados (o no) para determinar si los cambios realizados para resolver un problema han roto otra parte.

El concepto es hacer pequeños tests (los units esos) sobre pequeños bloques de código como funciones y demás. Y luego, cuando ya tienes un módulo completo e independiente (sea lo que quiera que signifique eso en tu caso), lo vuelves a verificar como un todo.

Y si has sido bueno y has hecho los deberes, todo debe funcionar perfectísimamente bien y no presentar ningún problema final, y si lo hace, pues vuelves a revisar el código e iteras.

Luego queda un paso final, que es poner delante del monitor (o lo que sea) a alguien y que compruebe a mano que todo va bien.

***

Esa es mi visión de una secuencia de verificación de código, pero en mi caso muchas veces te encuentras con serias limitaciones, como las que os voy a contar, aunque realmente no puedo hacerlo como quisiera porque tiene que ver con mi trabajo y no puedo hablar con mucho detalle sobre él.

El caso es que tenía una secuencia como la descrita que falló estrepitosamente en producción, o más bien que falló en la etapa final de verificación del producto, que es sacarlo a la calle y dejar que los lusers lo revienten, como así ha sido.

Supongamos por un momento que tenemos un algoritmo del que sólo hay una implementación razonable y posible con los recursos disponibles, y que encima dicho algoritmo es la primera vez que se realiza en donde trabajas, por lo que no hay experiencia anterior ni reutilización de código.

Y encima el algoritmo es indivisible, es decir, no se puede dividir en tareas más pequeñas y verificables. Bueno, sí, pero esas partes son triviales y ni siquiera necesitan un unit test por lo de siempre: un qsort va a funcionar y te va a dejar el tema ordenado sí o sí, y todavía va a funcionar mejor si usas de origen ya algún contenedor STL ordenado…

Y es en este caso donde entran a pleno rendimiento todas esas metodologías chachi piruli Juan Pelotilla que tanto bombo y platillo tienen.

Ahora suponed que con esfuerzo uno hace un análisis de lo que va a ser la entrada al algoritmo y descubre que tiene cuatro variaciones sobre otras cuatro, es decir, 16 familias o grupos de datos. Y a mano, hace al menos dos tests para cada una de ellas. Es decir, calcula a mano, para dos entradas de cada familia, qué dos salidas debe haber.

Y lo mete todo en un test por tabla iterativo. Ante cada entrada se procesa ésta y se compara la salida obtenida con la que se debería obtener, y si todo coincide, se pasa a la siguiente.

Aparte se generan entradas aleatorias y se anotan las salidas, y se le aplican todas las comprobaciones heurísticas posibles, como por ejemplo que el total de la entrada debe ser el total de la salida, que si A es cero, B debe ser cero, etc., pero lo que no se puede hacer es tener un código que verifique los datos porque ese código sería exactamente el mismo y no se puede realizar un algoritmo inverso porque el resultado es entrópico: no se puede obtener el origen en base al resultado.

Si recordáis otra entrada similar a ésta, ese proceso sí que se podía verificar ya que el PC tenía artimética de 32 bits pero el origen no, y lo que se tenía que simular era eso: cierta aritmética de 32 bits en un procesador de 8 bits en el que, encima, el compilador hacía cosas mal.

Bueno, volviendo al presente, una vez todos los casos funcionaban bien, y se generaban millones de secuencias sin que aparentemente nada descuadrara, y se revisaran algunas de forma aleatoria, se integró el algoritmo en el equipo de destino y… falló.

***

Tardó dos semanas pero falló. Vuelta al laboratorio. Primero se encontraron errores chorra de otras partes del código que alimentaban a la rutina con datos incorrectos, pero aun así seguía fallando miserablemente en uno de los 16 casos.

Escribirlo es infinitamente más sencillo que encontrarlo. Me refiero a delimitar el problema a una de las 16 familias, pero tras muchas pruebas, muchos análisis de logs, se encontró.

Luego uno miró los test cases y no vio error alguno… excepto que estaban mal. Es decir, la salida que el programa generaba era incorrecta, y como la salida coincidía (recordemos que pese a ser determinista es entrópica, como un compilador) con lo que decía el test, la cosa fue para adelante.

El primer problema es cómo cojones se pudo equivocar uno en eso. Para mí está claro: dislexia. Invertí dos valores de una tabla, que encima coincidían con la salida incorrecta. Una de esas casualidades que uno encuentra en la vida y que piensa que no deberían ocurrir porque son tan improbables… antes de que Murphy haga su aparición, claro.

Pues bien, una vez corregida la tabla, rápidamente se encontró el error… que era bien estúpido. Para que os hagáis una idea, después de una ordenación mediante quicksort (estamos en C embebido), se recorría la ordenación buscando un elemento con una característica concreta, pero la comparación se hacía… sobre la tabla sin ordenar, o más bien sobre el índice de uno de los elementos ordenados pero tomando la tabla de origen como base del índice… En fin.

Aquí entra una de las cosas que me molan mucho: código auto defensivo. En el caso de las otras familias el código no fallaba porque se recuperaba del error anterior debido a las seguridades insertas en él, pero en el caso que nos ocupa se concatenaba una debilidad de la auto defensa con el error y al final el algorrino terminaba fallando.

***

Ya como conclusión me gustaría hacer algunos comentarios. Los tests estaban mal. Si hubieran estado bien el problema se habría detectado inmediatamente, pero el hecho es que sirvieron de poco o de nada.

En el caso que nos ocupa, el algoritmo era bastante complejo y encima se tenían recursos limitadísimos en la máquina de destino, por lo que no se podían hacer grandes implementaciones ni grandes comprobaciones, y de hecho todavía no se está seguro de que se haya solventado por completo. Las verificaciones automáticas siguen ejecutándose todas las noches, con la ventaja de que ahora los fallos terminan en un bucle infinito desde el cual se puede determinar qué falló y por qué.

Pero la advertencia es muy seria, y es la conclusión a la que quiero llegar. Los tests no sirven para nada, o casi para nada. Porque cuando uno los necesita de verdad, nunca puedes estar completamente seguro de que estén bien. De hecho, los propios tests son código, y podría darse, y os aseguro que se da, la situación en la que un error en el código a comprobar, más un error en el código del test producen una salida correcta.

Es evidente que uno puede tener todo el cuidado del mundo en hacerlos, pero nadie está a salvo de equivocarse. Y al menos yo pienso que, si puedes hacer un test del que estés seguro que no va a fallar es porque no lo necesitas, ya que también estás seguro de que el código funciona bien porque es ese mismo código el que te indica la correctitud o no del test.

Y sobre la verificación de parámetros de entrada, en muchos casos es un objetivo irreal. En el caso que nos ocupa, el algoritmo (y nuestros tests) son completamente incapaces (o casi) de verificar que la entrada es incorrecta salvo en los casos más triviales. Y, de nuevo, los tests no sirven para nada.

Con esto no quiero decir que no se deban hacer tests, que se deben hacer, y cuantos más mejor, si no que cuando más útiles deberían ser, menos utilidad tienen, y encima podría decir que se trata de una relación exponencial inversa…

10 comentarios en “Lo que valen los test case”

  1. Yo me juego la mano a que los test te han ayudado a detectar al menos un fallo en la implementación, en fase de desarrollo. Sólo por esto en mi opinión ya han merecido la pena.
    Además, una vez que has encontrado un fallo no cubierto por las pruebas originales, como es el caso que comentas, lo codificas a posteriori, y ya tienes asegurado que ese fallo, no se va a producir nunca más. Tienes la tranquilidad de saber que al menos esa regresión no se va a producir. Este me parece otro motivo que por sí solo hace que merezcan la pena.
    Por otro lado, las situaciones extremas nunca son buenos ejemplos para sacar conclusiones. Los unit test no son “silver bullets” ni aseguran que el código no tenga fallos. Para la gran mayoría de los casos son muy útiles, pero existirán algoritmos muy complejos en los que haya que plantearse las cosas de otra manera.

  2. Rafa tio, que post. No se si darte la razón o empezar a escribir un poco para negar esto jeje.

    Pero me quedo con el excelente resumen de Cristhian: prefiero 2 tests (bien escritos) que me aseguren 2 casos correctos que nada.
    Aunque te doy la razón en algo: los tests, como el código, son escritos (y ejecutados también) por personas, con lo que existe el factor error. La única forma de evitar esto es automatizar, pero claro si la automatización se realiza sobre bases erróneas, pues más de lo mismo.

    Salu2 y gracias por compartir tus experiencias

    PD: Esto hasta que SkyNet este entre nosotros y nos saque estos problemas de encima, nos traerá otros pero … 😀

  3. @Cristhian, las perderías, las perderías (puede que no sólo las dos que tienes, si no muchas más). 😛

    @Bruno, mola marcar polémica, je je. Y a ver si nos vamos espabilando con lo de Skynet, que yo quiero jubilarme joven… aunque sea dentro de una cueva. 😛

    Y ahora en serio, esta entrada es un “refresco” para aquellos que piensan que las metodologías y los tests son la panacea hasta del hambre en África (no, Sanz, ahora no va contigo).

  4. A eso le llamo yo darse cabezazos contra la pared.

    El problema está en que hay que si fuéramos puristas, habría que validar los tests, contra simuladores, prototipos…

    Pero yo sigo creyendo en los tests, los unit tests con buena cobertura en más de una ocasión me han hecho encontrar errores que de otra forma no habría visto.

    Desde luego, estamos hablando de que detrás de cualquier metodología, hay personas, y errar es de humanos. Como dicen por ahí arriba, tal vez cuando SkyNet esté entre nosotros, no nos tengamos que preocupar de pruebas (tal vez de tener perros cerca)

  5. Me ha recordado cuando las cuentas se cuadraban a mano y, cuando no cuadraban, las repasabas entrando en un bucle pensando “7 y 8, 16 y me llevo una”, “7 y 8, 16 y me llevo una” y no veías donde estaba el error, hasta que algún otro te hacía ver que 7 y 8 eran 15.

    Los test son utiles siempre que puedas comprobar que testean bien lo que tienen que testear. Eso es más dificil cuando no tienes algoritmos independientes del que estas testeando para comprobar la validez del test. Pero el error te ha permitido corregir el test que ya tendrás disponible para cuando pruebes el algoritmo versión 2 🙂

  6. No hay balas de plata …
    Los test no hacen tu código infalible,simplemente te ayudan a reducir el número de errores – tu caso es un ejemplo claro de ello.

  7. Hola Rafael,

    creo que tu caso es un gran ejemplo de que los tests son necesarios y sirven de mucho… siempre que estén bien hechos claro. 😛

    Los tests són nuestra red de seguridad. ¿Tu harías de trapecista con una red con muchos agujeros e hilo de mala calidad? Yo tampoco. Pués lo mismo con los tests.

    Es una parte de nuestro código crítica y con la que tenemos que ir con mucho cuidado y como dice El Bruno, hasta que skynet no venga por aquí, nos toca hacerlo a nosotros.

    Salut!

  8. Resumen de tu post:

    El beisbol es un deporte que no sirve.
    No tengo ni idea de jugar al beisbol, pero al menos para mi no es un deporte que pueda servir para ponerte en forma.
    Una vez jugué al beisbol. No di ni una. Tampoco puse mucho interés en aprender más por que ya sabía yo que para mi es un deporte que no aporta nada.
    No entiendo que la gente juegue al beisbol.
    El beisbol no funciona.

    Por cierto, los metodos esos de entrenamiento y mejora… pues que no soy muy partidario tampoco.

    Post totalmente subjetivo, con cero de información.

    El argumente de que como un bug se ha escapado eso invalida los test cases también es como que de risa.

    Se que soy un poco taliban en esto, pero a estas alturas de la pelicula, la gente que escribe en internet y que es un referente para otros debería medir sus palabras. El testo unitario y los casos son buenos, punto. Igual que un gestor de código… no se puede discutir.

    Que luego me encontraré a algun indocumentado citandote para defender que hacer test es una aberración. Es que el gran Rfog, MVP, dice que no hay que hacer test…

    Saludos.

  9. Rodri, qué tal.

    Glups. ¿Me citan diciendo eso? 🙁

    La verdad es que la intención de la entrada no es esa. La intención de la entrada es decir que nada es tan bonito como se presenta a veces, tal y como todos y cada uno de los comentaristas de la entrada han dicho.

    Y sí, hacer test es útil, pero hacer test pensando que son la solución total a los bugs es una estupidez como un castillo.

    Como ya dije, hacer un unit test para una función que ya sé me va a devolver un número positivo es tontería y pérdida de tiempo.

    Por cierto, como declaración de intenciones, todo mi código está lleno de tests, pero no según nignún estándar porque no es posible para lo que yo hago, incluso muchos ficheros cuentan con un main condicional para verificar su contenido… pero de ahí a decir que los tests son lo más va un trecho…

    [Y sí, ahora les puedes citar este comentario para demostrarles lo contrario]. 😛

Deja un comentario

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