La pyramide des tests par la pratique (5/5)

Jusqu’à présent, dans les articles précédents (1, 2, 3, 4), nous avons toujours mis en place des tests en isolation, nous fournissant ainsi un feedback précis et fiable - et plus ou moins rapide selon qu’on teste unitairement ou avec des tests de composants (contexte Spring à charger). Mais ces tests ont leurs limites, justement du fait de l’isolation. Dans cet article nous allons traiter des tests qui se trouvent plus haut dans la pyramide: les tests d’intégration et les tests de bout en bout.

body .gist .highlight { background: #202020; } body .gist tr:nth-child(2n+1) { background: #202020; } body .gist tr:nth-child(2n) { background: #202020; } body .gist .gist-meta { display:none; } body .gist .blob-num, body .gist .blob-code-inner, body .gist .pl-s2, body .gist .pl-stj { color: #f8f8f2; } body .gist .pl-c1 { color: #ae81ff; } body .gist .pl-enti { color: #a6e22e; font-weight: 700; } body .gist .pl-st { color: #66d9ef; } body .gist .pl-mdr { color: #66d9ef; font-weight: 400; } body .gist .pl-ms1 { background: #fd971f; } body .gist .pl-c, body .gist .pl-c span, body .gist .pl-pdc { color: #75715e; font-style: italic; } body .gist .pl-cce, body .gist .pl-cn, body .gist .pl-coc, body .gist .pl-enc, body .gist .pl-ens, body .gist .pl-kos, body .gist .pl-kou, body .gist .pl-mh .pl-pdh, body .gist .pl-mp, body .gist .pl-mp1 .pl-sf, body .gist .pl-mq, body .gist .pl-pde, body .gist .pl-pse, body .gist .pl-pse .pl-s2, body .gist .pl-mp .pl-s3, body .gist .pl-smi, body .gist .pl-stp, body .gist .pl-sv, body .gist .pl-v, body .gist .pl-vi, body .gist .pl-vpf, body .gist .pl-mri, body .gist .pl-va, body .gist .pl-vpu { color: #66d9ef; } body .gist .pl-cos, body .gist .pl-ml, body .gist .pl-pds, body .gist .pl-s, body .gist .pl-s1, body .gist .pl-sol { color: #e6db74; } body .gist .pl-e, body .gist .pl-ef, body .gist .pl-en, body .gist .pl-enf, body .gist .pl-enm, body .gist .pl-entc, body .gist .pl-entm, body .gist .pl-eoac, body .gist .pl-eoac .pl-pde, body .gist .pl-eoi, body .gist .pl-mai .pl-sf, body .gist .pl-mm, body .gist .pl-pdv, body .gist .pl-som, body .gist .pl-sr, body .gist .pl-vo { color: #a6e22e; } body .gist .pl-ent, body .gist .pl-eoa, body .gist .pl-eoai, body .gist .pl-eoai .pl-pde, body .gist .pl-k, body .gist .pl-ko, body .gist .pl-kolp, body .gist .pl-mc, body .gist .pl-mr, body .gist .pl-ms, body .gist .pl-s3, body .gist .pl-smc, body .gist .pl-smp, body .gist .pl-sok, body .gist .pl-sra, body .gist .pl-src, body .gist .pl-sre { color: #f92672; } body .gist .pl-mb, body .gist .pl-pdb { color: #e6db74; font-weight: 700; } body .gist .pl-mi, body .gist .pl-pdi { color: #f92672; font-style: italic; } body .gist .pl-pdc1, body .gist .pl-scp { color: #ae81ff; } body .gist .pl-sc, body .gist .pl-sf, body .gist .pl-mo, body .gist .pl-entl { color: #fd971f; } body .gist .pl-mi1, body .gist .pl-mdht { color: #a6e22e; background: rgba(0, 64, 0, .5); } body .gist .pl-md, body .gist .pl-mdhf { color: #f92672; background: rgba(64, 0, 0, .5); } body .gist .pl-mdh, body .gist .pl-mdi { color: #a6e22e; font-weight: 400; } body .gist .pl-ib, body .gist .pl-id, body .gist .pl-ii, body .gist .pl-iu { background: #a6e22e; color: #272822; } body .gist .gist-file, body .gist .gist-data { border: 0px; border-bottom: 0px; }

Tests d’intégration

Tests d'intégration

Que tester ?

Malgré tous les tests que nous avons mis en place, il subsiste encore des trous dans la raquette. En effet, nous avons jusqu’à présent isolé l’ensemble des tests, mais que va-t-il vraiment se passer au moment de connecter l’ensemble ? Sommes-nous certains que notre composant journey-booking appelle bien connection-lookup ? Et que la base de données est correctement configurée et accessible ?

C’est l’objectif des tests d’intégration de vérifier que le “câblage” est bien fait et que notre composant communique effectivement avec ses dépendances, ni plus ni moins. On ne testera pas différents cas métiers (paramètres d’entrée, comportements, ...), cela a normalement déjà été fait dans les tests unitaires.

Le schéma suivant synthétise les tests d’intégration que nous allons mettre en place :

Tests d'intégration

Implémentation

Pour le coup, la simplicité d’écriture des tests est inversement proportionnelle à la complexité de leur exécution. Nous restons dans l’environnement JUnit / Spring et l’objectif est de valider l’intégration, pas le métier. Nous pourrions presque nous contenter de faire appel à la méthode, sans autre assertion que le fait de réussir à appeler le composant externe.

Base de donnée

Le test du Repository simple et sans configuration particulière va nous permettre de vérifier la connexion à la base configurée dans l’application.yml. Le test (lien gitlab) est donc très simple :

Voir le lien github

La vraie complexité se trouve plus dans les scripts de population des données, implémentés ici grâce à l’annotation @Sql, et qu’il faudra maintenir au gré des évolutions du modèle et conserver le plus rapide possible. De même, le script de nettoyage a une importance capitale dans la capacité à rejouer le test autant de fois que nécessaire et pour éviter les débordements sur d’autres tests (qui échoueront et nécessiteront du temps d’investigation).

Client HTTP

Le test de la connection au service de Lookup est encore plus simple (lien gitlab) :

Voir le lien github

Quid de l’exécution ?

Si les tests sont faciles à écrire, il va en revanche falloir disposer de tout l’environnement pour les exécuter. Et plus il y aura de microservices et de liens entre eux, plus l’environnement sera complexe à mettre en oeuvre.

Docker peut être notre ami pour résoudre un tel enjeu mais je ne rentrerai pas dans les détails. Le code disponible sur gitlab contient le nécessaire (config maven, Dockerfile, docker-compose.yml) pour démarrer une base de données postgresql et les deux microservices. Un point important tout de même : les tests d’intégration ne sont pas joués dans le cycle maven standard, ils sont dans un projet à part. Il vous faudra mettre en place un pipeline de build (sur gitlab, jenkins, …) afin de démarrer les services avant de pouvoir lancer ces tests (voir le readme).

Tests de bout en bout

Tests end to end

Que tester ?

Finalement, nous arrivons au sommet de la pyramide avec les tests de bout en bout dont je parlais en aparté dans le premier article. Comme son nom l’indique, l’objectif de ce type de test est de valider la chaîne complète. Il vise à valider l’intégration de tous les composants entre eux. Sur le schéma ci-dessous, on constate donc que l’on va tester nos deux services ainsi que la base et la connection à l’Open API de transport :

Tout comme les tests d’intégration, l’objectif d’un test de bout en bout est de valider la “tuyauterie”, aucunement le métier.

Les tests de bout en bout se déclinent généralement en tests d’interface graphique (IHM) lorsqu’on parle d’application web ou mobile, ou en tests d’interface programmatique lorsqu’on est sur des APIs REST comme dans notre cas.

Implémentation

Le point d’entrée de notre composant va nous aider à déterminer comment le tester. Sur une interface web, on utilisera probablement Selenium ou un équivalent (ex: Protractor sur de l’Angular). Sur une API, je recommande le framework rest-assured. C’est ce que nous allons mettre en place dans l’exemple (lien gitlab) :

Voir le lien github

Après avoir configuré l’url de votre API, rest-assured permet d’utiliser la syntaxe BDD pour requêter les différents endpoints. Il est ainsi possible de configurer la requête (given : headers, paramètres, body, …), de l’exécuter (when), et finalement de vérifier la réponse (then : code retour, headers, body, ...). Il est notamment possible de vérifier le contenu d’un json avec json path.

Tout comme les tests d’intégration, il faudra mettre en place l’environnement d’exécution avant de pouvoir lancer ces tests. Les tests de bout en bout sont donc aux côtés des tests d’intégration dans un projet dédié. Ils seront exécutés en même temps par le pipeline de build, après avoir instancié votre environnement (ou déployé les services sur des serveurs existants).

Autres tests

La pyramide telle que décrite initialement ne faisait pas mention d’autres types de tests. Mais nous ne serions pas complets si nous ne citions pas à minima les autres types de tests qu’il est important de mettre en oeuvre et d’automatiser autant que possible.

Tests de sécurité

Dans notre exemple, nous n’avons pas mis en place de sécurité applicative, mais sachez que Spring (et les autres librairies utilisées) fournissent le nécessaire (authentification et autorisations) pour les tests de composants, d’intégration ou bout en bout que nous avons développés.

Par ailleurs, TOP 10 2017). Ils fournissent notamment un guide très complet sur les tests ainsi qu’une liste d’outils permettant de les automatiser.

