PRUEBAS UNITARIAS DE SOFTWARE

download PRUEBAS UNITARIAS DE SOFTWARE

of 9

Transcript of PRUEBAS UNITARIAS DE SOFTWARE

Clase 17. Prcticas: JUnitEl marco Junit, que usted ha utilizado en este curso para probar su propio cdigo, merece ser objeto de estudio por su importancia. Fue desarrollado por Kent Beck y Erich Gamma. Beck es muy conocido por su trabajo con patrones y con la programacin XP (Extreme Programming); mientras que Gamma es coautor de un conocido libro sobre patrones de diseo. Al ser JUnit cdigo abierto, usted podr estudiar el cdigo fuente por su cuenta. Hay tambin un buen artculo aclaratorio en la distribucin de JUnit, titulado A Cooks Tour, que explica el diseo de JUnit desde la perspectiva de los patrones de diseo y del cual se ha extraido la mayor parte del material para esta clase. JUnit ha tenido un gran xito. Martin Fowler, un autor lcido y con un marcado sentido prctico, defensor de los patrones de diseo y de XP (y tambin autor de un excelente libro sobre modelos de objeto llamado Analysis Patterns), dice sobre JUnit: Jams, en el campo del desarrollo de software, tantas personas han debido tanto a tan pocas lneas de cdigo. Sin duda, la popularidad de JUnit se debe en gran parte a su facilidad de uso. Cabra pensar que, ya que se trata de un marco que no hace gran cosa simplemente ejecuta un grupo de pruebas e informa de sus resultados JUnit debera ser muy sencillo. Pero, en realidad, el cdigo es bastante complicado. La razn principal de su complejidad radica en que ha sido ideado como un marco, para ampliarse de diversas formas no previstas, por lo que est lleno de patrones complejos y generalizaciones diseadas con el fin de permitir a los implementadores anular algunas partes del marco y preservar otras. Otra influencia que aade complejidad al asunto es el deseo de que las pruebas resulten fciles de escribir. Para ello se utiliz una especie de truco tcnico (hack), basado en la tcnica de reflexin, que convierte mtodos de una clase en instancias individuales del tipo Test. Tambin se utiliz otra tcnica que, en principio, parece excesiva. La clase abstracta TestCase hereda de la clase Assert, que contiene unos cuantos mtodos estticos de certificacin, simplemente para que la invocacin del mtodo assert quede escrita slo como un comando assert (), en lugar de Assert.assert (). Est claro que de ninguna manera TestCase es un subtipo de Assert, por lo que esta estructuracin no tiene en realidad mucho sentido, aunque en el fondo permite escribir de manera ms sucinta el cdigo perteneciente a TestCase. Y, como todos los casos de prueba que el usuario escribe son mtodos de la clase TestCase, la tcnica resulta bastante valiosa. El uso de patrones es una actividad que requiere mucha pericia y motivacin. Los patrones clave que vamos a analizar son: Template Method, el patrn clave de la programacin del marco; Command, Composite, y Observer. Todos ellos se explican con detenimiento en Gamma et al, y, con la excepcin de Command, ya se han visto en el presente curso. Mi opinin personal es que el propio JUnit, la joya de la programacin XP, desdice el

mensaje fundamental del movimiento XP: el cdigo por s mismo es suficiente para su comprensin. JUnit es un ejemplo perfecto de programa que sera prcticamente incomprensible sin que algunas representaciones globales del diseo se expliquen de manera que se pueda entender el modo en que encajan. No resulta de gran ayuda el hecho de que el cdigo sea escaso en comentarios y que, cuando stos existen, tiendan a ser bastante oscuros. En este sentido, el artculo Cooks Tour es fundamental: sin l, llevara horas comprender las sutilezas de lo que sucede en el cdigo. Tambin sera de gran utilidad tener ms representaciones de diseo. El artculo presenta una visin simplificada, y yo mismo tuve que construir un modelo de objeto que explica, por ejemplo, como funciona el esquema de listeners. Si es usted uno de esos estudiantes que no cree en las representaciones de diseo y que piensa que el cdigo es lo ms importante, le recomiendo dejar de leer en este instante y sentarse cmodamente en un silln dispuesto a pasar toda la tarde con el cdigo fuente de JUnit. Quin sabe, tal vez cambie de idea Puede descargar el cdigo fuente y documentacin sobre JUnit de: http://www.junit.org/. Hay un almacn de cdigo libre en la direccin: http://sourceforge.net/projects/junit/ donde pueden verse (y aadirse) informes sobre fallos.

