Mar 25

Subir ficheros al servidor con JSP

 Dentro de la aplicación web que estamos haciendo, tenemos que permitir al usuario subir ficheros al servidor. Sé cómo se suben ficheros al servidor en PHP, pero nunca lo había hecho con JSP. Pensé que sería similar, pero me he encontrado con algunas sorpresas.

La primera sorpresa ha sido buscando en google. Unas búsquedas rápidas no me ha dado ningún resultado en el que se suba el fichero sin necesidad de librerías adicionales. Pensé que sería igual que con Apache/PHP, el servidor sube el fichero a una carpeta temporal y una variable le indica a nuestra aplicación PHP dónde está. En Tomcat/JSP parece ser que no es así. Insisto, quizás sí, pero no he buscado en profundidad.

Todos los resultados de google que he visto hacen referencia a que hay que usar alguna librería externa, como apache-commons-fileupload. Así que me puse a ello, usando esa librería.

La segunda pega es que al ser el form de html enctype="multipart/form-data", necesario para poder subir un fichero, en la parte del servidor JSP dejan de funcionar los request.getParameter(), siempre devuelve null. Resulta que si el request es multipart/form-data, hay que tratarlo de otra manera. Este asunto me ha llamado la atención y le ha hecho perder un puntito a JSP frente a PHP (o a Tomcat frente a Apache, no sé quién es el culpable), donde parece que  no hay esos problemas.

¿Y cómo se leen entonces los parámetros de la petición?. Pues nuevamente una búsqueda rápida en google parece indicar que la única solución es usar librerías externas y en concreto, la misma apache-commons-fileupload. Con esa librería se "parsea" la petición y obtenemos una lista de FileItem. Cada uno de ellos puede ser un fichero al que se ha hecho upload…. ¡¡ o uno de los parámetros !!. Llamando a los método getFieldName() y getString() (comprobando previamente si es parámetro o fichero) de esos FileItem obtenemos los valores.

En fin, algo que me ha parecido rebuscado y demasiado complejo frente a cómo se hace en PHP/Apache. Un pequeño tutorial de esto en la chuwiki: File Upload con JSP.

Mar 10

Malditas variables estáticas

En una aplicación más o menos grande y seria de java es normal leer algún fichero con propiedades de configuración para nuestro programa. Esas propiedades suelen ser múltiples y variadas, para módulos de todos los niveles que no tienen nada que ver unos con otros.

Una opción es leer ese fichero de propiedades desde un sitio cercano al main() de nuestra aplicación e ir pasando estas propiedades a los distintos módulos. Si hay módulos en varios niveles, estos deberán ir pasándose las propiedades de unos a otros, ya que el main() posiblemente no pueda acceder directamente a los módulos de bajo nivel.

Para evitar este trasiego de propiedades por todo el código, tiendo a hacer un código java de este estilo

public class FicheroPropiedades {

   private static String pathFicheroPropiedades = "path/fichero.properties";

   private static Properties propiedades = null;

   static {
      propiedades = cargaFichero();
   }

   public static String getProperty (String key) {
      return properties.getProperty(key);
   }
}

Es decir, una clase con todo estático a la que pedir los valores de las propiedades y un inicializador estático de forma que el fichero de propiedades se cargará en cuanto alguien "mencione" esta clase.

Pues bien, esto son todo problemas para los test de JUnit.

Por un lado, posiblemente tengamos o queramos un fichero de propiedades específico para nuestros test. O queramos cambiar alguna propiedad en algún test. Al leer nuestro código bajo test las propiedades de esta clase, es necesario hacerle cosas a esta clase para poder modificarla a gusto. Por ejemplo, poner public el path del fichero para poder cargar un fichero distinto en los test, o poner un cargaFichero(path), o añadir un setProperty() para modificar alguna propiedad concreta, o cualquier otra modificación que se os ocurra. Pero eso no es bonito. No es bonito tener que modificar una clase de nuestro código real para poder manejarla desde los test.

Por otro lado, si tenemos alguna herramienta que ejecuta automáticamente todos los test, es posible que esta clase sólo se inicialice en la ejecución del primer test, manteniendo sus valores para el resto de test. O que al pasar algún test que modifica propiedades, las deje modificadas para el siguiente. En fin, que podemos tener problemas en función de qué test se ejecuten primero, o si añadimos un test nuevo que modifique estas propiedades, puede alterar el resultado de los que van detrás.