Tests de performance

Ce type de test intervient généralement très tard dans le cycle de développement, ce qui est clairement à l’encontre du principe de rapidité du feedback. Il est pourtant possible d’automatiser une partie de ces tests, de les exécuter régulièrement dans le pipeline de build et d’avoir un feedback sur l’évolution des performances de l’application. Des outils tels que Gatling ou JMeter sont les outils de choix si vous souhaitez testez vos performances en continu.

Tests d’acceptance

Les tests d’acceptance ou tests fonctionnels sont les tests qui permettent de valider l’application d’un point de vue utilisateur (on dit d’ailleurs souvent User Acceptance Tests / UAT).

Je suis partagé sur ce type de tests : ils sont généralement du même type que les tests de bout en bout (tests d’IHM bien souvent) auxquels on ajoute une couche de BDD (cucumber) pour la compréhension par le métier / les testeurs. On souhaite donc les limiter (pour toutes les raisons exposées plus haut). Mais en parallèle le métier voudrait valider l’ensemble des règles, cas d’usage, etc.

Je conseille de se limiter à quelques smoke tests, qui permettent de traverser l’ensemble de l’application (le fameux happy path). Encore une fois, le métier doit être validé plus bas dans la pyramide. Il s’agira là de construire une relation de confiance entre les développeurs et le métier sur leur capacité à tester l’application fonctionnellement et assurer la non-régression grâce aux tests déjà développés.

