Tests unitaires

Automatiser les tests Spring avec JUnit

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

Qui ne teste rien, n'a rien - c'est comme ça qu'on pourrait paraphraser le fameux proverbe "qui ne tente rien, n'a rien" dans le monde du développement. Les tests sont obligatoires pour s'assurer du bon fonctionnement de l'application suite à des changements effectuées. Parfois ils font partie importante de certaines méthodes de développement (par exemple Test Driven Development).

Notre application d'exemple contient également une série des tests. Leur but est plutôt de vous présenter certaines fonctionnalités qui peuvent être implémentées pour s'assurer du bon fonctionnement du système. C'est la raison pour laquelle toutes ses parties ne sont pas couvertes des tests.

Test unitaires en théorie

On a abordé ce concept théorique très brièvement dans l'article consacré à des test sous Symfony2. Ici on va donc rappeler les fondamentaux et les élargir dans le contexte d'une application web Java.

Dans les tests unitaires il s'agit d'isoler chaque composant de l'application dans le but de vérifier son fonctionnement. Dans Spring ils sont très utiles dans la vérification du comportement des méthodes des services. Ils peuvent être également employés dans la validation des contrôleurs.

Qu'est-ce que c'est JUnit ?

En PHP, l'outil de tests incontournable est PHPUnit. Son homologue en Java s'appelle JUnit. Il s'agit d'un framework de tests unitaires pour les applications écrites en Java. Grâce à des annotations, on peut facilement manipuler le scénario d'exécution du test. Plusieurs méthodes (assertions) permettent de vérifier le comportement du système selon les résultats très variés.

Concernant les annotations, leurs noms sont très explicites :
- @Before : code est exécuté avant chaque test.
- @BeforeClass : code de la méthode annotée avec @BeforeClass est exécuté avant le premier test et avant la méthode @Before
- @After : code est exécuté après chaque test
- @AfterClass : code est invoquée après l'exécution de tous les tests

En ce qui concerne les méthodes utilisées pour la vérification des résultats, elles font partie de la classe org.junit.Assert. Les noms de la plupart d'entre elles commencent par assert. Grâce à elles on peut donc vérifier si les deux instances sont identiques, si une expression est vraie ou nulle. La seule fonction qui ne respecte pas cette règle de nommage est fail qui fait échouer le test.

Configurer JUnit dans Spring

L'implémentation des tests pour notre application se base sur les deux étapes. La première est la configuration du fichier build.xml qui est utilisé par ANT dans la préparation des JARs. En occurrence, on utilisera la commande ant test pour lancer des tests. Voici le fragment du fichier de configuration :

afficher le code

Dans la partie <unit /> on précise si l'on veut voir les résultats affichés dans la console (printsummary). Plus loin on définit les classpaths et les tests à lancer. Chaque test possède le nom de la classe (name), le repértoire de sauvegarde du résultat (todir) et le nom du fichier avec le résultat (outfile).

La seconde étape consiste à écrire les tests définis par l'attribut name. On commencera par le contrôleur abstrait qui va charger toute la configuration nécessaire dans l'annotation @ContextConfiguration et déterminer quelle classe doit exécuter les tests (@RunWith) :

@ContextConfiguration(locations={"file:///D:/resin-4.0.32/webapps/ROOT/META-INF/spring/test-config.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class AbstractControllerTest extends AbstractTransactionalJUnit4SpringContextTests {

}

On voit que le @ContextConfiguration charge un nouveau fichier de contexte :

afficher le code

Un point important se cache sous la balise <import />. Comme son nom indique, elle charge les fichiers de contexte déjà définis. Juste dans une seule situation, mailerTool, on surcharge le bean y précisé. La raison pour laquelle on le fait se cache dans le bean servletContext représenté par la classe org.springframework.mock.web.MockServletContext. Nos tests ne sont pas lancés via l'interface web, mais un outil de commande en ligne (il n'y a donc pas de contexte de l'utilisateur - les requêtes, les sessions...). On doit donc fournir un substitut à des requêtes et réponses générées correctement par les servlets. Il s'agit des objets qu'on appelle mock objects.

Quelle définition on pourrait associer à ces objets "mock" ? Dans la programmation orientée objet il s'agit des objets de substitution qui imitent le comportement de réels objets selon des méthodes strictement définies. Ces objets sont le plus souvent utilisés dans le cas des tests où la récupération de réels objets se révèle impossible. Dans le cas de nos tests, on emploiera les objets mocks pour remplacer l'instance HttpServletRequest. Le mock utilisé sera MockServletContext.

Ecrire un JUnit test sous Spring

Dans notre application on a créé les tests pour 4 scénarios. Cependant ici on n'évoquera qu'un seul. Il regroupe toutes les difficultés rencontrées dans d'autres scénarios. Le contrôleur testé concerne l'enregistrement de l'utilisateur :

afficher le code

Dans notre commentaire on ne va pas se focaliser sur toutes les assertions de cette classe. Par contre, on abordera les aspects qui posent souvent les soucis lors de l'écriture et de l'exécution des tests unitaires et fonctionnels sous Spring.

Tout d'abord, comment simuler l'activité d'un utilisateur connecté dans l'application ? Cela se fait via SecurityContextHolder.getContext().setAuthentication(). Quand cette ligne est absente et on tente d'accéder à des méthodes qui nécessitent l'instance de l'utilisateur connecté, on reçoit l'exception suivante : An Authentication object was not found in the SecurityContext.

Un autre problème rencontré est lié à la validation. L'exception lancée était HV000041: Call to TraversableResolver.isReachable() threw an exception. javax.validation.ValidationException: HV000041: Call to TraversableResolver.isReachable() (...) Caused by: java.lang.NullPointerException at javax.persistence.Persistence$1.isLoaded(Persistence.java:93). C'est un bug qu'il faut contourner par la surcharge du traversable resolver. La nouvelle classe implémentant javax.validation.TraversableResolver doit être passée au contexte de validation de cette manière : validatorContext.traversableResolver(traversableResolver);. Le traversable resolver surchargé se présente ainsi :

public class JPATraversableResolver implements TraversableResolver {

    @Override
    public boolean isReachable(Object traversableObject, Path.Node traversableProperty, Class<?> rootBeanType, 
        Path pathToTraversableObject, ElementType elementType) {
        return traversableObject == null || Hibernate.isInitialized(traversableObject);
    }

    @Override
    public boolean isCascadable(Object traversableObject, Path.Node traversableProperty, Class<> rootBeanType, 
        Path pathToTraversableObject, ElementType elementType) {
        return true;
    }
}
Bartosz KONIECZNY Tests

Une question ? Une remarque ?

*

*

Un conseil PHP

Utiliser XPath pour calculer le nombre d'apparitions d'un élément.

XPath est un langage qui sert à se déplacer au sein d'un document XML. Il contient beaucoup de fonctionnaltiés utilies lors de l'analyse de ces documents. Par exemple, pour voir le nombre d'apparition d'un tag, il suffit d'utiliser la méthode count et placer comme son paramètre le chemin vers les éléments à calculer. Imaginons qu'on veut calculer le nombre de noeds d'erreur. En PHP cela va se présenter ainsi :

// $this->xpath is the instance of DOMXPath
echo (int)$this->xpath->evaluate("count(/error)");