Pour démarrer sur le sujet, vous avez suivi les étapes écrites sur mon post, comme je l’ai fait lors de ma présentation. C’est bien joli tout ça, mais dans la vraie vie réelle, vous n’avez pas un module Maven dédié au code JavaScript. Votre code JavaScript est (bêtement) dans votre application web ! Du coup, vous vous posez des questions sur la façon de procéder, en particulier concernant les analyses Sonar…
Allez zou, suivez le guide !
Lors de ma présentation, j’ai utilisé l’archetype Maven de.akquinet.javascript.archetypes:javascript-quickstart, qui crée un squelette d’application contenant juste du JavaScript.
Cette fois-ci, on va partir d’un projet web classique dans lequel on incluera notre code JavaScript. Donc quelque chose de plus proche que ce que vous avez sur votre projet !
Je pars donc avec l’archetype org.codehaus.mojo.archetypes:webapp-javaee6 pour avoir un squelette d’application web. Il s’agit là de votre propre projet web… De mon côté, j’y ajoute une petite classe Java, ainsi qu’une class de test JUnit pour le fun.
Désormais, on peut lancer une première analyse Sonar, avec la commande mvn clean install sonar:sonar.
Maintenant, je rajoute mon code JavaScript (les fichiers underscore.js et undescore-test.js). Je les positionne respectivement dans les répertoires src/main/javascript et src/test/javascript. Il est possible d’adapter ces répertoires, bien entendu !
On ajoute le fichier de configuration de jsTestDriver.conf qui va nous permettre de lancer nos tests JavaScript de Jasmine. On ajoute aussi dans un coin (le répertoire lib/ disons) :
jasmine.js ;jasmineAdapter.js (pour lancer les tests JavaScript écrits en Jasmine avec js-test-driver) ;coverage-1.3.4.b.jar, qui est le plugin de calcul de couverture du code JavaScript pour js-test-driver.Au final, la structure de mon projet a la tête suivante :
chouchoutage (oui, c'est le nom pourri de mon projet)
+ pom.xml
` src
+ lib
| ` coverage-1.3.4.b.jar
| ` jasmine.js
| ` jasmineAdapter.js
+ main
| + java
| | ` ze-code-java
| ` javascript
| ` underscore.js
+ test
| + java
| | ` test-unitaires
| ` javascript
| ` underscore-test.js
` webapp
` toute-la-partie-web
Si vous souhaitez plus d’informations sur cette étape (comme le contenu du fichier jsTestDriver.conf par exemple), allez jeter un oeil sur mon post décrivant ma session Devoxx.
Voilà, on arrive à quelque chose qui ressemble en gros à votre projet. Maintenant, on va voir comment on va pouvoir exécuter nos codes JavaScript, et en faire l’analyse Sonar.
Attaquons nous au problème principal : la modification du pom. Nous allons d’abord l’adapter pour exécuter nos tests JavaScript. Nous verrons ensuite pour l’analyse Sonar…
Ajoutons d’abord la dépendance suivante :
<dependency>
<groupId>com.googlecode.jstd-maven-plugin</groupId>
<artifactId>jstd-maven-plugin</artifactId>
<version>1.3.2.5</version>
<scope>test</scope>
</dependency>
On ajoute ensuite le plugin de jstd-maven-plugin :
<plugin>
<groupId>com.googlecode.jstd-maven-plugin</groupId>
<artifactId>jstd-maven-plugin</artifactId>
<version>1.3.2.5</version>
<configuration>
<port>9876</port>
<!-- A adapter ! -->
<browser>/Applications/Firefox.app/Contents/MacOS/firefox-bin</browser>
<tests>all</tests>
<config>jsTestDriver.conf</config>
<testOutput>target/jstestdriver</testOutput>
</configuration>
<executions>
<execution>
<id>run-tests</id>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
</plugin>
Dernier point, nous ajoutons le repository contenant les plugins sus-cités :
<repositories>
<repository>
<id>jstd-maven-plugin google code repo</id>
<url>http://jstd-maven-plugin.googlecode.com/svn/maven2</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>jstd-maven-plugin google code repo</id>
<url>http://jstd-maven-plugin.googlecode.com/svn/maven2</url>
</pluginRepository>
</pluginRepositories>
Voilà, nous pouvons vérifier si cela fonctionne. La commande mvn clean install va nous le dire :
------------------------------------------------------- T E S T S ------------------------------------------------------- Running fr.linsolas.WorldTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.053 sec Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] --- jstd-maven-plugin:1.3.2.5:test (run-tests) @ chouchoutage --- ------------------------------------------- J S T E S T D R I V E R ------------------------------------------- Firefox: Runner reset. ................ Total 16 tests (Passed: 16; Fails: 0; Errors: 0) (10,00 ms) Firefox 9.0.1 Mac OS: Run 16 tests (Passed: 16; Fails: 0; Errors 0) (10,00 ms)
C’est plutôt une bonne nouvelle ça ! Surefire a lancé mon test JUnit (fr.linsolas.WorldTest), puis, dans un second temps, a exécuté les tests de js-test-driver.
Et bien voilà, il ne nous reste plus qu’à exécuter une analyse Sonar et le tour est joué… Mais ce n’est pas si simple en fait !
Sonar permet bien d’analyser un projet Java, il permet d’analyser également un projet JavaScript, mais hélas pas en même temps !
Nous allons devoir donc lancer deux analyses Sonar pour arriver à nos fins.
Etant donné que nous ne voulons pouvoir lancer avec le même pom.xml l’analyse Sonar pour le code Java et l’analyse Sonar pour le code JavaScript, nous allons créer un profil Maven qui sera dédié à l’analyse JavaScript :
<profiles>
<profile>
<id>js</id>
<properties>
<sonar.language>js</sonar.language>
<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
</properties>
</profile>
</profiles>
Voyons voir si cela fonctionne. Lançons une première analyse avec mvn clean install sonar:sonar. En exécutant cette commande, l’analyse Sonar ne montre aucune différence avec celle réalisée plus tôt. Normal, me direz-vous, on reste sur l’analyse Java.
Lançant maintenant la même commande, mais en activant le profile js : mvn -Pjs clean install sonar:sonar. Le résultat n’est pas celui attendu :
Houlà, mais on a tout cassé ! On n’obtient plus aucun chiffre, ni rien
Le problème vient simplement du fait que le plugin JavaScript pour Sonar va chercher les sources dans le répertoire src/main/java au lieu de src/main/javascript.
Il faut donc lui spécifier ce nouveau répertoire. On va donc spécifier un sourceDirectory dans notre profile js. Du moins on aimerait bien, car hélas Maven n’autorise pas que l’on redéfinisse ce répertoire dans un profil ! C’est étrange, mais c’est comme ça. Enfin pas tant que ça, car Maven – par défaut – n’accepte qu’un seul chemin de sources. Comme plusieurs profiles peuvent être actifs en même temps, on pourrait bidouiller les profiles pour avoir ainsi plusieurs répertoires de sources. On s’égare un peu là…
Nous pourrions passer par le plugin build-helper qui permet de spécifier plusieurs répertoires de sources. Je n’en ferais rien, je vais « simplement » fourvoyer Maven en jouant avec les propriétés :
<properties>
<source.dir>src/main/java</source.dir>
<test.source.dir>src/test/java</test.source.dir>
</properties>
<build>
<sourceDirectory>${source.dir}</sourceDirectory>
<testSourceDirectory>${test.source.dir}</testSourceDirectory>
...
</build>
<profiles>
<profile>
<id>js</id>
<properties>
<source.dir>src/main/javascript</source.dir>
<test.source.dir>src/test/javascript</test.source.dir>
...
Voilà, Maven n’y voit que du feu, et moi j’ai réussi à me débrouiller (à bidouiller disons plutôt).
Alors, que donne l’analyse Sonar maintenant ?
OUI ! Enfin, nous réussissons à obtenir notre belle analyse du code JavaScript par Sonar !
Et voilà, c’est fini.
Vraiment ? Non !
Un dernier souci se pose : si je lance l’analyse Java, j’ai un beau rapport Sonar. Mais dès que je lance l’analyse JavaScript, mon rapport JavaScript efface celui de Java !
Il faut donc trouver un moyen de faire cohabiter les deux. Je propose simplement de profiter de l’option sonar.branch pour différencier les deux projets. On ajoute donc dans le profil js :
<profiles>
<profile>
<id>js</id>
<properties>
<sonar.branch>javascript</sonar.branch>
...
Cette fois-ci, on relance les deux analyses : mvn clean install sonar:sonar puis mvn -Pjs clean install sonar:sonar.
Une fois les deux commandes terminées, nous nous retrouvons avec non plus un mais deux projets Sonar :
Chacun de ces projets est adapté à son langage, le premier pour Java, le second pour JavaScript.
Et voilà, c’est fini.
Vraiment ? Cette fois, oui
N’hésitez pas à faire part, dans les commentaires, de vos propres expériences, ou propositions d’améliorations !
Start Slide Show with PicLens Lite
Dans mon exemple, je me baserais sur les outils suivants : Sonar 2.8, Maven 2.2.1, Java 1.6.
Au sein de l’un de mes projets, je dispose de quelques rares tests unitaires, qui couvre environ 7% de l’application. C’est peu. Très peu. Trop peu même.
Mais nous avons également des tests d’intégration.
En gros, ces tests d’intégration vont exécuter un jeu d’environ 300 fichiers d’entrée, qui seront ensuite analysés par mon application.
Mon but est donc de connaître, en plus de ma couverture de test par les tests JUnit, la couverture du code exécuté par mes tests d’intégration.
La structure de mon projet ressemble en gros à ceci :
mon-projet
+- pom.xml (ce pom-là n'est qu'un pom d'aggrégation)
+- parent
| +- pom.xml (c'est le vrai parent de tous les autres modules)
+- module-1
| +- pom.xml
+- module-2
| +- pom.xml
+- ...
+- module-tests
+- pom.xml
+- src/test/java/
+- foo/bar/IntegrationTestsRunner.java
module-tests est un module ne contenant que quelques classes de tests, en particulier IntegrationTestsRunner.java dont le but est d’aller lire mes fichiers de tests, et les exécuter.
Afin de ne pas lancer ces tests d’intégration à chaque build, nous définissons un profil Maven dans le pom.xml de module-tests, comme suit :
<profiles>
<profile>
<id>run-it</id>
<activation>
<property>
<name>runIT</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/IntegrationTestRunner.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
Ce profil sera activé soit en ajoutant -DrunIT=true soit -Prun-it dans la ligne de commande Maven.
Avec une méthode « traditionnelle », c’est-à-dire l’exécution de tests unitaires via Surefire, aucune couverture de code ne sera détectée, car le code exécuté par les tests d’intégration l’est sur les autres modules du projet. Or Cobertura ne va instrumentaliser que les classes du module courant et non les dépendances (i.e. module-1, module-2, etc.).
Pour résoudre mon problème, je vais faire appel à JaCoCo, un outil de couverture de test plus performant que Cobertura. Son principal avantage est de pouvoir instrumentaliser les classes au runtime. Dans mon cas présent, JaCoCo va donc pouvoir vérifier la couverture du code sur les différents modules au moment où l’on exécute module-tests.
Avant toute chose, il va nous falloir configurer un peu notre environnement :
Maintenant que ceci est en place, nous allons devoir indiquer à Surefire d’utiliser un agent Java spécifique (l’agent JaCoCo).
Etant donné que nous n’avons besoin de cet agent qu’au moment de l’exécution des tests d’intégration, nous le définirons que pour le profil run-it.
Au sein du pom.xml parent, j’ai donc ajouté :
<profiles>
<!-- Profile used to run Integration Tests. In such case, we add the Java agent for JaCoCo... -->
<profile>
<id>run-it</id>
<activation>
<property>
<name>runIT</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:${jacoco.agent.path}=destfile=${jacoco.file.path}</argLine>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
J’ai défini mon profil deux fois, dans le pom.xml parent et dans celui de module-tests. Nous aurions pu tout mettre dans le parent.
Nous allons avoir besoin d’exécuter maintenant deux commandes Maven :
La première ligne va activer le profil run-it, ce qui aura pour conséquence de lancer le tests d’intégration.
On y définit les deux paramètres (jacoco.agent.path et jacoco.file.path) qui seront passés à Surefire pour définir un agent Java.
La seconde ligne va simplement exécuter l’analyse Sonar sur mon projet, mais en définissant le paramètre sonar.jacoco.itReportPath (généré lors de la première commande), cette analyse comprendra également la couverture des tests d’intégration.
Une fois les deux commandes exécutées, il suffit de se rendre sur son serveur Sonar pour apprécier cette nouvelle mesure de couverture de code !
Comme le montre les captures d’écran sur le post du blog de Sonar à ce sujet, il est possible, à l’instar de la couverture de code, de visualiser la couverture de code de tests d’intégration directement dans la fenêtre de visualisation du code source d’une classe.

For some reasons, I wanted to categorize my JUnit tests, in order to run only a subset of them.
But this is not as simple as it seems…
In one of my projects, I have many unit tests. It appears that many of them are not unit at all.
Some of them require a real Oracle connection, some others need to load a complex Spring context…
My goal is to split these tests into several categories.
To ease the explanations, I will only consider the two following categories: @FastTest and @SlowTest.
JUnit introduced recently, in version 4.8, the @Category annotation (see the changelog and also an example here).
The principle is to annotate a class (or a method) with @Category, and then to define the category on which it belongs. For my needs, I could write:
// Interfaces that define my test categories
public interface FastTest { }
public interface SlowTest { }
@Category(SlowTest.class)
public class ASlowTest {
@Test
public void foo() { ... }
}
@Category(FastTest.class)
public class ARealUnitTest {
@Test
public void foo() { ... }
}
This seems pretty good. But if we go into the details, in order to run all the tests that belong to a specific category, I have to write:
@RunWith(Categories.class)
@IncludeCategory(FastTest.class)
@SuiteClasses( { ARealUnitTest.class, AnotherRealUnitTest.class })
public class FastTestsSuite { }
Here comes the problem: I have to create a Suite (using the annotation @SuiteClasses) to define the classes that should be executed.
That means I have to maintain a list of all the tests for a given Suite. The omissions are therefore likely to be frequent…
How to solve this problem? Here are some ideas:
Suite manually as I just explained before, but I may forget to maintain them…Suite. I wrote such a plugin some years ago, and I can reuse it now. However, I need to define when this plugin should be executed. Indeed, as the purpose of this plugin is to create or modify Java classes, I can’t run it automatically or anytime. Moreover, it is possible that some classes should not be included in these suites. So this solution will not be considered here.**FastTest.java, while the other ones will be named **SlowTest.java. Then, I will configure Maven, and some profiles, to use the correct pattern regarding which kind of tests category I want to run. I will not consider this solution neither, as this may also lead to errors.groups that would be perfect for our needs, especially since Surefire (the test plugin for Maven) natively supports them. This is maybe the best solution for our current concern, but I want to keep JUnit, so, again, I will ignore this idea.@Category without having to define a Suite. However, this is not a good idea to modify these libraries…So none of these ideas seems to be helpful…
The solution I will explain now is based on JUnit Suite. However, these suites will define dynamically the test classes that should be run.
This Suite will look into the classpath for all the classes that are annotated with a particular annotation.
During my searches, I found a project called classpathSuite. One of the purpose of this project is to build a Suite by looking into the ClassPath for some specific classes. Exactly what I want to do!
Using this library, I write the following classes:
@RunWith(Categories.class)
@Categories.IncludeCategory(FastTest.class)
@Suite.SuiteClasses( { AllTests.class })
public class FastTestsSuite {
}
@RunWith(ClasspathSuite.class)
public class AllTests {
}
and classpathSuite will find all the classes in the ClassPath that belong to the category FastTest.
Nice! But this seems to work only when I run my tests in Eclipse, not in a Maven context
Or did I miss something?
So I have to re-implement this feature, but that will work with Maven!
First step: I create two simple annotations, @FastTest and @SlowTest.
They will be used to annotate my JUnit test classes. Note that my solution will only allow you to categorize an entire class, not a method. These annotations will be a replacement for @Category(FastTest.class) and @Category(SlowTest.class).
Now, I create a utility class that will look into the classpath and find the list of classes that are annotated with a specific annotation (@FastTest or @SlowTest in my case) and that belong to a given package (this condition is not technically mandatory, it is just to limit the searches).
On the Java point of view, I get:
public final class ClasspathClassesFinder {
/**
* Get the list of classes of a given package name, and that are annotated by a given annotation.
*
* @param packageName The package name of the classes.
* @param testAnnotation The annotation the class should be annotated with.
* @return The List of classes that matches the requirements.
*/
public static Class<?>[] getSuiteClasses(String packageName, Class<? extends Annotation> testAnnotation) {
try {
return getClasses(packageName, testAnnotation);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* Get the list of classes of a given package name, and that are annotated by a given annotation.
*
* @param packageName The package name of the classes.
* @param annotation The annotation the class should be annotated with.
* @return The List of classes that matches the requirements.
* @throws ClassNotFoundException If something goes wrong...
* @throws IOException If something goes wrong...
*/
private static Class<?>[] getClasses(String packageName, Class<? extends Annotation> annotation) throws ClassNotFoundException, IOException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String path = packageName.replace('.', '/');
// Get classpath
Enumeration<URL> resources = classLoader.getResources(path);
List<File> dirs = new ArrayList<File>();
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
dirs.add(new File(resource.getFile()));
}
// For each classpath, get the classes.
ArrayList<Class<?>> classes = new ArrayList<Class<?>>();
for (File directory : dirs) {
classes.addAll(findClasses(directory, packageName, annotation));
}
return classes.toArray(new Class[classes.size()]);
}
/**
* Find classes, in a given directory (recursively), for a given package name, that are annotated by a given annotation.
*
* @param directory The directory where to look for.
* @param packageName The package name of the classes.
* @param annotation The annotation the class should be annotated with.
* @return The List of classes that matches the requirements.
* @throws ClassNotFoundException If something goes wrong...
*/
private static List<Class<?>> findClasses(File directory, String packageName, Class<? extends Annotation> annotation)
throws ClassNotFoundException {
List<Class<?>> classes = new ArrayList<Class<?>>();
if (!directory.exists()) {
return classes;
}
File[] files = directory.listFiles();
for (File file : files) {
if (file.isDirectory()) {
classes.addAll(findClasses(file, packageName + "." + file.getName(), annotation));
} else if (file.getName().endsWith(".class")) {
// We remove the .class at the end of the filename to get the class name...
Class<?> clazz = Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6));
// Does the file is annotated with the given annotation?
if (clazz.getAnnotation(annotation) != null) {
classes.add(clazz);
}
}
}
return classes;
}
NB: I used the example given in this post to write this code.
Thus, the code ClasspathClassesFinder.getSuiteClasses("my.company", FastTest.class) will return an array of classes that belong to the package my.company and annotated with @FastTest.
The next step consists in building a Suite that will use my utility class to find all the fast tests:
public class FastTestsSuite extends org.junit.runners.Suite {
public FastTestsSuite(Class<?> clazz, RunnerBuilder builder) throws InitializationError {
this(builder, clazz, ClasspathClassesFinder.getSuiteClasses("my.company", FastTest.class));
}
public FastTestsSuite(RunnerBuilder builder, Class<?> clazz, Class<?>[] suiteClasses) throws InitializationError {
super(builder, clazz, suiteClasses);
}
}
Last class to write: the entry point for the Maven Surefire plugin (this class will only run @FastTest, so I will have to write the same class for @SlowTest):
@RunWith(FastTestsSuite.class)
public class RunAllFastTests {
}
Now, I just need to configure Maven in order to run « only » this test:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Run only tests annotated with @FastTest. -->
<includes>
<include>**/RunAllFastTests.java</include>
</includes>
...
</configuration>
</plugin>
Please note that if you are working on a project with multi-modules (this is my case), you will have to define this RunAllFastTests.java in each module.
Regarding the slow tests, I just have to define a Maven profile that will launch the RunAllSlowTests class, which calls the Suite SlowTestsSuite. This Suite will use the utility class to find all classes annotated with @SlowTest. My continuous integration server Hudson Jenkins will activate this profile during the nightly-builds.
Finally, this solution is not really complex, and when the mechanism is developed, you just have to annotate your JUnit test class with the adequate annotation. This is simple.
However, I am quite disappointed that JUnit, a widely used test framework, does not provide such a mechanism natively. I hope that it will change in the near future…
Feel free to post your comments if you think that my solution is not the best one to solve this kind of problem.
Jean-Philippe Briend suggested me on Twitter to provide the sources of the classes. Here they are:
This project requires Maven 2 or 3, Java 1.6 and JUnit 4.8.1.
In addition to all the classes required for the mechanism, this project contains three basic unit tests:
OneFastTest, annotated with @FastTest ;OneSlowTest, annotated with @SlowTest ;TestWithoutAnnotation, without any annotation. As this test should not be executed, it contains a method that will call the JUnit fail() method. Thus, if this class is executed, the test will fail.If you run the command mvn clean install, only the class annotated with @FastTest will be executed.
If you run the command mvn clean install -Djenkins=true (or -Pjenkins), both annotated classes will be executed.
Maven a une actualité assez chargée ces derniers temps, avec la sortie toute proche de la troisième génération de l’outil, mais également avec la publication d’un ouvrage sur Apache Maven, écrit par Nicolas de Loof et Arnaud Héritier.
J’ai écrit courant décembre deux articles sur le site developpez.com concernant ces actualités :
Bonne lecture, et n’hésitez pas à réagir à ces articles sur le site de developpez.com ou en commentaires ici !
Start Slide Show with PicLens Lite