Lo que valen los test case
Publicado
29/3/2011 22:36
por
Rafael Ontivero
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…