Σάββατο 5 Οκτωβρίου 2013

On the continuous integration (jenkins, maven, junit, jacoco) of multi-module projects using DI frameworks

This is anything BUT strictly Bo2, but it is useful and fun. Bo2 offers lightweight dependency injection. The primary functionality, without going into many details, is similar to the core concept of Google Guice. A Bo2 supported application with a mix of convention and configuration may correspond java interfaces to implementation classes.
This leads to the following, quite usual scenario with DI frameworks: Core module OfficeAutomation declares interface LoginOperation that represents the business layer concept for logging into the system using a username and a password. This interface is implemented in module AbcUserManagement with class AbcLoginOperationImpl. The existence of AbcUserManagement module is not known to the rest of the system, and only in the Bo2 deployment configuration it is visible that LoginOperation is associated, on runtime, with AbcLoginOperationImpl. This way, the core system can be shipped to another customer with the single requirement to implement a number of modules (plugins) based on known contracts defined in the core application.
At the same time, monitoring the progress of the development of large systems with respect to build stability and test success and coverage has practically become a standard in the domain. The setup with Jenkins as a continuous integration server and Maven as a build and dependency management platform is very straightforward and popular. JUnit as a testing framework and JaCoCo as a test coverage measuring tool integrate well with both Maven and Jenkins in order to produce timely reporting of failures as well as nice trending charts for both test success and test coverage on the production code.
Due to the nature of the dependencies of the core module(s) with the plugin modules, it is not possible to run all unit and integration tests without introducing extra integration modules. Maven abhors the existence of cyclic dependencies and it is impossible to declare on the poms that module AbcUserManagement depends compile time on OfficeAutomation while the latter depends on the first one on runtime (or test time). These integration modules contain only the integration tests and no production code. So far, so good, this is all straightforward. What is not too straightforward, but is often desired, is to get production code coverage induced by integration tests to be integrated on the production code coverage measurements of the unit tests.
This post solves this without using any extra tools other than the aforementioned and a bit of good old Ant. You will also find a link to a sample project with a core and an integration tests module that provides a proof of concept. The solution builds on the following maven plugins:
  • maven-surefire-plugin: this runs the JUnit tests found in the modules
  • maven-source-plugin: this installs source jars to the repository for built modules
  • jacoco-maven-plugin: provides the JaCoCo runtime agent to on test execution time.
  • maven-dependency-plugin: this unpacks source and class file. This is necessary in order to have the jacoco plugin instrument the classes of the dependencies of the integration projects
  • maven-antrun-plugin: to run some ant for the integration tests coverage
Let's get to it. This is the example multi-module project to test and demo the solution on:
Project layout
Foo has three methods: unit(), a method that is unit tested; integration() a method that cannot be unit tested in FooTest and we test it IntegrationTest that belongs to the integration tests module, it; finally untested(), a method that we were too lazy to test. 
This is the pom of p:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 
 <groupId>test</groupId>
 <artifactId>p</artifactId>
 
 <version>1.0-SNAPSHOT</version>
 
 <packaging>pom</packaging>

 <properties>
  <version.junit>4.11</version.junit>
  
  <version.jacoco>0.6.3.201306030806</version.jacoco>
  <version.jacoco.ant>0.6.3.201306030806</version.jacoco.ant>
  <version.maven-surefire-plugin>2.15</version.maven-surefire-plugin>
  <version.maven-antrun-plugin>1.7</version.maven-antrun-plugin>
  <version.maven-source-plugin>2.2.1</version.maven-source-plugin>
  <version.maven-dependency-plugin>2.8</version.maven-dependency-plugin>
  <version.eclipse.m2e.lifecycle>1.0.0</version.eclipse.m2e.lifecycle>
 </properties>

 <modules>
  <module>a</module>
  <module>it</module>
 </modules>

 <dependencies>

  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>${version.junit}</version>
   <scope>test</scope>
  </dependency>

 </dependencies>

 <build>
  <plugins>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>${version.maven-surefire-plugin}</version>
   </plugin>

   <plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>${version.jacoco}</version>
    <executions>
     <execution>
      <id>jacoco-initialize</id>
      <goals>
       <goal>prepare-agent</goal>
      </goals>
     </execution>
     <execution>
      <id>jacoco-site</id>
      <phase>package</phase>
      <goals>
       <goal>report</goal>
      </goals>
     </execution>
    </executions>
   </plugin>

   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-source-plugin</artifactId>
    <version>${version.maven-source-plugin}</version>
    <executions>
     <execution>
      <id>attach-sources</id>
      <goals>
       <goal>jar</goal>
      </goals>
     </execution>
    </executions>
   </plugin>

  </plugins>
 </build>

</project>

Nothing spectacular, the configuration of the maven-jacoco-plugin is the standard configuration from the plugin examples. No rules specified here, these will be taken care in the Jenkins configuration who will generate the final coverage report.
The pom of a is trivial:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 
  <parent>
    <groupId>test</groupId>
    <artifactId>p</artifactId>
    <version>1.0-SNAPSHOT</version>
  </parent>
  
  <groupId>test.p</groupId>
  <artifactId>a</artifactId>
  
 </project>