17.1 Resumen generalJUnit tiene diversos paquetes: framework como paquete bsico de marcos, runner para algunas clases abstractas y para la ejecucin de pruebas, textui y swingui para interfaces de usuario y extensions para algunas contribuciones prcticas al marco. Vamos a ocuparnos principalmente del paquete de framework. Los diagramas siguientes muestran el modelo de objeto y el diagrama de dependencia modular. Es aconsejable que siga los diagramas a medida que lee el contenido de esta clase. Ambos diagramas incluyen slo los mdulos del marco, aunque he incluido TestRunner en el modelo objeto para demostrar cmo se conectan los listeners; sus relaciones, suite y result, son variables locales de su mtodo doRun.

Observe que el diagrama de dependencia modular est conectado casi por completo. No es sorprendente si tenemos en cuenta que se trata de un marco, ya que los mdulos no estn pensados para trabajar de forma independiente.

17.2 El patrn CommandEl patrn Command encapsula una funcin como un objeto. De esta forma se implementa un cierre recuerdan el curso 6.001? en un lenguaje orientado a objetos. La clase command suele poseer un mtodo nico denominado do, run o perform. Se crea una instancia de la subclase que anula este mtodo, encapsulando tambin, normalmente, algn estado de la clase (en el lenguaje del curso 6.001 llambamos a esto el entorno del cierre). El comando entonces puede pasar por un objeto y ejecutarse invocando el mtodo. En JUnit, los casos de prueba se representan a travs de objetos de comando que implementan la interfaz Test: public interface Test { public void run(); }

Casos de prueba verdaderos son instancias de una subclase de una clase concreta TestCase: public abstract class TestCase implements Test { private String fName; public TestCase(String name) { fName= name; } public void run() { } } En realidad, el cdigo actual no es as, pero empezar a partir de esta versin simplificada nos permitir explicar los patrones bsicos ms fcilmente. Observe que el constructor asocia un nombre con el caso de prueba, lo que resultar til al informar de los resultados. De hecho, todas las clases que implementan Test tienen esta propiedad, lo que quizs hubiera sido buena idea aadir el mtodo public String getName () a la interfaz Test. Observe tambin que los autores de JUnit utilizan la convencin de que los identificadores que comienzan por una f minscula son campos de una clase (esto es, variables de instancia). Veremos un ejemplo ms elaborado del patrn command ms adelante, cuando estudiemos el programa Tagger.

17.3 Mtodo TemplatePuede determinarse que run sea un mtodo abstracto, que exige, por tanto, que todas las subclases lo superpongan. Pero la mayora de los casos de prueba tienen tres fases: determinacin del contexto, ejecucin de la prueba y desmontaje del contexto. Podemos automatizar la utilizacin de esta estructura haciendo que run sea un mtodo template: public void run() { setUp(); runTest(); tearDown(); } Las implementaciones por defecto de los mtodos hook no realizan ningn procesamiento: protected void runTest() { } protected void setUp() { } protected void tearDown() { } Estn declaradas como protected, de forma que sean accesibles a partir de las subclases (y,

por consiguiente, se puedan superponer) pero no desde fuera del paquete. Estara bien poder restringir el acceso excepto a partir de las subclases, pero Java no ofrece dicho modo. Una subclase puede superponer esos mtodos arbitrariamente; si slo superpone runTest, por ejemplo, no habr ningn comportamiento especial de los mtodos setUp o tearDown. Observamos el mismo patrn en la ltima clase, en las implementaciones organizadas en jerarquas de esqueleto de la API de colecciones de Java. A este patrn a veces se le conoce con el nombre, bastante cursi, de Hollywood Principle. Una API tradicional ofrece mtodos que son invocados por el cliente; un marco, por el contrario, hace llamadas a los mtodos de su cliente: no nos llame, le llamaremos nosotros.. El uso cada vez ms extendido de las plantillas es la esencia de la programacin de marcos. Resulta sencillo y llama mucho la atencin escribir programas que sean completamente incomprensibles, pues las implementaciones de mtodos realizan llamadas en todos los niveles de la jerarqua de herencia. Puede ser difcil saber qu esperar de una subclase en un marco. No se ha desarrollado una analoga de las condiciones previas y subsiguientes, y la tecnologa actual an no est muy desarrollada. Por lo general, es preciso leer el cdigo fuente del marco para poder usarlo eficazmente. La API de colecciones de Java es mejor que la mayora de los marcos, ya que incluye en las especificaciones de los mtodos de plantilla descripciones exhaustivas sobre su implementacin. Esto se puede interpretar como una afrenta a la idea de especificacin abstracta, pero es inevitable en el contexto de un marco.

