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« .

10 commentaires sur “Quels sont les types de tests que l’on utilise sur un projet agile ?”

  • J'ai 2 questions assez distinctes, autour de l'utilisation de frameworks. * y a-t-il des bonnes pratiques autour de l'utilisation de Spring, qui garantissent que les tests "round trips" (qui peuvent disposer de leur propre application context, pour le bouchonnage, l'accès aux .properties, etc.) ne s'exécutent pas avec un contexte Spring trop différent de celui de l'appli ? Je pense par exemple à l'injection de beans par type, qui empêche de définir des bouchons pour les tests en important la config Spring principale, sous peine d'avoir des conflits à l'injection. A l'inverse, ne pas importer la config Spring principale n'aurait aucun sens pour un tel test puisqu'elle fait partie intégrante du code à tester. * sur EasyMocks maintenant, j'ai constaté que pour l'utiliser il fallait spécifier soi-même, dans le test, les appels bouchonnés (méthodes expect()), sans exception. Il ne s'agit pas tant de tester une classe "boîte noire" que de vérifier qu'elle fait bien les appels qu'on veut. Outre la redondance avec le code testé lui-même, est-il souhaitable de devoir réécrire le test lorsque le code à tester évolue dans son fonctionnement interne ? Je ne parle même pas de TDD qui recommande d'écrire le test avant la classe principale :-) Merci de m'éclairer ! Thomas
  • Bonjour, et merci pour cet article. Mes deux centimes au sujet des tests unitaires : la définition qui en est proposée me laisse un peu mal à l'aise... Il y a 1001 façons d'écrire un test qui respecte les 5 critères listés sans pour autant que ce dernier soit unitaire. Si on considère un vrai système - et non pas l'exemple bateau consistant à tester un ICalculator - écrire un test unitaire pertinent est souvent une tâche dont la complexité est sous-estimée. A mon sens, la seule façon d'y parvenir est de pratiquer massivement, et surtout, d'en comprendre profondément l'intérêt. Les commandements de M. Feathers sont à considérer comme la description de quelques conséquences évidentes de l'isolation. Mais les contre-exemples sont trop nombreux (1001) pour qu'on parle de définition. Suivre quelques guidelines, se baser aveuglément sur les rapports de coverage, ou le nombre de tests est selon moi le meilleur moyen de s'égarer. Il n'y a pas de balle-en-argent, hein. Programmer c'est compliqué, tester c'est compliqué, donc let's go shopping !
  • @Thomas : Dans la seconde partie de ton commentaire, tu es en train de t'interroger sur les mocks, et plus généralement sur l'interaction-based testing. Je disais précédemment que tester est compliqué, en voici une des raisons. Il y a des dizaines de questions de ce genre à se poser, et les concepts manipulés sont loin d'être triviaux. Quelques pointeurs, toujours au sujet de "interaction-based tests versus state-based tests" : Martin Fowler, qui explique bien les différences essentielles qu'il faut faire entre les différents types de test-doubles : martinfowler.com/articles... Un article de Ben Pryor sur toute la question : benpryor.com/blog/2007/01... Un post de Jeremy D. Miller, auteur de StructureMap dans le monde .NET : codebetter.com/blogs/jere...
  • Romain : Je ne considère pas la définition de Michaël Feathers comme un dogme, mais bien comme de l'information que l'on peut en tirer sur l'état des tests d'un projet. J'ai eu l'occasion d'intervenir sur des projets legacy (AKA sans tests), agiles ou pas, ou on me demandait de l'aide sur les tests, et utiliser cette définition m'a aidé à clarifier des choses avec l'équipe de développement, et d'avancer sur la transmission de pratiques de développement autour des tests. Sur la question de la pratique du développement piloté par les tests, elle est évidement essentielle. Pour ma gouverne, j'aimerais avoir un exemple parmi les 1001 façons de respecter la définition de M.Featers, sans que ce soit un test unitaire (et je ne parle pas encore de TDD). Thomas : En terme de bonnes pratiques, j'ai pu voir des confs de tests, identique à celle qui part en prod, mais avec tout l'environnement externe bouchonné (BDD en mémoire, ...). Dans d'autres cas, c'était une configuration dédiée au test, mais évidement éloignée de celle qui part en prod. Et dans d'autre un peu des deux. Et il faudrait que je vois un bout de ton code pour donner un avis plus tranché :) Pour la question des mocks, il y a en effet un tradeoff sur la réécriture du code de dépendance. Si tu réécris beaucoup de code pour préparer tes mocks, afin de tester, c'est aussi une info sur l'état de ton code ...
  • Un petit exemple idiot : http://gist.github.com/60735 Désolé, c'est du C#.
  • Et bien, je ne vois pas en quoi ton code, bien qu'il ne fasse pas grand chose à part créer des objets, serait différent d'un test unitaire.
  • Héhé, oui, pas facile de décider lorsqu'il est question de cheeseburgers, hein. Mais quel composant la méthode "I_can_haz_cheezburger" teste unitairement ? Si le teste échoue, est-ce que le problème vient de Foo ? De Bar ? Ou de FooBar ? Ici le setup est trivial, mais il n'est pas difficile d'imaginer un graphe de dépendances plus complexe, et des types plus complexes. Sans découplage, la granularité du test est compromise, et peut empêcher ce dernier d'être pertinent. D'où la nécessité d'un design permettant les tests unitaires, d'où la nécessité d'utiliser des abstractions afin de pouvoir stubber/mocker les dépendances. L'exemple posté est une illustration de ce problème : le design ne permet pas de profiter pleinement de l'intérêt des tests unitaires, puisque pour tester une instance de Foo, tu as besoin d'une instance de Bar, qui a elle-même besoin d'une instance de FooBar. Il est donc impossible de tester Foo en complète isolation. Tu es obligé de tester Foo avec Bar avec FooBar. On est déjà dans quelque chose qui ressemble à un roundtrip, puisqu'on ne teste en réalité qu'un agglomérat de composants, et non pas chaque composant unitairement. Attention, je ne suis pas un malade mental qui pense que toutes les classes doivent pouvoir être testées en isolation :) Il peut arriver que l'on décide de ne tester que la racine d'un aggrégat lorsque l'encapsulation a du sens. Ici, pour que ça soit plus clair, on peut imaginer que Foo est un controller, que Bar est un service, et que FooBar est un repository. Et dans ce cas, il est évident que pour tester un controller en isolation, je ne veux dépendre d'aucune implémentation concrète de service. De même, si Foo est une vue, Bar un presenter, et FooBar une commande (quel exemple !), on ne peut pas prétendre tester la vue unitairement en gardant un couplage si fort inter-implémentations. Je cherche juste dire que ce qui ressemble à un test unitaire n'est pas forcément un test unitaire. Et encore une fois, le fait d'être détaché d'une base de données (ou d'une autre dépendance évidente assimilable) ne garantit en rien le caractère unitaire d'un test. Certains designs laissent d'autres dépendances moins visibles compromettre l'atomicité des tests et réduire (voire même annuler) leur intérêt.
  • Je lis >> tu as besoin d'une instance de Bar, qui a elle-même besoin d'une instance de >>FooBar. Il est donc impossible de tester Foo en complète isolation J'espère que tu isoles ton code pour mocker les classes String, Integer, etc car tu vas en avoir besoin pour être en totale isolation. Puis >>Attention, je ne suis pas un malade mental qui pense que toutes les classes >>doivent pouvoir être testées en isolation :) Il peut arriver que l'on décide de ne >>tester que la racine d'un aggrégat lorsque l'encapsulation a du sens. Ca dépend, dans le découpage Commande / Service / Repository, tu dois pouvoir tester unitairement, avec des mocks ou pas. Dans le cas du repository, tu auras le choix, comme dans mon article, entre des tests d'intégration ou unitaire. >>Je cherche juste dire que ce qui ressemble à un test unitaire n'est pas forcément >>un test unitaire. Et la pureté n'existe que dans les laboratoire. Je pense que pour développer et tester, on a besoin de quelques règles simples (j'en expose quelques unes dans l'article, il y en a plein d'autres) et beaucoup, beaucoup de pratiques. Et j'ai souvent utilisé la définition de Feathers lors de mission sur des projets agiles, elle a souvent été bien accepté par les développeurs, et permettait de clarifier de nombreuses choses dans des tests qui commencent à devenir compliquer. Bref, je pense que l'on parle de cas particulier, qui nécessite d'être tranchés devant du vrai code, dans un IDE, avec un clavier ...
  • Hey Mathieu, Le ton de ta réponse me laisse penser que tu as pris mes remarques personnellement :) Ca me désole un peu car il ne me semble pas avoir émit un jugement te concernant, ou concernant tes méthodes de travail. Ton collègue Djamel Zouaoui, d'Octo, participera à la réunion ALT.NET parisienne du 18 mars pour animer une présentation sur le TDD. Même s'il s'agit de .NET, je t'invite à venir y faire un tour. Nous pourrions ainsi échanger nos points de vue de vive voix, et il est très probable que je parvienne à mieux me faire comprendre ainsi. Nous sommes apparemment déjà d'accord sur le fait que le testing est une discipline nécessitant une grande pratique. C'est encourageant, non ? A bientôt j'espère.
  • Mathieu, Romain, Je suis moi aussi gêné par la définition de Feathers. Un test qui utiliserait 50 classes de mon application, mais pas la BD, pas de fichiers, pas le réseau et pourrait s'exécuter en même temps qu'un autre, rempli les critères de cette définition. Je comprends son intérêt didactique, et plus particulièrement dans un environnement legacy (code existant sans tests). En effet, sans environnement maitrisé, tester n'est pas possible (unitairement ou pas d'ailleurs) car je ne suis pas sur que mon test aura le même comportement d'une exécution sur l'autre. Cependant, j'ai tendance à considérer que le granularité de mon test doit être fine (i.e. il ne doit tester si possible qu'une méthode, dont les dépendances sont mockées, donc maitrisées) car : 1. Si je veux tester la combinatoire d'un test impliquant 50 classes, il ne me faudra pas moins de test - je ne gagne rien à impliquer 50 classes si ce n'est l'effort de mock (pas négligeable parfois je l'accorde) 2. Je vois les tests comme un outil pour trouver une erreur, et j'ai envie que les lumières rouges m'indiquent le plus précisément possible ou se trouve mon erreur / la régression. 3. Je documenterai plus précisément mon application (les tests unitaires sont écrits pas les développeurs, pour les développeurs). Nous sommes d'accord sur le fait qu'il faut bien commencer par un bout, et que la définition de Feathers m'aide à cela (elle indique le bon bout pour commencer). Je vois donc cette définition plus comme un outil que comme un dogme (comme Mathieu je pense). Nous sommes d'accord également que je pars du principe que la classe String du framework .NET fait ce qui est marqué dans sa documentation (le framework est livré sans ses tests ;-)) et que mon TestRunner ne m'affiche pas une lumière verte lors d'une assertion non vérifiée ;-) Merci Mathieu pour cet article et merci Romain tes précisions. Vive les moyens de communication chauds et le code (au 18 mars ?!?).
    1. Laisser un commentaire

      Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *


      Ce formulaire est protégé par Google Recaptcha