Ejecución de tests unitarios con junit en proyectos ant y su integración en jenkins y sonar para medir la cobertura.
0. Índice de contenidos.
- 1. Introducción.
- 2. Entorno.
- 3. junit con ant.
- 4. Integración en jenkins.
- 5. Configuración de sonar.
- 6. Referencias.
- 7. Conclusiones.
- 8. Extra ball: informes de cobertura con jacoco.
1. Introducción
En ocasiones tenemos que desaprender lo aprendido y, en el caso que nos ocupa, acostumbrados a llevar a cabo la gestión de la configuración de
nuestros proyectos con maven, volver a ant es todo un reto. En este tutorial vamos a ver cómo integrar una fase de
ejecución de tests unitarios como una tarea de ant, forzando a que pasen los tests para llevar a cabo el empaquetado.
El objetivo no es más que emular lo que estamos acostumbrados a hacer con el soporte de maven, pero con ant, con lo
que la configuración de los pasos dentro de ciclo de vida es totalmente manual, tenemos que olvidar la configuración de
plugins a la que estamos acostumbrados con maven y configurar los targets de ant necesarios.
Una vez tengamos ese soporte a nivel de gestión de configuración lo elevaremos al entorno de integración continua
de tal modo que veremos como llevar a cabo la configuración del proyecto a nivel de jenkins y sonar.
El objetivo final es medir la cobertura de los tests de junit que se ejecutarán con el soporte de ant en sonar, para
reforzar las métricas de calidad.
2. Entorno.
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil MacBook Pro 15′ (2.4 GHz Intel Core i7, 8GB DDR3 SDRAM).
- Mac OS X Lion 10.7.5
- Ant 1.8.2
- Sonar 3.5.1
- Jenkins 1.513
- Sonar Runner 2.2.2
3. junit con ant.
Lo primero es exponer el contenido de nuestro fichero de ant, nuestro build.xml:
<?xml version="1.0" encoding="UTF-8"?> <project name="build" basedir="." default="main"> <property name="src.dir" value="src"/> <property name="test.dir" value="test"/> <property name="lib.dir" value="lib"/> <property name="build.dir" value="bin"/> <property name="classes.dir" value="${build.dir}/classes"/> <property name="jar.dir" value="${build.dir}"/> <path id="classpath"> <fileset dir="${lib.dir}" includes="org/**/*.jar"/> </path> <path id="application" location="${classes.dir}"/> <target name="clean"> <delete dir="${build.dir}"/> </target> <target name="compile"> <mkdir dir="${classes.dir}"/> <javac fork="true" srcdir="${src.dir}" destdir="${classes.dir}" classpathref="classpath" includeantruntime="false" /> <copy todir="${classes.dir}"> <fileset dir="src" /> </copy> </target> <target name="jar" depends="tests"> <mkdir dir="${jar.dir}"/> <jar destfile="${jar.dir}/${ant.project.name}.jar" basedir="${classes.dir}" /> </target> <target name="compile-tests" depends="compile"> <mkdir dir="${classes.dir}-tests"/> <javac fork="true" srcdir="${test.dir}" destdir="${classes.dir}-tests" classpathref="classpath" includeantruntime="false"> <classpath> <pathelement location="${lib.dir}/junit-4.11.jar"/> <path refid="application"/> </classpath> </javac> </target> <target name="tests" depends="compile-tests"> <junit printsummary="yes" haltonfailure="yes"> <classpath> <pathelement location="${lib.dir}/junit-4.11.jar"/> <pathelement location="${lib.dir}/hamcrest-core-1.1.jar"/> <path location="${classes.dir}"/> <path location="${classes.dir}-tests"/> </classpath> <formatter type="xml"/> <batchtest fork="yes" todir="${test.dir}/reports"> <fileset dir="${test.dir}"> <include name="**/*Test.java"/> </fileset> </batchtest> </junit> </target> <target name="clean-build" depends="clean,jar"/> <target name="main" depends="clean-build"/> </project>
Lo más interesante es el uso que hacemos de la etiqueta <junit que nos permite:
- definir un classpath para la ejecución de los tests,
- indicar el formato de salida de los informes de ejecución de los tests, y
- con <batchtest indicar el path de ejecución de los tests con una expresión.
Con el atributo depends establecemos un orden de ejecución y con haltonfailure=»yes» estamos indicando que si fallan los tests
se pare la ejecución, con lo que, en este caso, no se generará el jar.
Desde la vista de ant de eclipse los targets son los siguientes:
Efectivamente, estamos emulando el ciclo de vida de maven, pero con ant y, como lo hemos planteado, el único
problema que debemos resolver es la gestión de dependencias que nos facilita maven, manualmente con ant.
Las soluciones posibles son las de antaño:
- mantener las librerías de las que depende a nivel de proyecto, como en el ejemplo que exponemos,
en el que tendríamos un directorio lib a nivel de proyecto con las librerías, o - disponer de un proyecto de recursos o librerías comunes a todos los proyectos versionado en el SCM.
Para el objetivo de este tutorial vamos a presuponer que no disponemos de un repositorio de control de versiones, no solo por el
tema de la gestión de dependencias sino también para su configuración en jenkins.
Si ejecutamos la build deberíamos tener una salida como la que sigue:
... [junit] Running com.autentia.tntconcept.domain.ObjectDaoTest [junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0,205 sec jar: [mkdir] Skipping /Applications/development/eclipseJuno/wokspace/tntconcept/bin because it already exists. [jar] Building jar: /Applications/development/eclipseJuno/wokspace/tntconcept/bin/tntconcept.jar [jar] adding directory META-INF/ [jar] adding entry META-INF/MANIFEST.MF [jar] adding directory com/ [jar] adding directory com/autentia/ [jar] adding directory com/autentia/tntconcept [jar] adding directory com/autentia/tntconcept/domain [jar] adding entry com/autentia/tntconcept/domain/ObjectDao.class [jar] No Implementation-Title set.No Implementation-Version set.No Implementation-Vendor set. [jar] Location: /Applications/development/eclipseJuno/wokspace/tntconcept/build.xml:31: clean-build: main: BUILD SUCCESSFUL Total time: 1 second
4. Integración en jenkins.
Una vez tenemos el soporte de ant para la ejecución de la fase de tests vamos a configurar el proyecto dentro de
nuestro entorno de integración continua comenzando por crear un nuevo job en jenkins.
Como no tenemos el soporte de un SCM, no podemos seleccionar uno, usaremos el directorio de espacio de trabajo local
de jenkins para realizar esta prueba de concepto.
En mi caso he hecho una copia local del proyecto en el directorio /Users/miusuario/.jenkins/jobs/tntconcept
y después he asignado las siguientes acciones:
Con ello, ejecutando una build deberíamos tener una ejecución correcta y una salida por consola de la ejecución como la que sigue:
Lanzada por el usuario anonymous Ejecutando.en el espacio de trabajo /Users/miusuario/.jenkins/jobs/tntconcept/workspace [workspace] $ ant -file build.xml main Buildfile: /Users/miusuario/.jenkins/jobs/tntconcept/workspace/build.xml clean: [delete] Deleting directory /Users/miusuario/.jenkins/jobs/tntconcept/workspace/bin compile: [mkdir] Created dir: /Users/miusuario/.jenkins/jobs/tntconcept/workspace/bin/classes [javac] Compiling 3 source files to /Users/miusuario/.jenkins/jobs/tntconcept/workspace/bin/classes compile-tests: [mkdir] Created dir: /Users/miusuario/.jenkins/jobs/tntconcept/workspace/bin/classes-tests [javac] Compiling 1 source file to /Users/miusuario/.jenkins/jobs/tntconcept/workspace/bin/classes-tests tests: [junit] Running com.autentia.tntconcept.domain.ObjectDaoTest [junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0,038 sec jar: [jar] Building jar: /Users/miusuario/.jenkins/jobs/tntconcept/workspace/bin/tntconcept.jar clean-build: main: BUILD SUCCESSFUL Total time: 1 second Grabando resultados de tests Finished: SUCCESS
5. Configuración de sonar.
Para llevar a cabo la configuración de sonar en jenkins primero debemos instalar el puglin en jenkins que nos da soporte para hacer uso de sonar runner.
En la configuración de jenkins seleccionamos un tipo de instalación
Y a nivel de proyecto podemos asignar los siguientes parámetros:
Con estas propiedades no es necesario tener un sonar-project.properties a nivel de proyecto.
Con ello ahora deberíamos tener una salida por consola como la que sigue:
[workspace] $ /Users/miusuario/.jenkins/tools/hudson.plugins.sonar.SonarRunnerInstallation/sonar-runner/bin/sonar-runner -Dsonar.projectBaseDir=/Users/miusuario/.jenkins/jobs/tntconcept/workspace -Dsonar.dynamicAnalysis=reuseReports -Dsonar.sources=src -Dsonar.binaries=bin -Dsonar.projectVersion=0.0.1-SNAPSHOT -Dsonar.projectKey=com:autentia:tntconcept -Dsonar.projectName=tntconcept Sonar Runner 2.2.2 Java 1.6.0_43 Apple Inc. (64-bit) Mac OS X 10.7.5 x86_64 INFO: Runner configuration file: /Users/jmsanchez/.jenkins/tools/hudson.plugins.sonar.SonarRunnerInstallation/sonar-runner/conf/sonar-runner.properties INFO: Project configuration file: NONE INFO: Default locale: "es_ES", source code encoding: "MacRoman" (analysis is platform dependent) INFO: Work directory: /Users/miusuario/.jenkins/jobs/tntconcept/workspace/.sonar INFO: Sonar Server 3.5.1 ... ... ... INFO: ------------------------------------------------------------------------ INFO: EXECUTION SUCCESS INFO: ------------------------------------------------------------------------ Total time: 12.760s Final Memory: 7M/81M INFO: ------------------------------------------------------------------------ Grabando resultados de tests Finished: SUCCESS
Y accediendo a sonar deberíamos disponer una información como la que sigue:
Casi listo, pero… ¿y la cobertura?, ooooops que no se nos olvide que esto ya no es maven…
5.1. Informes de cobertura con el soporte de jacoco.
Pues si, necesitamos algo más de configuración para generar los informes de cobertura y poder importar esa información en sonar para que forme parte de las métricas.
Sonar utiliza por defecto jacoco para medir la cobertura y lo que podemos hacer es añadir a la tarea de ant de junit el soporte de jacoco para generar un informe de cobertura.
Para ello, debemos modificar nuestro build.xml para que quede como sigue:
<?xml version="1.0" encoding="UTF-8"?> <project name="tntconcept" basedir="." default="main"> <property name="src.dir" value="src"/> <property name="test.dir" value="test"/> <property name="lib.dir" value="lib"/> <property name="build.dir" value="bin"/> <property name="classes.dir" value="${build.dir}/classes"/> <property name="jar.dir" value="${build.dir}"/> <path id="classpath"> <fileset dir="${lib.dir}" includes="org/osgi/**/*.jar"/> </path> <path id="application" location="${classes.dir}"/> <target name="clean"> <delete dir="${build.dir}"/> </target> <target name="compile"> <mkdir dir="${classes.dir}"/> <javac fork="true" debug="true" srcdir="${src.dir}" destdir="${classes.dir}" classpathref="classpath" includeantruntime="false" /> </target> <target name="jar" depends="tests"> <mkdir dir="${jar.dir}"/> <jar destfile="${jar.dir}/${ant.project.name}.jar" basedir="${classes.dir}" /> </target> <target name="compile-tests" depends="compile"> <mkdir dir="${classes.dir}-tests"/> <javac fork="true" debug="true" srcdir="${test.dir}" destdir="${classes.dir}-tests" classpathref="classpath" includeantruntime="false"> <classpath> <pathelement location="${lib.dir}/junit/junit/4.11/junit-4.11.jar"/> <path refid="application"/> </classpath> </javac> </target> <target name="tests" depends="compile-tests"> <mkdir dir="${test.dir}/reports"/> <!-- Import the JaCoCo Ant Task --> <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml"> <classpath path="${lib.dir}/jacocoant.jar" /> </taskdef> <jacoco:coverage destfile="bin/jacoco.exec" xmlns:jacoco="antlib:org.jacoco.ant"> <junit fork="yes" printsummary="yes" haltonfailure="yes"> <classpath> <pathelement location="${lib.dir}/junit-4.11.jar"/> <pathelement location="${lib.dir}/hamcrest-core-1.1.jar"/> <path location="${classes.dir}"/> <path location="${classes.dir}-tests"/> <pathelement location="${lib.dir}/jacocoant.jar" /> </classpath> <formatter type="xml"/> <batchtest fork="yes" todir="${test.dir}/reports"> <fileset dir="${test.dir}"> <include name="**/*Test.java"/> </fileset> </batchtest> </junit> </jacoco:coverage> </target> <target name="clean-build" depends="clean,jar"/> <target name="main" depends="clean-build"/> </project>
Unos sutiles cambios:
- hemos añadido el atributo debug=»true» a la compilación para que la compilación
se realice con información de debug, - hemos añadido la definición de una tarea importandola del fichero de configuración
org/jacoco/ant/antlib.xml que podemos encontrar en la distribución de jacoco y - hemos envuelto la tarea de junit con la tarea de cobertura de jacoco, generando la salida en el fichero jacoco.exec
Además de lo anterior, debemos añadir las siguientes propiedades a la configuración de sonar runner del proyecto:
Con esta configuración deberíamos tener en la salida por consola las siguientes líneas
... ... ... tests: [jacoco:coverage] Enhancing junit with coverage [junit] Running com.autentia.tntconcept.domain.ObjectDaoTest [junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0,051 sec ... ... ... 23:08:00.448 INFO - Sensor JaCoCoSensor... 23:08:00.451 INFO - Analysing /Users/miusuario/.jenkins/jobs/tntconcept/workspace/bin/jacoco.exec 23:08:00.502 INFO - Sensor JaCoCoSensor done: 54 ms ... ... ...
Y, por fin, en nuestro dashboard de sonar la información de cobertura:
6. Referencias.
- https://wiki.jenkins-ci.org/display/JENKINS/Building+a+software+project#Buildingasoftwareproject-BuildsforNonSourceControlProjects
- http://www.eclemma.org/jacoco/
- https://github.com/SonarSource/sonar-examples/blob/master/projects/code-coverage/ut/ant/ut-ant-jacoco-runTests/build.xml
7. Conclusiones.
Sin el soporte de maven todo se hace más tedioso, por lo manual; pero en una arquitectura con una gestión de
la configuración basada en ant, si queremos incluir en el desarrollo el aseguramiento de la calidad con herramientas de
integración contínua y métricas de calidad automáticas, no es más que documentarse y dedicarle tiempo.
Un saludo.
Jose
8. Extra ball: informes de cobertura con jacoco.
Si, por lo que sea, no disponemos de un entorno de integración continua esto es, no tenemos ni jenkins ni sonar,
podemos generar los informes de jacoco dentro de la propia tarea de junit de ant, añadiendo la siguiente configuración a nuestro build.xml
<?xml version="1.0" encoding="UTF-8"?> <project name="tntconcept" basedir="." default="main" xmlns:jacoco="antlib:org.jacoco.ant"> ... <target name="tests" depends="compile-tests"> <mkdir dir="${test.dir}/reports"/> <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml"> <!-- Update the following line, or put the "jacocoant.jar" file in your "$HOME/.ant/lib" folder --> <classpath path="${lib.dir}/jacocoant.jar" /> </taskdef> <jacoco:coverage destfile="bin/jacoco.exec" xmlns:jacoco="antlib:org.jacoco.ant"> <junit fork="yes" printsummary="yes" haltonfailure="yes"> <classpath> <pathelement location="${lib.dir}/junit-4.11.jar" /> <pathelement location="${lib.dir}/hamcrest-core-1.1.jar"/> <path location="${classes.dir}"/> <path location="${classes.dir}-tests"/> <pathelement location="${lib.dir}/jacocoant.jar" /> </classpath> <formatter type="xml"/> <batchtest fork="yes" todir="${test.dir}/reports"> <fileset dir="${test.dir}"> <include name="**/*Test.java"/> </fileset> </batchtest> </junit> </jacoco:coverage> <jacoco:report> <executiondata> <file file="bin/jacoco.exec"/> </executiondata> <structure name="${ant.project.name}"> <classfiles> <fileset dir="${build.dir}"/> </classfiles> <sourcefiles encoding="UTF-8"> <fileset dir="${src.dir}"/> </sourcefiles> </structure> <html destdir="${test.dir}/reports"/> </jacoco:report> </target> ... </project>
En el directorio de salida de los informes tendremos disponible los informes de jacoco cada vez que ejecutemos la tarea manualmente.