Al igual que los objetos físicos, las pruebas se pueden estropear. No se estropean, sino que algo ha cambiado en su
entorno. Quizás se han trasladado a un sistema operativo nuevo. O, más probablemente, el código que ejercen ha cambiado
de modo que provoca correctamente que la prueba falle. Imagine que está trabajando en la versión 2.0 de una
aplicación de banca electrónica. En la versión 1.0, este método se utilizaba para iniciar la sesión:
registro booleano público (cadena de caracteres nombre de usuario);
En la versión 2.0, el departamento de marketing ha considerado que la protección por contraseña puede ser una buena
idea. El método ha pasado a ser este:
registro booleano público (Cadena de caracteres nombre de usuario, Cadena de caracteres
contraseña);
Todas las pruebas que utilicen el Inicio de sesión fallarán Ni siquiera compilará. Como no se puede hacer demasiado
trabajo útil en este punto, no se pueden escribir demasiadas pruebas útiles sin iniciar la sesión. Puede encontrar
cientos o miles de pruebas con fallos.
Estas pruebas se pueden solucionar utilizando una herramienta de buscar y reemplazar global que encuentre todas las
instancias de registro (algo) y lo reemplace con registro (algo, "contraseña ficticia"). A continuación,
establezca que todas las cuentas de prueba utilicen esta contraseña, y estará solucionando el problema.
Más adelante, cuando marketing decida que las contraseñas no pueden contener espacios, deberá repetirlo todo de nuevo.
Esto es una carga inútil, especialmente cuando los cambios en las pruebas no son tan sencillos -lo que ocurre a menudo.
Existe un modo mejor.
Imagine que las pruebas originalmente no llamaron el registro del método del producto. En su
lugar, llamaron un método de biblioteca que hace lo que puede para registrar la prueba y está listo para continuar.
Inicialmente, este método podría ser parecida al siguiente:
public boolean testLogin (String username) {
return product.login(username);
}
Cuando se produce el cambio a la versión 2.0, la biblioteca del programa de utilidad se cambia para que coincida:
public Boolean testLogin (String username) {
return product.login(username
, "dummy password");
}
En lugar de cambiar mil pruebas, cambien un método.
Idealmente, todos los métodos de biblioteca necesario estarán disponibles al principio del esfuerzo de prueba. A la
práctica, no se pueden anticipar todos. Es posible que no se de cuenta de que necesita un método de programa de
utilidad testLogin hasta la primera vez que cambie el registro del
producto. Por eso los métodos de programa de utilidad de prueba suelen extraerse de pruebas existentes cuando sean
necesarias. Es muy importante que efectúe esta reparación de prueba constante, incluso bajo presión de la
planificación. Si no lo hace, gastará mucho tiempo tratando un conjunto de aplicaciones de prueba feo e insostenible.
Es posible que esté desaprovechando, o que no pueda escribir los números necesarios para las nuevas pruebas porque todo
el tiempo de prueba disponible se dedica a mantener los antiguos.
Nota: las pruebas del método de registro del producto todavía lo llamarán directamente.
Si cambia este comportamiento, una parte o todas las pruebas deberán actualizarse. (Si ninguna prueba de registro falla cuando cambie el comportamiento, probablemente no son demasiado buenos detectando
defectos).
El ejemplo anterior mostraba cómo las pruebas pueden abstraerse de la aplicación concreta. Muy probablemente puede
realizar más abstracción. Encontrará que una serie de pruebas empiezan con una secuencia común de llamadas a método: se
registran, establecen algún estado y navegan hasta la parte de la aplicación que está probando. Sólo entonces cada
prueba hace algo diferente. Toda esta configuración podría -y debería- contenerse en un único método con un nombre
evocador como readyAccountForWireTransfer. De este modo, ahorrará un tiempo considerable cuando
se escriban nuevas pruebas de un tipo concreto y también realiza la intención de que cada prueba sea mucho más
comprensible.
Es importante tener unas pruebas comprensibles. Un problema común con los conjuntos de aplicaciones de prueba antiguos
es que nadie sabe qué hacen las pruebas o por qué. Cuando se estropean, la tendencia es arreglarlos del modo más
sencillo posible. Esto suele resultar en pruebas más débiles a la hora de encontrar defectos. Ya no prueban lo que
pretendían probar originalmente.
Imagine que está probando un compilador. Algunas de las primeras clases escritas definen el árbol de análisis interno
del compilador y las transformaciones que se han realizado sobre este. Tiene una serie de pruebas que construyen
árboles de análisis de construcción y prueban las transformaciones. Una prueba podría ser parecida a la siguiente:
/*
* Given
* while (i<0) { f(a+i); i++;}
* "a+i" cannot be hoisted from the loop because
* it contains a variable changed in the loop.
*/
loopTest = new LessOp(new Token("i"), new Token("0"));
aPlusI = new PlusOp(new Token("a"), new Token("i"));
statement1 = new Statement(new Funcall(new Token("f"), aPlusI));
statement2 = new Statement(new PostIncr(new Token("i"));
loop = new While(loopTest, new Block(statement1, statement2));
expect(false, loop.canHoist(aPlusI))
Esta prueba es difícil de leer. Imagine que pasa el tiempo. Algo cambia que requiere que se actualicen las pruebas. En
este punto, tiene más infraestructura de producto en la que dibujar. Concretamente, puede tener una rutina de análisis
que convierte las cadenas de caracteres en árboles de análisis. Sería mejor reescribir completamente las pruebas para
utilizarlas en este punto:
loop=Parser.parse("while (i<0) { f(a+i); i++; }");
// Get a pointer to the "a+i" part of the loop.
aPlusI = loop.body.statements[0].args[0];
expect(false, loop.canHoist(aPlusI));
Estas pruebas serán mucho más fáciles de comprender, y ahorrarán tiempo inmediatamente y en el futuro. En realidad, su
coste de mantenimiento es muy inferior y sería lógico convertir la mayoría de ellas hasta que el analizador esté
disponible.
Existe un pequeño inconveniente en este enfoque: estas pruebas pueden descubrir un defecto en el código de
transformación (tal como estaba previsto) o en el analizador (por accidente). El problema del aislamiento y la
depuración puede resultar algo más difícil. Por otro lado, encontrar un problema que las pruebas de analizador no
detectan no es tan negativo.
También existe la posibilidad de que un defecto del analizador pueda enmascarar un defecto en el código de
transformación. La posibilidad de que suceda es más bien pequeña, y su coste es ciertamente inferior al coste de
mantener las pruebas más complicadas.
Un conjunto de aplicaciones de prueba grande contendrá algunos bloques de prueba que no cambian. Corresponden a áreas
estables de la aplicación. Otros bloques de prueba cambiarán a menudo. Corresponden a áreas de la aplicación en que el
comportamiento cambia a menudo. Estos últimos bloques de prueba tendirán a utilizar más las bibliotecas de programas de
utilidad. Cada prueba probará comportamientos específicos en el área cambiable. Las bibliotecas de programa de utilidad
están diseñadas para permitir que esta prueba compruebe los comportamientos de destino manteniéndose relativamente
inmune a los cambios de los comportamientos no probados.
Por ejemplo, la prueba "loop hoisting" que se ha mostrado anteriormente ahora es inmune a los detalles de cómo se
construyen los árboles de análisis. Sigue siendo sensible a la estructura de un árbol de análisis del bucle mientras (debido a la secuencia de accesos necesarios para buscar el subárbol para a+i). Si esta
estructura se puede cambiar, la prueba puede ser más abstracta creando un método de programa de utilidad fetchSubtree :
loop=Parser.parse("while (i<0) { f(a+i); i++; }");
aPlusI = fetchSubtree(loop, "a+i");
expect(false, loop.canHoist(aPlusI));
La prueba ahora es sensible sólo a dos cosas: la definición del lenguaje (por ejemplo, que los enteros se puedan
incrementar con ++), y las reglas que gobiernan el loop hoisting (el comportamiento cuya
corrección se está comprobando).
Incluso con las bibliotecas, una prueba se puede romper periódicamente con los cambios de comportamiento que no están
relacionados con lo que comprueba. Al arreglar la prueba es poco probable que se encuentre un defecto debido al cambio;
es algo que se hace para mantener las posibilidades de que la prueba encuentre otro defecto en algún momento. Pero el
coste de esta serie de correcciones puede superar el valor de que la prueba encuentre, hipotéticamente, un defecto.
Puede ser mejor desechar, simplemente, la prueba y dedicar el esfuerzo a crear nuevas pruebas con un mayor valor.
La mayoría de personas se resisten a la noción de deshacerse de una prueba - al menos hasta que están tan cargados con
el peso del mantenimiento que desechan todas las prueba. Es mejor tomar la decisión con cuidado y con
continuidad, prueba a prueba, preguntando:
-
¿Cuánto trabajo será necesario para arreglar esta prueba, quizás añadiendo a la biblioteca del programa de
utilidad?
-
¿De qué otro modo se podría utilizar el tiempo?
-
¿Qué probabilidad existe de que la prueba encuentra defectos graves en el futuro? ¿Cuál ha sido el registro de
seguimiento de ésta y de las pruebas relacionadas?
-
¿Cuánto tardará la prueba en volverse a romper?
Las respuestas a estas preguntas serán estimaciones imprecisas o suposiciones. Pero preguntarlas producirá unos mejores
resultados que tener simplemente una política de corrección de todas las pruebas.
Otro motivo para desechar las pruebas es que ahora sean redundantes. Por ejemplo, en un estadio temprano del
desarrollo, puede haber una multitud de pruebas simples de métodos de construcción de árboles de análisis (el
constructor LessOp y similares). Posteriormente, durante la escritura del analizador existirán
una serie de pruebas de analizador. Como el analizador utiliza métodos de construcción, las pruebas de analizador
también las probarán indirectamente. Cuando los cambios del código rompan las pruebas de construcción es razonable
descartar algunas de las que son redundantes. Por supuesto, cualquier comportamiento de construcción nuevo o cambiado
necesitará nuevas pruebas. Se pueden implementar directamente (si es difícil probarlas exhaustivamente con el
analizador) o indirectamente (si las pruebas con el analizador son adecuadas y más sostenibles).
|