Este es otro motivo, además de la mantenibilidad del código, para no usar/acceder a atributos estáticos desde todo el código  si no es realmente necesario. Los test de JUnit pueden hacerse dependientes del orden en que se ejecutan, ya que unos pueden cambiar o no los valores de estas variables estáticas según sus necesidades, incordiando a los que vienen detrás.

Aunque sea un poco rollo a la hora de codificar, suele ser mejor pasar las cosas a cada módulo a traves de métodos set() o en el constructor, aunque sea todo de golpe con un setProperties(Properties), que andar inicializando automáticamente variables estáticas y acceder a ellas desde todos sitios.

Nov 24

Winpcap, jpcap y java

 En uno de los proyectos (java) tenía necesidad de tocar uno de los campos que hay en las cabeceras IP de los mensajes, en concreto, el campo de tipo de servicio, que de alguna forma establece prioridades para el mensaje.

Mirando la API de java, veo que la clase Socket tiene un método setTrafficClass() que de alguna manera sirve para cambiar este campo, pero fíjate tú que cosas, hago mis pruebas, arranco un sniffer (Wireshark gratuito) y de todos los bits de ese campo sólo consigo cambiar el segundo. Todos los demás bits siempre se captura con ceros. No es de extrañar, la misma API de java indica que ese método puede o no funcionar dependiendo del "underlying network implementation", así que empiezo a buscar librerías que me faciliten esto.

Al final, el conjunto de librerías a usar es Winpcap + jpcap. La primera es un instalable windows que nos proporciona unas librerías de base (dll) para programar sockets a bajo nivel. La segunda es una librería java que nos facilita las llamadas a la librería Winpcap. Con ambas juntas, podemos programar sockets a bajo nivel desde java con comodidad. Eso sí, hay JNI entre medias aunque no lo veamos al programar.

La aplicación típica que muestra en los ejemplos de esta librería jpcap es la captura de paquetes de red, para hacerse uno su propio sniffer. También permite el envío de paquetes IP, dando valores a todos los campos de la cabecera IP a nuestro gusto. Tras pelearme con ella un par de días, todo parece funcionar bien. Sin embargo, en esos días encontré pegas que ¿cómo no?, pongo aquí. No son pegas de la librería, sino del entorno. Allá van:

  1. Posiblemente se necesitan permisos de administrador para ejecutar nuestro programa si anda manejando las tarjetas de red con la librería jpcap.
  2. En Windows XP hay una variable de registro que debe tener un valor concreto para que nuestro programa tenga permisos para modificar el campo traffic class: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters : "DisableUserTOSSetting"=dword:00000000
  3. Cuando se envía un paquete IP, en el ejemplo pone unas direcciones mac "aleatorias". Para que funcione bien es importante poner direcciones mac correctas. La de origen puede tener o no importancia, dependiendo de si por debajo se nos permite o no enviar cabeceras IP con una mac propia falsa. La de destino debe ser correcta o el mensaje nunca llegará. Se puede poner 255:255:255:255:255:255 si la mac de destino es desconocida.
  4. La mayoría de los routers/switch de la red están configurados por defecto para poner a cero los campos del traffic class de la cabecera IP, por lo que aunque cambiemos esos campos, es muy posible que a destino lleguen como ceros. No es que no funcione la librería, sino que hay que configurar bien los routers/switches.
  5. Así que el sniffer que usemos para ver si todo va como debe, debemos arrancarlo tanto en el PC que envía como en el que recibe. En el que envía podemos ver si hemos rellenado todos los campos como queremos. En el que recibe veremos si ningún router malvado lo ha modificado por el camino.

En fin, que he estado entretenido un par de días.

 

Oct 20

Jugando con Proguard

Candado de código ofuscado Estos días estoy jugando con Proguard, una herramienta que coge nuestro jar en java y realiza básicamente tres tareas: ofuscarlo, optimizarlo y eliminar sobrantes. Por supuesto, estas tareas son independientes y podemos realizar unas sí y otras no a nuestro gusto. Tiene plugin para maven, por lo que si usamos maven, podemos realizar cualquiera de estas tres tareas automáticamente al generar nuestro jar.

Ofuscado

