Quels sont les types de tests que l’on utilise sur un projet agile ?
Constat
Typiquement lorsqu’une équipe de développement commence à appliquer les différentes pratiques issues de méthodes agiles comme eXtreme Programming, la question des tests finit par venir. Lorsque l’équipe a compris la nécessité d’écrire des tests, elle risque de se heurter très rapidement à quelques obstacles. Un de ceux là concerne notamment les types de tests.
C’est ainsi que l’on se retrouve généralement avec un jeu de tests JUnit qui vérifient par exemple les résultats des appels HTTP vers des Web Services REST déployés dans des serveurs applicatifs avec leurs bases de données et systèmes de fichiers. Quels sont les obstacles ? Des tests longs à exécuter car ils doivent s’intégrer avec certains environnements et complexes à développer puisqu’ils prennent en compte ces environnements bien particuliers.
Dans ce cas, il convient alors de séparer les tests unitaires des tests d’intégration.
Définition
Michaël Feathers nous rappelle la définition du test unitaire dans son livre "Working Effectively With Legacy Code"
Un test est unitaire lorsque :
• Il ne communique pas avec la base de données
• Il ne communique pas avec d’autres ressources sur le réseau
• Il ne manipule pas un ou plusieurs fichiers
• Il peut s’exécuter en même temps que les autres tests unitaires
• On ne doit pas faire quelque choses de spécial, comme éditer un fichier de configuration, pour l’exécuter
Et généralement un test unitaire est petit et rapide, il vérifie le traitement d’une méthode de classe et des interactions avec d’autres méthodes de classe. Ainsi tout ce qui n’est pas un test unitaire constitue alors un test d’intégration.
Attention, il ne s’agit pas ici d’opposer ces deux types de tests, qui sont nécessaires pour un projet. Mais comme on utilise généralement JUnit dans les deux cas, il convient parfois de séparer ceux qui sont longs à exécuter de ceux qui ne le sont pas, tout en gardant à l'esprit que c’est l’utilisation des deux va nous dire si notre système fonctionne.
Maintenant, il se peut que l’on ait à développer une classe dont la responsabilité est d’accéder à la base de données. On dispose alors de plusieurs choix pour tester cette classe :
• Ecrire un test unitaire en utilisant des objets bouchons (Mock Objects) sur la couche de connexion à la base de donnée
• Ecrire un test unitaire en bouchonnant la base de données réelle par une base de données mémoire (par exemple HSQLDB)
• Ecrire un test d’intégration avec le code et la base de données
Mais il se peut que l’on ne puisse tester unitairement cette classe puisqu’elle peut faire appel à des procédures stockées par exemples. Dans ce cas on reste avec un test d’intégration de la classe d’accès à la base de données et on se met alors à tester unitairement la procédure stockée (avec des frameworks de test comme UTPLSQL).
Round Trip
En effectuant cette classification, on observe alors que pour un test unitaire, on va idéalement traverser une seule classe (celle qui est testée) lors de son exécution. Pour un test d’intégration on aura plusieurs classes et briques de l’architecture à traverser. On parle alors de round-trip. Les tests Round Trip vérifient des résultats de l’interface et traversentc de une à plusieurs couches (méthode d’une classe, IHM d’une application).
Bouchonnage
Si on souhaite tester unitairement une classe qui a une dépendance avec une autre qui accède à la base de données, on va devoir bouchonner cette dépendance. Dans un premier temps on peut se baser sur un framework de bouchonnage (mock object), comme Easymocks. Le snippet de code suivant montre comment on crée dynamiquement un mock, qui remplace le code de la classe MaDependanceDao, puis on injecte ce mock dans le code à tester, et on vérifie que le mock a bien été appelé par le code de la classe à tester :
//Acteurs
MaClasseATester maclasse = new MaClasseATester()
Bean bean = new Bean("bean") ;
MaDependanceDao dao = EasyMock.createMock(MaDependanceDao.class);
dao.add(bean);
EasyMock.replay();
//Actions
maclasse.setDao(dao);
//Assertions
…
EasyMock.verify();
Cette méthode fonctionne généralement très bien, mais il est parfois plus simple d’utiliser directement la surcharge de dépendance pour couper le lien avec la ressource externe. Dans l’exemple suivant, on surcharge directement le code d’accès à la base de données par héritage, et on injecte ce code à la classe que l’on souhaite tester :
MaClasseATester maclasse = new MaClasseATester()
MaDependanceDao dao = new MaDependanceDao() {
@Override
public void add(Bean bean) {
System.out.println("mocked") ;
}
} ;
//Actions
maclasse.setDao(dao);
//Assertions
…
Tests Fonctionnels
Parce que les tests développés avec JUnit sont souvent illisibles pour les clients du projet, on définit alors des tests qui leur sont dédiés. Ces tests sont alors définis avec le client et sont exclusivement tournés sur le métier du client. Un effort régulier sera mis sur l’expressivité de ces tests, pour que tous les acteurs du projets puissent les comprendre, et on s’attachera à les garder simples pour favoriser un développement itératif et incrémental. Sa mise en place doit être facilitée par des données de tests exprimées par le client.
On utilise souvent Fitnesse ou GreenPepper, qui permettent de formaliser ces tests par le biais d’une page Wiki, ce qui est beaucoup plus lisible pour un client. On s’en sert aussi comme outil d’aide à la communication entre client et équipe de développement.
Mais ces tests, même s’ils communiquent avec le système global (Base de données, fichiers, ressources réseaux) ne doivent pas être utilisés pour des tests d’intégration. On formalise là du métier qui définit une fonctionnalité du point de vue du client. Ce test permet de valider cette fonctionnalité par le biais de tout un tas d’exemples (cas nominaux, cas aux limites) pour lever les ambiguïtés de la formalisation du besoin.
Dans le cas de tests fonctionnels, on va porter toute son attention pour savoir ce qui est important à formaliser sous la forme de test, qui est le client, et quel est son métier.
Pour aller plus loin
Nous avons vu dans cette article trois types de tests : les tests unitaires, d’intégration et de recette fonctionnelle. Mais il reste encore beaucoup d’autres types de tests : performances, IHM, scalabilité, robustesse, sécurité, … Certains sont faciles à automatiser, d’autres moins … Tous ces tests entrent évidemment dans le tableau au cours du projet si l’on souhaite passe du mode "Code & Pray" au mode "Test & Update".