Conclusion

Il est temps de conclure cette série d’articles. Au final, nous avons 26 tests unitaires, 15 tests de composants (+ 1 test de contrat qui pourrait avantageusement remplacer un test de composant), 2 tests d’intégration et 2 tests de bout en bout. La pyramide est donc globalement respectée et notre composant sécurisé.

Pour résumer, et si vous ne deviez retenir que quelques points :

  • Favorisez les tests qui donnent un feedback précis, rapide et fiable.
  • Investissez massivement dans les tests unitaires, qui sont peu chers à écrire et fournissent les meilleurs feedbacks sur votre code métier.
  • Ne délaissez pas les tests de composants qui permettent de valider la configuration et la “glue”.
  • Si vous faites des web services / API / microservices, peu importe le nom que vous leur donnez, envisagez sérieusement les tests de contrat pour vous assurer de la rétrocompatibilité.
  • Limitez les tests d’intégration ou de bout en bout, plus complexes à exécuter, à la seule vérification de la tuyauterie entre votre application et le reste du SI.
  • Automatisez l’ensemble de ces tests et exécutez-les le plus souvent possible sur un serveur d’intégration continue.
  • Ne dupliquez pas les tests à chaque niveau de la pyramide : chaque type de test a son intérêt/objectif. Il s’agit d’une pyramide et non d’un cube, pyramide que j’ai complétée dans le schéma suivant pour synthétiser cet article.

Références