Lors du Paris JUG du 11 janvier 2011 David Gageot nous a présenté sa vision des tests dans un projet. Ils doivent prendre moins de 5 minutes tout en s’exécutant sur le poste du développeur. Plus on écrit de code, plus il y à de tests, plus il sont longs à s’exécuter. Il faut trouver des moyens de garder le temps de build sous les 5 minutes. Cela permet d’obtenir un retour rapide, plus de réactivité et de productivité. David nous présente le programme de la soirée:
- comment rendre sa suite de tests plus rapide
- les outils pour gérer ses tests et un focus sur JUnit
Make that test suite run faster
On n’envisage pas de faire tourner le build sur une ferme de serveurs en rajoutant des slaves hudson par exemple. Les solutions recherchées se doivent d’être simples. Cela va nous amener la rapidité du feedback et la simplification du produit (principe KISS).
On distingue 3 catégories de populations : les tricheurs, les fainéants et les courageux.
Stratégie du tricheur
- Avec un CPU plus rapide (problème, souvent un seul cœur est utilisé ce qui ne rend pas forcément le build plus rapide sur un processeur plus récent)
- En utilisant le parallel build : mvn -T4 clean install
- En effectuant plusieurs tests en parallèle : avec Surefire on peut préciser un nombre de threads qui vont faire tourner les tests (attention aux problèmes de concurrence)
Stratégie du fainéant
-
En supprimant les tests redondants ou non nécessaires et souvent il y en a beaucoup
-
En supprimant le code mort (le code qui ne sert pas est de tout de manière historisé dans le gestionnaire de source, on pourra le récupérer au besoin). Ces suppressions conduisent à un cercle vertueux : je supprime du code, mes tests ne compilent plus, je nettoie les tests.
-
En travaillant dans une sandbox afin de limiter les accès au réseau et au système de fichiers pour cela il faut
-
- Travailler avec une base de données en mémoire de type H2 à la place de la base Oracle spécialement présente pour les tests. Cela peut ne pas être possible si l’on fait appel à des procédures stockées
-
Avant de choisir la base de données utilisée par le projet il est intéressant de regarder s’il elle possède un mode permettant des tests rapides. Dans le cas des bases de données NoSQL par exemple, Voldemort (la base de données de LinkedIn) répond bien à ce critère.
-
Remplacer le serveur SMTP d’entreprise par un serveur en mémoire (Subetha SMTP par exemple)
-
Eviter les accès aux fichiers en utilisant Apache VFS ou des ressources Spring
Ainsi le build va gagner en rapidité et en stabilité.
Stratégie du courageux
- En retirant les tests de règles métier des tests d’intégration. Pas besoin de traverser toutes les couches de l’application pour tester ces règles. On teste les règles métier dans des tests unitaires. (ex: test d’un composant de pagination à travers selenium que l’on a ensuite passé en test unitaire)
- En s’attaquant au test qui prend le plus de temps afin de le rendre plus rapide. On peut imaginer de séparer un test d’intégration en test d’intégration moins lourd assorti de tests unitaires. Il faut veiller à ne pas mettre trop de choses dans les tests d’intégration.
- En mockant les couches les plus lentes (ex Spring+Mockito dans des tests d’intégration)
- En ne testant pas à travers le navigateur. David déconseille d’utiliser selenium car les tests sont lents par essence. Si on ajoute régulièrement des tests selenium, le build va prendre de plus en plus de temps et ne pourra plus être lancé que de manière journalière.
- En simplifiant. La complexité a un coût que l’on paie à chaque fois que l’on lance les tests. C’est pour cela qu’il faut constamment simplifier et optimiser son code.
My own preferred testing tools
Dans une deuxième partie, David nous a présenté des outils liés à JUnit pour améliorer la création et la gestion des tests ainsi que les nouveautés de JUnit 4.
moreUnit : Il s’agit d’un plugin eclipse. C’est un outil qui ne paye pas de mine mais qui facilite la création et la gestion des Tests.Une petite icône verte indique qu’une classe dispose d’un test. On peut naviguer facilement de la classe au test et inversement. On peut également lancer le test depuis la classe testée. Un commiter de moreUnit étant dans la salle on apprend qu’une version future permettra de gérer la création de Mocks lors de la création d’un test. Utilisant cet outil je peux confirmer qu’il est très efficace.
Infinitest : après l’intégration continue, le test continu (“continuous testing”). C’est un plugin eclipse (il existe également un plugin IntelliJ) qui permet à chaque modification de code de relancer les tests impactés et uniquement ceux-ci. Ainsi on a toujours un indicateur rouge ou vert mis à jour presque en temps réel. Il permet d’ajouter des indications d’erreur directement dans l’éditeur de code d’eclipse. Les fichiers infinitest.filters et infinitest.args permettent de le paramétrer finement.
JUnitMax : il fait plus ou moins la même chose qu’infinitest, cependant c’est un outil payant. Il a été développé par Kent Beck. Il lance en premier les tests qui échouent le plus souvent ainsi que les tests les plus courts afin d’offrir un retour le plus rapide possible. David ne l’utilise pas.
Hamcrest : une librairie de matchers pour JUnit qui permet une plus grande richesse d’assertions pour une meilleure lisibilité. David ne l’utilise pas.
Fest-Assert : semble une meilleure alternative qui dispose d’une Fluent API permettant d’écrire des assertions très lisibles comme ci desous.
assertThat(yoda).isInstanceOf(Jedi.class) .isEqualTo(foundJedi) .isNotEqualTo(foundSith);
Les “nouveautés” JUnit
@Rule permet d’externaliser les règles d’initialisation(@Before) et de fin de test (@After) dans une classe indépendante du test. Une @Rule implémente MethodRule et étend souvent ExternalResource.
différents exemples de Rules que l’on peut développer
- TemporaryFolder (présente de base dans JUnit 4.8.2) qui permet de créer un répertoire temporaire avant le test et le supprime après le test.
- ExpectedException qui permet de verifier qu’une exception survient et que son message est bien celui attendu
@Rule public ExpectedException expectedException = new ExpectedException(); public void withRule() { expectedException.expect(IllegalStateException.class) expectedException.expectMessage("failed because of reason") TestedCode.doSomething(); }
- RunTestMultipleTime permettant … de lancer le test plusieurs fois
- ConsoleRecorder permettant de rediriger System.out et System.err dans une chaîne de caractère afin de vérifier cette valeur dans le test.
@ClassRule devrait apparaitre en version 4.9 car pour le moment on ne peut pas externaliser une Rule qui corresponde à un @BeforClass et un @AfterClass
@Theory et @DataPoint(s) : Il s’agit ici de vérifier qu’un algorithme fonctionne. Pour toutes les combinaisons possibles de DataPoints sauf celles ne correspondant pas à l’assertion assumeThat comme dans l’exemple ci-dessous.
@DataPoints int[] = {1, 3, 8, 13, 7, 6} @Theory public void addIsCommutative(int value1, int value2) { assumeThat(value2, not(0)) int res1 = TestedClass.add(value1, value2); int res2 = TestedClass.add(value2, value1); assertEquals("addition should be commutative",res1, res2); }
@Category devrait permettre de categoriser ses tests (SlowTest, FastTest par exemple), ainsi on peut choisir de n’executer qu’une catégorie. Cependant cela nécessite de ne pas oublier de catégoriser tous les tests. Comme cela semble fastidieux, David ne l’utilise pas.
Pour finir David nous invite à aller nous inspirer des collections de @Rule JUnit existant sur internet.