La parte de ofuscado es sencilla, símplemente coge los paquetes, las clases y los métodos y les cambia el nombre por a, b, c, etc. Así, en vez de persona.setId(7), si descompilamos veríamos a.b(7). Es decir, se puede seguir descompilando pero el código es mucho menos claro.

Tiene opciones para decirle qué clases o métodos no debe ofuscar. Son candidatos claros a no ofuscar el método main() de la clase principal e incluso el nombre de esa clase, para que luego la máquina virtual java sepa qué debe ejecutar. Los métodos write() y read(9 de serialización, etc. También, en el caso de que estemos intentando ofuscar una librería, se deben no ofuscar las clases que se usen desde el programa principal.

Curiosamente, proguard es bastante inteligente y tiene en cuenta la relexión de java. Si ve que usamos cosas como Class.forname(), o Class.getDeclaredMethod()…., nos avisa con un error si intentamos ofuscar las clases a las que hace referencia esa reflexión.

Optimizado

Con esto no me he metido a fondo, porque no hay muchas posibilidades de ver qué es lo que hace. Entiendo que limpia nuestro código ineficiente, borrando variables locales no usadas o arreglando cualquier cosa que tenga que ver con la efectividad de nuestro código.

Eliminar sobrantes

Esta es la funcionalidad que no esperaba de la herramienta y que más me ha llamado la atención. Elimina de nuestro jar todas las clases que no usamos y borra todos los métodos que no se usan en el resto de clases. ¿Y cómo sabe si lo usamos o no?. Pues por la clase que le hemos dicho que contiene el main(). Empieza a tirar de ahí y borra todo lo que no se use. El resultado es que en muchas ocasiones obtendremos un jar mucho más pequeño si llevamos tiempo trabajando en el proyecto y somos reacios a borrar clases y métodos que no se usan "por si acaso me hacen falta más adelante".

Y ya lo máximo, al integralo en maven, el proceso queda totalmente automático, por lo que una vez configurado en nuestro pom.xml, podemos olvidarnos de que usamos esa herramienta y seguir con nuestros comandos de maven típico mvn package, mvn install, mvn deploy, etc. Eso sí, ojo al hacer el javadoc.

Sep 29

jax-ws y maven

 Siguiendo con los web services y yo, he estado probando herramientas como axis2 y jax-ws. Por supuesto, nada me ha funcionado a la primera y llevo casi dos días peleándome con esto para arrancar un "hola mundo"

Con axis2 el problema es que se me ocurrió hacerme mi propia clase de ejemplo, sin copiar la de los tutoriales. Al final descubro que no soporta enumerados de java y qué casualidad, a mí se me había ocurrido poner uno de los métodos con un parámetro enumerado. Una vez conseguido arrancar un servidor y hacerme un cliente, el servidor me da error cuando intento acceder al web service desde el ciente. Ante las pocas pistas que daba el error, decidí dejar axis-2 de momento y probar con jax-ws.

Con jax-ws me cree un proyecto maven y me puse a ello. Pues bien, las dependencias maven teóricas según la documentación son estas https://jax-ws.dev.java.net/guide/Using_JAX_WS_from_Maven.html . Creo el proyecto, arranco el servidor y todo aparentemente correcto. Accedo desde un navegador a http://localhost:8080/MiServicio?WDSL con el que teóricamente debería obtener el fichero WSDL del servicio… y el servidor da error. Buscando el error por google, me encuentro con esto http://forums.java.net/jive/message.jspa?messageID=222799 Parece que la librería sjsxp de la que depende según maven el jax-ws no es correcta y hay que coger la versión 1.0.1 en vez de la 1.0. Así que me toca "tunear" el pom.xml y hacer esta ñapa

 

<dependency>
<groupId>com.sun.xml.ws</groupId>
<artifactId>jaxws-rt</artifactId>
<version>2.1.1</version>
<exclusions>
<exclusion>
<groupId>com.sun.xml.stream</groupId>
<artifactId>sjsxp</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.sun.xml.stream</groupId>
<artifactId>sjsxp</artifactId>
<version>1.0.1</version>
</dependency>
Dicho de otra forma, poner la dependencia de jaxws-rt, decirle que no me traiga su sjsxp y luego poner la dependencia del que yo quiero. Esto ha funcionado bien y ya tengo el servidor arrancado. Ahora toca pelearse con el cliente…. espero no tener demasiados problemas. Luego volveré con axis2.
 
Y aunque todavía tengo que probar más, de momento me gusta mucho más jax-ws que axis2. Los scripts de axis2 generan montones de clases que luego tampoco facilitan tanto la creación de un cliente. jax-ws también genera clases, pero parece que son menos y además quedan "ocultas", ya que genera los .class que luego se empaquetan en el war o jar.
 
Lo segundo que no me ha gustado de axis2 es que aparentemente no tiene posibilidad de hacer un main() que arranque un servidor web con los webservices, dependemos de un Tomcat ajeno con una webapp axis2 instalada sobre la que desplegamos nuestros web services. Bueno, sí tiene un servidor, pero según la documentación puede usarse sin garantías, ya que no está soportado. jax-ws permite hacerse un main y publicar el servicio de una forma tan fácil como esta
public static void main(String [] args) {
   Endpoint.publish("http//localhost:8080/MiServicio", new MiWebService());
   // Aqui puedes seguir haciendo cosas.
}
Bueno, no es nada grave si quieres publicar webservices en internet. Pero en mi caso, necesito que el servidor aparte de publicar los webservices, haga más cosas.

Jun 30

Curiosidad en java

auto-boxing en javaDe este post del foro, sale un ejemplo curioso de java. Fíjate en el siguiente código

public static void main (String [] args) {
   Integer a = 100;
   Integer b = 100;

   System.out.println(a == b);
}

Pues bien, este ejemplo da como resultado "true". Si cambias los valores de a y b (pero manteniéndolos iguales), verás una cosa curiosa. Para valores entre -128 y 127, el resultado es "true". Para valores fuera de ese rango, el resultado es "false".

El motivo podemos encontrarlo en este post sobre Auto-Boxing del Weblog de Victor Ramirez. Teóricamente las instancias de Integer que se crean de forma más o menos automáticamente en esas asignaciones deberían ser distintas y el == debería dar siempre "false". Pero por eficiencia, el compilador reaprovecha instancias de estas clases, de forma que cuando se hace este tipo de conversión (de un tipo primitivo a un objeto Integer, Boolean, o lo que sea), se tiene que == da "true" en los siguientes casos:

  • Siempre en los Boolean y en los Byte
  • Entre -128 y 127 en los Short e Integer
  • Entre u0000 y u007F en los Character.

Una curiosidad que puede dar muchos quebraderos de cabeza a un programador novato que no tiene muy controlada la diferencia entre el método equals() y el ==.

Apr 28

WeakHashMap y una posible solución de diseño

WeakHashMapHace poco descubrí la clase WeakHashMap de java y encontré un ejemplo de su uso con una idea interesante que no se me había ocurrido y que puede ser una alternativa buena de diseño para resolver determinados problemas. Me "explayo":

Imagina que en tu empresa haces proyectos de software en los que siempre hay listados de personas, pero la información que se guarda de cada persona varía de un proyecto a otro. Por ejemplo, en todos los proyectos las personas tienen DNI, nombre, apellidos, fecha de nacimiento, sexo, etc. Pero en un proyecto que hacéis de un taller de coches la persona tiene además asociado el coche que tiene, con su marca, modelo, etc. En otro proyecto que hacéis de nóminas la persona tiene también su cargo en la empresa, sueldo base, complementos, antigüedad, etc. Y así con cualquier tipo de proyecto que se os ocurra relacionado con personas.

Como somos una empresa espabilada y vemos que siempre tenemos personas en los proyectos y unos datos que siempre son comunes, decidimos hacernos una librería en la que tenemos la clase Persona con los atributos comunes, paneles para editar personas, JTable de personas, daos de persistencia en bd, tablas en bd, etc.

Cuando llega un proyecto concreto debemos ampliar los datos de esa Persona que tenemos en la librería. Y se nos presentan varias opciones.

La que rápidamente piensa cualquiera que haya estudiado orientación a objetos (y quizás sin mucha experiencia), es heredar de Persona y añadir en la clase hija los atributos adicionales. Así, en el ejemplo de nuestro taller, tendríamos PersonaConCoche extends Persona y le añadimos todo lo relativo al coche. Esta solución se puede hacer y funciona, pero al implementarla vemos que no acaba de convencer. El código que hagamos adicional para el taller va a tratar con una Persona a la que hay que hacer cast a PersonaConCoche, debemos rellenar los datos de un mismo objeto en dos consultas de bd e insertarlo en dos veces, unos campos primero y otros después, posiblemente en el orden adecuado, etc, etc.

La siguiente opción que se nos ocurre es poner un método en nuestra clase Persona que sea setDatosAdicionales(datosAdicionales). ¿Pero qué tipo ponemos a esos datosAdicionales en nuestra librería general para que nos sirva en cualquier proyecto?. Pues o bien ponemos Object, o bien hacemos que nuestra clase Persona sea un genérico. La primera opción no es especialmente clara, porque nunca sabremos que hay dentro de datosAdicionales y si somos varios programadores, alguno puede despistarse y meter ahí cualquier cosa. Lo del genérico es un rollo, ya que obliga a casi todas las clases de nuestra librería general (paneles, daos de bd) a ser a su vez genéricos.

Y finalmente está la solución del ejemplo de WeakHashMap que había comentado y que, aunque no he probado, tiene muy buena pinta. Consiste básicamente en meter esos datos adicionales directamente en un HashMap. La clave sería nuestra clase Persona tal cual la tenemos en la librería común y el valor los datos adicionales. Podemos concretar el HashMap genérico para nuestros datos concretos HashMap<Persona, Coche> y así todo queda claro sin posibilidad de confusión. No necesitamos tocar nada de nuestra librería y podemos hacer nuestros paneles y daos de Coche por separado. Por supuesto, este WeakHashMap se guarda fuera de la clase Persona (si no, no hemos mejorado nada respecto a las soluciones anteriores) y hay que hacerlo accesible al que lo necesite.

La ventaja para este tipo de solución de un WeakHashMap frente a un HashMap normal es que el primero no se guarda referencias reales a los objetos que tiene almacenados (ni de Persona ni de Coche), por lo que si en nuestra lista de personas (que tenemos aparte) eliminamos una persona y perdemos todas las referencias a ella, el recolector de basura reclamará también la Persona y sus datos guardados en el WeakHashMap. Si lo hubiéramos metido en un HashMap normal, las referencias a Persona y Coche ahí dentro son reales, por lo que el recolector de basura no los libera. por lo que después de borrar la persona de nuestra lista de personas, debemos además acordarnos de hacer y hacer una llamada al hashmap.remove(persona). Ya sabes que un un grupo de uno o más programadores, siempre hay al menos un despistado que se olvida de estas cosas.

No veo el momento de aplicar una solución de este estilo en algún sitio, a ver si me encuentro con pegas prácticas.

Mar 11

Subiendo “extraños” al repositorio de maven

 Cuando usamos maven entre varios desarrolladores en proyectos más o menos grandes, es normal que montemos un repositorio de jars, estilo nexus o archiva. Cuando ejecutamos el comando mvn deploy, maven sube nuestro jar recién compilado a este repositorio y lo hace accesible para los demás desarrolladores.

Sin embargo, es posible subir a este tipo de repositorios ficheros que no sean .jar, podemos subir cualquier tipo de fichero. El comando mvn deploy:deploy-file con los parámetros adecuados, nos permite subir un fichero cualquiera.

¿Para qué queremos subir ficheros que no sean .jar?. Supón que como parte de nuestro proyecto java usamos JNI con una librería .dll cuyos fuentes en C/C++ también desarrollamos nosotros. No parece una buena forma de compartir la .dll recién generada metiéndola en nuestro sistema de control de versiones. Aunque podríamos hacerlo así, es más elegante usar el sistema de control de versiones para ficheros fuente o de texto que generemos a mano y no usarlo para los "artefactos" que construye nuestro proyecto, como ejecutables, librerías, etc. La solución entonces consiste en subir esa .dll recién generada al repositorio de jars, aunque no sea un jar. El comando puede parecerse a esto (no pongo todos los parámetros, que sería muy largo)

mvn deploy:deploy-file -DgroupId=… -DartifactId=libreria -Dversion=1.0 -Dpackaging=dll -Dfile=libreria.dll -Durl=….

Y esto subiría la .dll al repositorio de jars. La parte interesante de este asunto está en el -Dpackaging=dll. Esto hace que maven ponga al fichero la extensión .dll para subirlo y lo subirá con el nombre libreria-1.0.dll, independientemente del nombre que tenga la .dll antes de subirla.

Y ahora viene otra parte interesante. En nuestro proyecto java podemos poner, en el pom.xml, la dependencia en runtime de esa librería que acabamos de subir

<dependency>
   <groupId>…</groupId>
   <artifactId>libreria</artifactId>
   <version>1.0</version>
   <type>dll</type>
   <scope>runtime</scope>
</dependency>

Aquí, nuevamente, la gracia está en poner <type>dll</type>, ya que esto hace saber a maven y al repositorio de jars que en realidad estamos buscando un fichero .dll, que habíamos subido previamente con -Dpackaging=dll

Por supuesto, la ejecución del comando mvn deploy:deploy-file podemos añadirla al proceso de compilado de nuestros fuentes C/C++ y podemos poner version 1.0-SNAPSHOT, de forma que se suban versiones de desarrollo nuevas cada vez que se compile y que el resto de desarrolladores puedan disponer de ellas.

Mar 03

javadoc con UML

Hurgando por ahí me he encontrado con UMLGraph, una herramienta que genera gráficos UML a partir de unos textos que los describen. Lo bueno es que, al menos para el diagrama de clases, el texto que describe el diagrama UML es exactamente igual que un fuente java. Dicho de otra forma, si ponemos en el fichero "class UnaClase extends UnPadre {}", obtendremos un diagrama con una cajita UnaClase que hereda (flechita con triángulo) de una cajita UnPadre. Puedes ver el ejemplo aquí. Esto lo hace ideal para generar los gráficos UML de clases a partir de código fuente ya hecho.

Otro punto interesante es que viene con la posibilidad de invocar javadoc usando UMLGraph, de forma que en nuestro javadoc se generaría diagramas de clases incrustados. UMLGraph viene con un doclet que se puede usar desde el comando javadoc de java y de esta forma, javadoc generará la documentación de la forma habitual, pero incluyendo un gráfico de UML. En la descripción de un package, pondrá todas las clase incluidas en ese package y las relaciones entre ellas. En la descripción de una clase pondrá un dibujo de dicha clase con las relaciones (herencias, dependencias, etc) con otras clases del paquete o de otros paquetes. En la figura puedes ver un ejemplo de javadoc generado con el doclet de UMLGraph.

javadoc con grafico UML

Un detalle a tener en cuenta es que para que UMLGraph pueda generar los ficheros gráficos es necesario tener instalado previamente GraphViz.

En el diagrama de clases podrían configurarse muchas cosas, poner notas asociadas a las clases, poner otro tipo de cajas que no sean de clases, métodos que deben o no mostrarse, etc. La pega de ello es que iría configurado en código a base de anotaciones, por lo que el código quedaría algo "guarreado" para luego ver el dibujo bonito.

También pueden hacerse diagramas de secuencia, pero desgraciadamente la sintaxis del fichero de texto que lo describe ya no es java, así que no deja de ser una forma alternativa de hacer el diagrama. Puede ser interesante, por ejemplo, si guardamos los diagramas de secuencia en un sistema de control de versiones (como subversion). Siempre ocupa menos y es más interesante para ver diferencias con versiones anteriores un fichero de texto que no un gráfico o un proyecto entero de alguna herramienta compleja de generación de gráficos UML (Together, Rational,…).

Y otra cosa que a mí siempre me viene bien, es que UMLGraph está subido al repositorio ibiblio de maven y tiene plugin para el mismo. De esta forma, configurando el fichero pom.xml de nuestro proyecto maven (en concreto, configurando el plugin de javadoc para que use el doclet de UMLGraph), podemos generar el javadoc con gráficos UML directamente desde maven. La configuración sería algo parecido a esto

<reporting>
   <plugins>
      <plugin>
         <artifactId>maven-javadoc-plugin</artifactId>
         <configuration>
            <source>1.5</source>
            <aggregate>true</aggregate>
            <doclet>gr.spinellis.umlgraph.doclet.UmlGraphDoc</doclet>
            <docletArtifact>
               <groupId>gr.spinellis</groupId>
               <artifactId>UmlGraph</artifactId>
               <version>4.6</version>
             </docletArtifact>
             <additionalparam>
                    -inferrel -inferdep -quiet -hide java.*
                    -collpackages java.util.* -qualify
                    -postfixpackage -nodefontsize 9
                   -nodefontpackagesize 7
             </additionalparam>
          </configuration>
       </plugin>
    </plugins>
</reporting>

Con esto, un simple mvn javadoc:javadoc nos generaría la documentación javadoc de nuestro proyecto maven, gráficos UML incluidos.

 

Feb 22

Más sobre los test automáticos

 

Comenté ayer que estaba jugando con fest-swing y otras herramientas para test automáticos de pruebas de las interfaces gráficas de usuario. Pues bien, hoy he seguido un poco y he conseguido resolver la mayor parte de los problemas que se me presentaron. Eran, por supuesto, culpa mía, de no conocer la herramienta y de la mala costumbre de jugar el ensayo y error en vez de leerse la documentación.

Por un lado, en vez de bajarme fest-swing de la página de fest-swing, usé el repositorio maven que hay para ello, dejando que fuera maven el que se bajara el jar de fest-swing y todas sus dependencias. Entiendo que esto no tiene mucho que ver (más bien nada) con los problemas que se me presentaron.

El segundo punto, y este sí es importante, es que si hacemos el ejemplo tonto de test que viene con fest-swing no hay ningún problema, sale bien a la primera. Pero si ejecutamos una batería de test con fest-swing, es muy importante reiniciar los recursos que usa fest-swing. Esto se traduce que en el método tearDown() (que JUnit ejecuta después de cada test), debemos hacer la llamada correspondiente

private FrameFixture window; // Clase de Fest-Swing con la ventana principal.

public void tearDown() {
   window.cleanUp();  // reiniciar recursos
}

Y esto ha solucionado gran parte de los problemas que tenía. Ahora las ventanas de test sí salen cuando se ejecutan desde maven y no hacen tantas cosas raras cuando se ejecutan desde eclipse.

Otro tema importante es que aunque Fest-Swing se encarga de acceder a los componentes en el hilo de tratamiento de eventos de swing, es posible que el orden de estos eventos no sea el que esperamos. Si nuestro test manda visualizar una ventana, he encontrado útil esperar a que dicha ventana esté realmente visible antes de proseguir con el test. Esto se puede conseguir con la llamada a window.requireVisible() de fest-swing. Esta llamada hace fallar el test si la ventana no está visible, pero espera un tiempo prudencial antes de cantar el fallo. De esta forma, la llamada se queda bloqueada hasta que la ventana realmente está visible, o hasta que pasa un tiempo excesivo de espera (configurable, por supuesto).

Lo que no he conseguido es que desde eclipse los test se ejecuten dando resultados siempre. Sí es cierto que con todo esto parece que falla menos, pero siguen saliéndome barras grises o negras (en vez de verdes o rojas) de vez en cuando.

Una vez conseguido que todo funcione más o menos correctamente, lo he metido en Subversion y he esperado a que el sistema de integración continua (Hudson) compilara y ejecutara los test. A pesar de las advertencias en la documentación de fest-swing de que puede haber problemas con la integración continua, todo ha ido sobre ruedas y sin problemas.

Otra característica que me ha parecido interesante de fest-swing, aunque no la he probado, es que si un test falla, fest-swing puede capturar y almacenar la pantalla en el momento que se produce el fallo. De esta forma, viendo la foto de nuestra interfaz gráfica de usuario con el fallo, podemos ver si realmente hay algo incorrecto o el fallo se ha producido por otra circunstancia. Por ejemplo, que la ventana de que "windows se ha actualizado" haya salido justo encima de nuestra interfaz justo cuando la estamos testeando.

Así que de momento fest-swing queda incorporado en mi trabajo como herramienta para hacer test de interfaces gráficas de usuario. Sólo me falta ir convenciendo a mis "compis" para que también la usen.

Comenté también en el post anterior que quería probar Abbott. Bien, después del éxito con fest-swing, no lo he hecho. Pero leyendo la documentación de Abbott, he visto una característica también interesante. Abbott, ¿cómo no?, viene con Costello, una aplicación que permite ejecutar nuestra interfaz de usuario de forma normal (a través de su método main()) y actuar manualmente sobre ella. Costello se encargará de capturar toda nuestra interacción con ella (a modo de grabación), de forma que luego Costello puede reproducirla una y otra vez sobre nuestra aplicación, testeando que los resultados son los mismos. Es una forma interesante de hacer los test, sin necesidad de programar demasiado. A pesar de que usaremos fest-swing, probaré Abbott y Costello en algún momento, para ver si se puede testear el sistema completo en el entorno de pruebas.