17.4 El patrn CompositeComo vimos en la clase 11, los casos de prueba se agrupan en suites. Pero lo que se hace con una suite de pruebas es bsicamente lo mismo que se hace con una prueba: ejecutarla e informar del resultado. Esto nos sugiere la utilizacin del patrn Composite, en el que un objeto compuesto comparte una interfaz con sus componentes elementales. Aqu, la interfaz es Test, el objeto compuesto es TestSuite y los componentes elementales son miembros de TestCase. TestSuite es una clase concreta que implementa Test, pero cuyo mtodo run, al contrario que el mtodo run de TestCase, invoca el mtodo run de cada una de las pruebas de la suite. Las instancias de los TestCase se aaden a la instancia TestSuite con el mtodo addTest; hay tambin un constructor que crea una TestSuite con un grupo de casos de prueba, como veremos ms adelante. En el ejemplo Composite del libro de Gamma la interfaz incluye todas las operaciones del objeto compuesto. Siguiendo este enfoque, Test debera incluir mtodos como addTest, que se aplican solamente a objetos TestSuite. La seccin de implementacin de la descripcin del patrn explica que se da una compensacin entre transparencia haciendo que los objetos compuesto y hoja se muestren de la misma forma y seguridad impidiendo las invocaciones a operaciones no apropiadas. En los trminos de nuestro debate en la clase sobre subtipos, la cuestin es si la interfaz debera ser un verdadero supertipo. En mi opinin s debera serlo, pues los beneficios de la seguridad son mayores que los de la

transparencia, y, es ms, la inclusin de operaciones compuestas en la interfaz crea confusin. El proyecto JUnit adopta este enfoque, y no incluye addTest en la interfaz Test.

17.5 El patrn del parmetro collectingEl mtodo run de Test posee en realidad esta firma: public void run(TestResult result); Recibe un nico argumento que se altera para registrar el resultado del cdigo ejecutado. Beck llama a esta tcnica parmetro collecting y lo considera como un patrn de diseo propiamente dicho. Una prueba puede fracasar de dos modos. O bien porque produce un resultado errneo (lo que puede incluir no lanzar la excepcin esperada), o bien porque lanza una excepcin inesperada (como IndexOutOfBoundsException). JUnit llama a los primero fallos (failure) y a los segundos errores (error). Una instancia de TestResult contiene una secuencia de fallos y una secuencia de errores, representndose cada fallo o error como una instancia de clase TestFailure, que contiene una referencia a un Test y una referencia al objeto de excepcin generado por el fallo o error. (Los fallos siempre producen excepciones, ya que incluso cuando un resultado inesperado se produce sin una excepcin, el mtodo assert utilizado en la prueba convierte el fallo en una excepcin). El mtodo run en TestSuite permanece inalterado; apenas pasa un objeto TestResult cuando invoca el mtodo run de cada una de sus pruebas. El mtodo run en TestCase tiene el siguiente aspecto: public void run (TestResult result) { setUp (); try { runTest (); } catch (AssertionFailedError e) { result.addFailure (test, e); } (Throwable e) { result.addError (test, e); } tearDown (); } De hecho, el flujo de control del mtodo template run es ms complicado de lo que hemos sugerido. Ms abajo hay unos fragmentos de pseudocdigo que muestran lo que ocurre. Ignora las actividades setUp y tearDown, y considera una utilizacin de TestSuite dentro de una interfaz de usuario de texto: junit.textui.TestRunner.doRun (TestSuite suite) { result = new TestResult (); result.addListener (this); suite.run (result);

print (result); } junit.framework.TestSuite.run (TestResult result) { forall test: suite.tests test.run (result); } junit.framework.TestCase.run (TestResult result) { result.run (this); } junit.framework.TestResult.run (Test test) { try { test.runBare (); } catch (AssertionFailedError e) { addFailure (test, e); } catch (Throwable e) { addError (test, e); } } junit.framework.TestCase.runBare (TestResult result) { setUp(); try { runTest(); } finally { tearDown(); } } TestRunner es una clase de interfaz de usuario que invoca al marco y muestra los resultados. Hay una versin con interfaz grfica de usuario junit.swingui y una versin simple con terminal de texto junit.textui, de la que hemos mostrado un fragmento. Presentaremos el sistema listener ms adelante, por ahora lo pasaremos por alto. He aqu cmo funciona. El objeto TestRunner crea un nuevo TestResult para almacenar los resultados de la prueba, ejecuta la suite de pruebas e imprime los resultados. El mtodo run de TestSuite invoca el mtodo run de cada una de sus pruebas constituyentes, que pueden ellas mismas ser objetos TestSuite, por lo que el mtodo puede ser llamado recurrentemente. Este es un ejemplo ptimo de la simplicidad de Composite. Al final, como hay una invariante que determina que TestSuite no se puede contener a s mismo que, en verdad, no est especificado ni definido por el cdigo de TestSuite el mtodo acabar por invocar los mtodos run de objetos de tipo TestCase.

