Astuces pour faire des tests unitaires en Java

Bug

Image by Ecyrd via Flickr

Les tests unitaires ont pour but de tester le fonctionnement de chaque module, objet indépendamment de l’environnement qui dans lequel il sera utilisé par la suite. L’objectif est de garantir que le contrat de de module ou de la classe est bien respecté.

Le développeur inexpérimenté aura tendance à coder tous les modules avant des les assembler puis de constater que cela fonctionne ou pas. Ce test dit d’intégration ne permet pas de trouver rapidement l’origine du problème.

Pour autant est-ce que le test d’intégration doit être négligé ? En effet une bonne pratique est l’analyse descendante illustrant le vieille adage « diviser pour mieux régner ». En partant du haut, les premières méthodes utilisent alors des modules ou classes non encore écrites. Doit-on tester ces grosses briques ? Si oui comment ?

Le test doit être un acquis dogmatique. Un code non testé est un code sans valeur. La réticence première à tester est souvent justifiée par l’impossibilité à ce stade du développement d’avoir l’écosystème pour tester. Par exemple le développeur va dire « je n’ai pas encore tel ou tel module qui doit me fournir des données » ou bien « je n’ai pas encore la couche de persistance ». Il faut être clair. On test ce qui est utile et ce qui est utile c’est l’implémentation des fonctionnalités, des spécifications. Si on ne peut pas tester un code terminé c’est signe d’une mauvais;e conception: on mélange dans une même méthode ou classe des responsabilités différentes. Un code bien écrit est un code testable !

voyons comment écrire du code testable à travers des exemples.

Utilisation de classe non encore implémentée

Supposons que l’on ait une classe A qui fait des choses intéressantes mais qui doit les algorithmes sont paramétrés. Bien sûr la mécanique de paramétrage avec fichier de configuration ou configuration en base n’existe pas encore..
Si on code en due la configuration dans A, il va falloir revenir sur A pour y mettre le code définitif d’accès à la configuration. Or il y a une règle qui dit un code bien conçu doit marcher avec du code non encore écrit. Pour y parvenir on utilise le vielle adage diviser pour mieux régner qui sera incarné par une interface. La classe A sera paramétrée par une interface IAConfig passée au constructeur pour faire simple. Dès lors le test unitaire ne pose plus de problème puisqu’il va porter la configuration en dur et le code de A fonctionnera dans son utilisation réelle au sein de l’application.

Signature de méthode incompatible

Ce cas peut s’illustrer par une méthode m qui prend en argument un type la rendant difficile à tester. Par exemple disons une méthode qui prend un java.io.File en argument. Le développeur pourra se plaindre qu’il n’est pas aisé de faire un test unitaire fonctionnant sur n’importe quelle machine. Il a raison. Référencer un File dans un test JUnit sous Maven demande quelques lignes de code. Il faut filtrer un fichier de propriété contenant project.basedir=${project.basedir} qui sera placé en resource de test. Puis dans le test unitaire charger le fichier de propriétes avec un getResourceAsStream() et enfin construire le chemin vers le fichier. Pas direct mais faisable.

D’ailleurs pourquoi faire compliqué quand on peut faire simple? Un code bien écrit est un code testable. A-t-on vraiment besoin d’un File en paramètre? Utilise-t-on des propriétés ou des méthodes spécifiques au File? Sans doute non, donc un InputStream devrait être suffisant. L’idée est de changer la signature de la méthode pour la rendre le moins spécifique possible, ainsi elle sera plus facilement testable. Dans son utilisation réelle l’appelant peut manipuler un File s’il le souhaite et passer un FileInpuStream à la méthode dûment testée.

Cas de la base de données avec des DAO

Une dernière raison invoquée pour refouler les tests est la présence d’une base de données. Le premier point est d’éviter de mettre de la logique dans le traitement SQL. Si ce n’est pas passible il faut investir dans du DBUnit parce que cela signifie que la valeur ajoutée, l’implémentation des spécifications est dans le code SQL… Vraiment? La plupart du temps le code métier dans le SQL est un signe de médiocrité.

Le deuxième point est que l’absence de code métier dans la couche de persistance fait qu’il n’est pas vraiment besoin de la tester. Par exemple si on utilise un pattern DAO, on ne va pas tester les DAO si ils ne font que CRUD.

Le 3e point est que le code à tester se trouve au dessus de la couche de persistance donc testable en bouchonnant les DAO : on ne se mock pas.

L’utilisation de DAO permet de substituer facilement les implémentation SQL avec des implémentations de test. En effet le bon pattern DAO ne signifie pas seulement isoler le code SQL dans des classdes DAO mais aussi rendre les DAO interchangeable. On obtient cela on utilisant une fabrique abstraite de DAO que j’appelle DAOFactoryManager, classe intermédiaire entre le code métier et les DAO. Le DAOFactoryManager est configurable avec le type d’implémentation. Dans son utilisation réelle il renvoit par exemple une fabrique de DAO pour MySQL et en test une fabrique de mock. Le code métier doit y voir que du feux en ne manipulant qu’une interface IDAOFactory obtenue auprès du DAOFactoryManager et des interface pour chaque DAO obtenus depuis le IDAOFactory.
Cela semble lourd mais les bienfaits valent le coût. En effet le code métier est testable sans implémentation de la couche de persistance. Le developpeur peut à loisir paramétrer un MockDAOFactory dans la phase de préparation des acteurs du test, puis appeler la méthode métier qui va lors de son execution appeler le DAOFactorManager configurer en mode test, et finalement utiliser des DAO « mockés ».

On prendra juste soin de ne pas liée le DAOFactoryManager avec les objets mocké. En effet le manager de DAO est une classe qui ira en production, alors que les mock qui sont des classes de tests ne le devraient pas. En java il suffit d’utiliser la réflexion pour charger la classe dynamiquement avec un :

Class.forName("com.acme,app.dao.MockDAOFactory").newInstance()

Lors de la phase de test la classe sera disponible mais pas en production.

Tests d’intégrations

Nous entendons pas test d’intégration les tests des méthodes de façades. Ce sont celles qui orchestre les appels aux objets métiers pour accomplir les services attendus de l’application. Oublions un instant toute la pollution intellectuelle apportée par les EJB. Dans un contexte uniquement objet, on doit avoir des façades qui constituent les points d’entrées sur les fonctions du système: ce sont les implémentations informatiques des cas d’utilisations. Par exemple une application qui va tous les soirs lire une base de données puis envoyer des mails de rappel  à tous les utilisateurs portera une méthode dédié à ce cas d’utilisation dans sa façade. Cette façade pourra être appelé en ligne de commande ou par une IHM Swig ou web. Le métier ne changeant pas, le fait de garantir le fonctionnement de la façade est aussi important que de garantir le fonctionnement unitaire. ll faut tester la façade et ce n’est pas faire une entorse à la définition de test unitaire. Quand on teste une méthode qui utilise la classe ArrayList, on suppose que celle-ci n’est pas boguée et on ne la  » mock » pas. De même une fois que les briques de bases sont développées et testés unitairement, on doit les considérer comme des instructions de base et les utiliser dans la façade que l’on va tester « unitairement ».

Ce niveau de test d’intégration est pour moi pas moins que nécessaire. Alors gardons à l’esprit qu’un code non testé est un code sans valeur. Tester doit être une seconde nature qui oblige à vérifier que le code fonctionne et en les automatisant ils deviennent répétables sans effort permettant ainsi de détecter les régressions. Alors testons, testons, nos utilisateurs nous le rendront !

Cet article, publié dans Langage, est tagué , , , . Ajoutez ce permalien à vos favoris.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s