Ahora que ya sabemos cómo hacer los test de JUnit con eclipse o maven, viene una parte importante, que es empezar a hacer los test automáticos de nuestras clases.
Muchas veces hay clases que son dificilmente testeables, pero también es cierto que muchas de esas muchas veces, las clases son dificiles de testear por el diseño que hemos hecho de ellas. La inmensa mayoría de las clases se pueden hacer de una forma que sea más fácilmente testeables.
En este artículo vamos a ver un ejemplo concreto de una clase que es dificilmente testeable, pero vamos a ver que podríamos haberla hecho de forma que sea más fácilmente testeable y que, curiosamente, esta forma más fácilmente testeable está más de acuerdo con un buen diseño orientado a objetos.
Vamos a partir para este ejemplo de la ClaseMazacote.java. Esta clase es un simple main() en el que se leen dos números de teclado, se suman y se saca el resultado de la suma por pantalla. Como al leer de teclado el usuario puede escribir algo que no sea un número, se ha hecho un método estático y privado por separado, llamado getSumando(), cuya función es estar pidiendo uno de los sumandos hasta que el usuario introduzca algo correcto.
El programa, si lo probamos a mano, funciona. Pero si intentamos hacer un test de JUnit con él, lo tenemos bastante difícil. Al leer directamente de teclado, no podemos automáticamente pasarle los datos de entrada. Al escribir directamente en pantalla, no podemos automáticamente ver sus resultados.
Si tenemos inventiva, podemos arreglarnos para hacer el test. Por ejemplo, podemos arrancar el programa como si fuera una aplicación externa con Runtime.getRuntime(), con lo que sí tendremos acceso tanto a entradas como salidas a través de la clase Process que obtenemos al arrancar el programa. Pero parece un proceso demasiado complejo para un test automático.
O también podemos usar System.setOut() y System.setIn() para pasar un PrintStream y un InputStream propios, de forma que podemos controlar lo que leerá nuestro programa o escribirá nuestro programa. Pero nuevamente parece algo rebuscado y complejo.
¿Cómo hacemos esto entonces?. Pues haciendo la clase de otra manera. Vemos que nuestra clase, para leer, usa una clase java Scanner y de ella, en concreto, el método nextLine() para obtener la siguiente línea de teclado. Pues bien, en vez de dejar que el programa haga eso directamente, vamos a hacer una interface que tenga el método nextLine(), como la que vemos en IfzScanner.java. A nuestra ClaseMazacote.java, le ponemos un método setIfzScanner(IfzScanner) que reciba esta interface. Dentro de la ClaseMazacote, cambiamos para que en vez de hacer directamente un new Scanner(System.in), use esta IfzScanner que le han pasado.
De la misma forma, vemos que la ClaseMazacote usa directamente System.out.println() para sacar los textos y resultados por pantalla. Hacemos entonces una IfzMuestraResultados.java con un método println(), y a la ClaseMazacote le ponemos un nuevo método setIfzMuestraResultados(IfzMuestraResultados), de forma que en vez de usar directamente System.out, usará esta IfzMuestraResultados.java
Pero claro, ahora nuestro main() debe instanciar una implementación para cada una de estas interfaces, así que hacemos dos implementaciones muy inmediatas, en el que el método de cada interface sólo redirige la llamaa hacia un Scanner de verdad o el System.in, tal que así:
// Implementacion de IfzScanner, que lee de teclado.
IfzScanner unScanner = new IfzScanner() {
private final Scanner scanner = new Scanner(System.in);
public String nextLine() {
return scanner.nextLine();
}};
// Implementacion de IfzMuestraResultados, que escribe en pantalla
IfzMuestraResultados muestraResultados = new IfzMuestraResultados() {
public void println(String textoPeticion) {
System.out.println(textoPeticion);
}
};
Vemos que las implementaciones son muy sencillas, símplemente una línea de código (si descontamos el new de Scanner). Ahora el main() sólo tiene que llamar a los métodos setIfzScanner() y setIfzMuestraResultados().
Como ahora la clase no puede empezar a trabajar directamente hasta que le pasemos las interfaces, hemos añadido además un método empiezaATrabajar(), al que llamaremos después de pasar ambas interfaces y en el que la clase empezará a leer números y sumarlos. El código quedará así
setIfzScanner(unScanner);
setIfzMuestraResultados(muestraResultados);
empiezaATrabajar();
También y aunque no es estrictamente necesario para el test, hemos separado el método main() y el resto del código en dos clase. La ClaseSemiMazacote.java con sólo el main que hace los news, llama a los set y al empiezaATrabajar(). Por otro lado, la clase Sumador.java, que es la que recibe las interfaces y hace la suma.
Ahora que las entradas y salidas están separadas en interfaces, podemos hacer el test automático mucho más fácil. Sólo tenemos que hacer una implementación específica para el test de forma que en el método nextLine() devuelva lo que nos venga bien para el test y comprobar en el método println() que llega lo esperado. Esto, aunque se parece a reemplazar el System.out o el System.in por algo a medida, es más sencillo, puesto que sólo debemos implementar un método en cada Interface, el único que usa la clase, mientras que en PrintWriter o InputStream, tenemos un montón de métodos complejos que debemos implementar/sobreescribir.
Cuando implementamos algunas interfaces con una implementación específica para el test automático, se conoce a esta implementación como mock objects (simulacro de objeto, en cristiano). Así, en nuestro ejemplo, tendremos MockScanner.java y MockMuestraResultados.java. Ambas implementaciones tiene un array de String, con los textos que van a devolver o esperan recibir (según la clase) y un contador para saber por qué texto del array van.
El test automático es ahora tan sencillo como la clase main(). En vez de instanciar las implementaciones reales de las interfaces, instancia los objetos Mock, llama a los métodos setIfzScanner() y setIfzMuestraResultados(), pone la clase a trabajar y luego comprueba, símplemente, que se han leido todas las cadenas que se deberían leer y que se han recibido todas las que se esperaba recibir. Aquí abajo tienes le método concreto de test, pero en TestSumador.java tienes el test completo.
sumador = new Sumador();
scanner = new MockScanner();
muestraResultados = new MockMuestraResultados();
sumador.setIfzScanner(scanner);
sumador.setIfzMuestraResultados(muestraResultados);
sumador.empiezaATrabajar();
// Se comprueba que se han leido las dos lineas del scanner
assertEquals(2, scanner.getContador());
// Se comprueba que han salido tres lineas por pantalla: para pedir los
// dos sumadores y el resultado.
assertEquals(3, muestraResultados.getContador());
Vemos entonces la importancia de separar la forma de obtener las entradas y salidas de la clase a testear. De esta forma, en nuestro test automático, podremos facilitar fácilmente las entradas a la clase y leer sus salidas. Es muy importante dejar en las implementaciones reales el menor código posible, ya que este código no se testeará. Hemos visto en la implementación de IfzMuestraResultados que el método println() sólo llama al método del mismo nombre de System.out (una línea de código nada compleja) y algo parecido con la implementación de IfzScanner.
Y este diseño, desde el punto de vista de la orientación a objetos, es mucho mejor. Hemos pasado de tener una única clase que lo hace todo, a tener tres clases: la que lee de teclado, la que hace las cosas y la que muestra los resultados. Ya tenemos, de acuerdo a un buen diseño orientado a objetos, que cada clase es responsable de una cosa.
Se puede seguir mejorando el diseño, partiendo más la clase Sumador, se acuerdo a las distintas responsabilidades que hay dentro. Se puede hacer una clase encargada de convertir el String leido en un double y dar la brasa hasta que el usuario escriba algo correcto. Se puede hacer otra clase que sólo hace la suma. Si lo hacemos así, podremos testear por un lado que la conversión de String a double se hace correctamente y que la suma se hace correctamente. Obviamente, en un ejemplo tan simple como este, posiblemente no merece la pena, pero si hablamos de programas más grandes con cuentas más complejas, sí merecerá la pena.
Cuando se lleva tiempo haciendo test de JUnit en los proyectos en los que se trabaja, hay dos cosas que son particularmente dificiles de testear de forma automática: Las interfaces gráficas de usuario y las bases de datos.
La interface gráfica de usuario es claramente difícil de testear automáticamente. Es difícil pulsar los botones sin ayuda de un usuario con un ratón y es difícil ver qué demonios hay escrito en un JTextField escondido en un JPanel dentro de un JInternalFrame que está dentro de un JDesktopPane....
Hay formas de probarlas, pero nuevamente es complejo. Tenemos la clase java.awt.Robot, que permite simular clicks de ratón y pulsanciones deteclado, pero tenemos que saber con exactitud las coordenadas en la que se pintará nuestro botón o nuestra caja de texto. Con el método getComponents() que tienen todos los componentes SWING, también podemos ir bucenado recursivametne hasta encontrar el JButton o JTextField que nos interese y luego, llamar a sus métodos doClick() o getText(). Pero también es código complejo.
Hay librerías, como Fest-Swing, que nos facilitan la labor de bucear entre los componentes Swing, pero nuevamente sigue siendo algo complejo.
Por otro lado, las bases de datos también son difíciles de testear. En primer lugar, nuestros test necesitarían tener acceso a la base de datos y deberían borrar todos los datos existentes, insertar algo conocido, ejecutar las clases que trabajan con base de datos y luego leer en base de datos a ver si los registros en ella son los esperados. No siempre tenemos disponible la base de datos en los test y mucho menos para borrar todo. El código de test también se complica si se tiene que dedicar a hacer inserts, updates y/o deletes, casi tanto como el código real.
¿Cual es la solución entonces?
La solución es, al igual que el caso anterior, hacer que el grueso de nuestro código NO tenga nada de Swing ni relación directa con base de datos. Debe hablar con la interface Swing o con la base de datos a través de los métodos de una Interface java y debe hacerse de tal forma que el código en la interface Swing real o en la implementación de la interface con la base de datos sea lo menor y más simple posible, ya que quedará sin testear.
Por ejemplo, si la interface java para hablar con la interface gráfica Swing se llama IfzVista, los métodos de IfzVista deben ser casi directamente el poner algo en un componente Swing. Por ejemplo, para poner algo en un JTextField, IfzVista debe tener un método lo más parecido posible al setText() del JTextField, de forma que la implementación de este método sea directa
public class VentanaSwing implements IfzVista {
...
// Correcto
public void setUnNumero(String texto) {
unJTextField.setText(texto);
}
// Incorrecto, lleva algo de logica: la conversion de double a String.
public void setUnNumero (double valor) {
unJTextField.setText(Doubel.toString(valor));
}
De igual manera, los botones de nuestra interface gráfica de usuario deberían llamar, al pulsarse, directamente a un método de nuestra clase de la aplicación
botonSalvar.addActionListener ( new ActionListener() {
public void actionPerformed(ActionEvent e) {
claseDeAplicacion.pulsadoSalvar();
}
});
De esta forma, en la interface gráfica de usuario habrá código que se queda sin testear, pero que será código muy tonto. Existe un patrón de diseño, llamado Vista-Presenter, que precisamente indica que se deben hacer de esta forma las cosas para hacer un diseño testeable.
Las bases de datos son otro problema aparte. Por un lado, si nuestra aplicación usa una base de datos, prácticamente todo el mundo está de acuerdo en que hay que hacer test automáticos en donde se use la base de datos. Estos test, de alguna forma, deben poner datos conocidos en la base de datos, ejecutar las clases de nuestra aplicación y verificar en base de datos que se han insertado, modificado o borrado los datos que corresponda. Esto, sin embargo, tiene dos problemas importanes:
Por supuesto, existen herramientas y trucos para intentar solucionar estos problemas. Veamos, por encima, algunos de ellos.
Hemos visto cómo hacer las clases para que se puedan testear fácilmente y para ello es básico separar la forma de leer entradas y salidas de la clase que queremos testear. También hemos visto que hacer las clases para que se puedan testear en general nos lleva a un mejor diseño. Todo esto nos lleva al siguiente punto: La metodología TDD o Test Driven Development.