The pom of it is more interesting: The maven dependency plugin unpacks classes and source dependencies of it module (module a, specifically) in the generate-resources phase. Note where these are unpacked, it will be useful later (and iirc Jenkins expects to find them in these directories). The maven-ant-run plugin runs an ant file after making the org.jacoco.ant.ReportTask ant task known to ant as report. The pluginManagement section is only useful to make eclipse shut up :)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <parent>
  <groupId>test</groupId>
  <artifactId>p</artifactId>
  <version>1.0-SNAPSHOT</version>
 </parent>

 <groupId>test.p</groupId>
 <artifactId>it</artifactId>

 <dependencies>
  <dependency>
   <groupId>test.p</groupId>
   <artifactId>a</artifactId>
   <version>1.0-SNAPSHOT</version>
   <scope>test</scope>
  </dependency>
 </dependencies>

 <build>

  <plugins>

   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>${version.maven-dependency-plugin}</version>
    <executions>
     <execution>
      <id>unpack-classes</id>
      <phase>generate-resources</phase>
      <configuration>
       <includeGroupIds>test.p</includeGroupIds>
       <outputDirectory>${project.build.directory}/classes</outputDirectory>
      </configuration>
      <goals>
       <goal>unpack-dependencies</goal>
      </goals>
     </execution>
     <execution>
      <id>unpack-sources</id>
      <phase>generate-resources</phase>
      <configuration>
       <classifier>sources</classifier>
       <includeGroupIds>test.p</includeGroupIds>
       <outputDirectory>${project.build.directory}/sources</outputDirectory>
      </configuration>
      <goals>
       <goal>unpack-dependencies</goal>
      </goals>
     </execution>
    </executions>
   </plugin>

   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-antrun-plugin</artifactId>
    <version>${version.maven-antrun-plugin}</version>
    <executions>
     <execution>
      <id>default-cli</id>
      <phase>post-integration-test</phase>
      <goals>
       <goal>run</goal>
      </goals>
      <configuration>
       <target>
        <taskdef name="report" classname="org.jacoco.ant.ReportTask"
         classpathref="maven.plugin.classpath" />
        <ant antfile="${basedir}/build.xml" />
       </target>
      </configuration>
     </execution>
    </executions>
    <dependencies>
     <dependency>
      <groupId>org.jacoco</groupId>
      <artifactId>org.jacoco.ant</artifactId>
      <version>${version.jacoco.ant}</version>
     </dependency>
    </dependencies>
   </plugin>

  </plugins>

  <pluginManagement>

   <plugins>
    <!--This plugin's configuration is used to store Eclipse m2e settings 
     only. It has no influence on the Maven build itself. -->
    <plugin>
     <groupId>org.eclipse.m2e</groupId>
     <artifactId>lifecycle-mapping</artifactId>
     <version>${version.eclipse.m2e.lifecycle}</version>
     <configuration>
      <lifecycleMappingMetadata>
       <pluginExecutions>
        <pluginExecution>
         <pluginExecutionFilter>
          <groupId>
           org.apache.maven.plugins
          </groupId>
          <artifactId>
           maven-dependency-plugin
          </artifactId>
          <versionRange>
           [2.1,)
          </versionRange>
          <goals>
           <goal>
            unpack-dependencies
           </goal>
          </goals>
         </pluginExecutionFilter>
         <action>
          <ignore></ignore>
         </action>
        </pluginExecution>
       </pluginExecutions>
      </lifecycleMappingMetadata>
     </configuration>
    </plugin>
   </plugins>

  </pluginManagement>

 </build>

</project>

Last, but not least, the ant file. Note that a standard ant installation will suffice, no need for antcontrib or anything. Also, the default target will never fail and will exit gracefully if the tests have not run (typical when someone builds just to install in a local repo).
<!--
Helper for integration tests related ant tasks executed by
the maven-ant-run plugin. See the pom.xml. The report task
is made available by the target executed in the pom maven
ant run plugin configuration. 

We want to check if jacoco.exec exists before attempting to
generate the report. This is done nicely in an ant build.xml
that is called via the pom configuration with an ant call.
This way, a developer may build the project without actually
running the tests.
-->
<project name="it.helper" default="it.report">
 
 <target name="it.report" depends="flag" if="exists">
  <report>
   <executiondata>
    <file file="${project.build.directory}/jacoco.exec"/>
   </executiondata>
   <structure name="Integration test coverage">
    <classfiles>
     <fileset dir="${project.build.directory}/classes"/>
    </classfiles>
    <sourcefiles encoding="${project.build.sourceEncoding}">
     <fileset dir="${project.build.directory}/sources"/>
    </sourcefiles>
   </structure>
  </report>
 </target>
 
 <target name="flag">
  <available file="${project.build.directory}/jacoco.exec" property="exists"/>
 </target>
 
</project>

After all that is done, the only thing that remains is to configure the job on Jenkins. This is fairly straightforward and won't be covered here. When Jenkins runs the job, expect to see something like this:

Jenkins coverage report on class Foo
One nasty lesson learned while applying this prototype to an existing real world project, is this: Maven promotes and uses heavily convention over configuration. It's plugins do the same. The jacoco plugin modifies the property referenced by ${argLine} that is the default configuration of the maven surefire plugin. If, by any chance you need to configure the surefire plugin to pass additional jvm arguments, do not leave ${argLine} out. It will be confusing and frustrating.

An honorary mention goes to this blog post, which gave some basic ideas on how to do something similar. As always, stackoverflow was useful to fill the blanks left by the maven docs (or my inability to read through them properly and comprehend them).

Edit: You can download the demo project here

Δεν υπάρχουν σχόλια:

Δημοσίευση σχολίου