Ahora, en el mtodo run de TestCase, el objeto receptor TestCase le cambia el sitio al objeto TestResult e invoca el mtodo run de TestResult con TestCase como argumento. (Por qu?). A continuacin, el mtodo run de TestResult invoca el mtodo runBare de TestCase, que es el mtodo template real que ejecuta la prueba. En caso de error en la prueba, se arroja una excepcin, que es interceptada por el mtodo run de TestResult, el

cual a continuacin empaqueta la prueba y la excepcin como un fallo o error de TestResult.

17.6 El patrn ObserverQueremos mostrar, para una interfaz de usuario alternativa, los resultados de la prueba de modo incremental, mientras sta tiene lugar. Para lograrlo, JUnit utiliza el patrn Observer. La clase TestRunner implementa una interfaz TestListener que tiene mtodos addFailure y addError propios. La interfaz hace el papel de Observer (observador). La clase TestResult hace el papel de Subject (sujeto observado). TestResult ofrece un mtodo public void addListener(TestListener listener) que aade un observador. Cuando se invoca el mtodo addFailure de TestResult, adems de actualizar su lista de fallos, llama al mtodo addFailure en cada uno de sus observadores: public synchronized void addFailure(Test test, AssertionFailedError e) { fFailures.addElement(new TestFailure(test, e)); for (Enumeration e= cloneListeners().elements(); e.hasMoreElements(); ) { ((TestListener)e.nextElement()).addFailure(test, e); } } En la interfaz de usuario textual, el mtodo addFailure de TestRunner muestra simplemente un carcter F en la pantalla. En la interfaz grfica de usuario, este mtodo aade el fallo a una lista de exhibicin y cambia el color de la barra de progreso a rojo.

17.7 La tcnica de reflexinHay que recordar que un caso de prueba es una instancia de la clase TestCase. Para crear una suite de pruebas en Java puro y simple, el usuario tendra que crear una nueva subclase de TestCase para cada caso de prueba e instanciarla. Una forma elegante de hacerlo es por medio de clases internas annimas, creando el caso de prueba como una instancia de una subclase que no tiene nombre. Este mtodo no deja de ser muy trabajoso, por lo que JUnit utiliza un hack o truco tcnico denominado reflexin. El usuario ofrece una clase para cada suite de pruebas denominada, digamos, MySuite que es una subclase de TestCase, y que contiene muchos mtodos de prueba, cada uno de los cuales tiene un nombre iniciado con la string test. Estas clases se tratan como casos de prueba individuales. public class MySuite extends TestCase { void testFoo () { int x = MyClass.add (1, 2); assertEquals (x, 3); } void testBar () {

} } La propia clase objeto MySuite se pasa al constructor de TestSuite. Mediante la tcnica de reflexin, el cdigo de TestSuite instancia MySuite para cada uno de los mtodos que comienzan con test, pasando los nombres de los mtodos como un argumento para el constructor. Como resultado, para cada mtodo de prueba se crea un nuevo objeto TestCase, con su nombre vinculado al nombre del mtodo de prueba. El mtodo runTest de TestCase invoca, de nuevo a travs de la reflexin, el mtodo cuyo nombre corresponde al nombre del propio objeto TestCase, ms o menos as: void runTest () { Method m = getMethod (fName); m.invoke (); } Este esquema es oscuro y presenta sus riesgos; no es el tipo de cosa que usted debe imitar en su cdigo. En este caso se justifica porque se limita a una pequea parte del cdigo JUnit y aporta una gran ventaja al usuario de ste.

17.8 Cuestiones para el estudioEstas cuestiones surgieron cuando constru el modelo de objeto para JUnit. No todas tienen una nica respuesta. Por qu los listeners forman parte de TestResult? No es TestResult una especie de listener por s mismo? Es posible que un TestSuite no contenga ninguna prueba? Se puede contener a s mismo? Son nicos los nombres de los Test? El campo fFailedTest de TestFailure apunta siempre a un TestCase?