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

Le précédent article nous présentait la base de la pyramide : les tests unitaires et leur caractère indispensable pour assurer la non-régression d’une application. Mais loin d’être suffisants, nous devons y associer d’autres types de tests. Dans cet article, nous allons traiter des tests de composant.

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 de composant

Tests de composant

En remontant la pyramide (plus d’intégration, moins d’isolation), nous arrivons sur les tests de composant. Leur objectif est de valider notre composant ainsi que ses limites externes, comme le feraient des tests d’intégration. Mais pour éviter une intégration complète avec des dépendances externes qui pourraient ne pas être en ligne ou fonctionnels, nous allons nous isoler (en partie) de celles-ci. Le test de composant est donc à la croisée du test unitaire (isolation) et du test d’intégration (interaction intra/inter composant) :

Tests de composant

Que tester ?

Ce type de test permet notamment de valider la configuration Spring (toutes les annotations, le fichier application.properties/yml, l’injection de dépendances, ...) et ainsi de s’assurer que chacun des objets qui constituent notre composant s’intègre correctement. Cela inclut également les interfaces du composant avec le monde extérieur. Typiquement :

  • Le Controller expose-t-il correctement nos endpoints ?
  • Notre configuration de base de données est-elle correcte ? Le Repository, que nous avions délaissé pour les tests unitaires, fonctionne-t-il ?
  • Le Client, lui aussi délaissé, est-il bien annoté ?

Implémentation

Controller Spring

S’il est vrai que nous l’avons testé unitairement, nous ne sommes pas certains que la configuration Spring est correcte. Des tests peuvent ainsi être ajoutés pour certifier que les URLs sont correctes, que le Service est bien injecté, que la validation fonctionne, que les erreurs HTTP sont gérées… Nous utiliserons Spring MVC Test Framework (MockMvc pour les intimes) qui permet entre autres de simuler des requêtes HTTP entrantes ; le framework étant lui-même basé sur les mocks de l’API Servlet.

Ci-dessous un extrait du test (lien Gitlab) :

Voir le lien github

Les choses importantes dans cet extrait :

  1. L’annotation @WebMvcTest est un peu plus légère qu’un @SpringBootTest. Plutôt que de tout auto-configurer, il ne va configurer que la partie MVC. Et pour réduire encore un peu le scope, on précise quel Controller on souhaite charger/tester. Cette annotation permet donc d’utiliser le fameux MockMvc qui va pouvoir :
    1. Simuler des appels HTTP et donc valider nos mappings d’urls,
    2. Valider les paramètres d’entrée (si vous avez utilisé l’api de validation)
    3. Vérifier la réponse que le Controller fabrique (status, body, headers, …).
  2. L’annotation @AutoConfigureJsonTesters permet d’injecter des JsonTester, classes utilitaires pour manipuler du json, et dans notre cas pour sérialiser un objet.
  3. Nous utilisons l’annotation @MockBean pour créer un mock du Service et remplacer le Bean du même type dans le contexte Spring. Comme pour les tests unitaires, cela nous permet d’être isolé. Bien que ce soit un mock, cela permet tout de même de vérifier que le Service est correctement injecté dans le Controller via Spring.

Ces tests vous sembleront assez proches des tests unitaires et pourtant nous ne validons pas du tout la même chose. En comparant les deux (TU, TC), vous remarquerez notamment que les tests de composant valident toute la glue autour du Controller (HTTP, désérialisation, validation), ici matérialisée par les annotations, alors que les tests unitaires valident les aspects “métier”.

Base de données

Peu importe l’environnement, utiliser une vraie base de données serait problématique à plus d’un titre : version du schéma, jeux de données pour les tests, nettoyage en fin de test, latence réseau ou tout simplement indisponibilité de la base. Pour pallier cela, nous pouvons utiliser une base de données mémoire (H2) lors des tests, en remplacement de la base Postgres pour l’application. Ainsi nous avons le contrôle sur le schéma et les données de test. La base mémoire est également beaucoup plus rapide.

Spring Boot fournit un mécanisme permettant de configurer “auto-magiquement” une base de données mémoire : l’annotation @DataJpaTest. Associée à la bonne dépendance Maven (H2, HSQL, Derby), Spring va configurer une Datasource et un EntityManager, sans que vous ayez la moindre ligne de code ou de configuration à fournir.

Voir le lien github

L’objectif n’étant pas de tester le framework Spring, nous nous contentons de tester l’insertion en base. Il est également pertinent de tester les méthodes custom (ex : findByFromAndTo) et/ou annotées par @Query.

Remarque : Spring fournit également des classes utilitaires d’accès aux données, typiquement DataAccessUtils utilisée ci-dessous.

Remarque 2 : @DataJpaTest est utile pour du test de Repository, ne l’utilisez pas à toutes les sauces comme nous avons pu voir chez certains clients. Ex: Testez unitairement vos Services en bouchonnant les Repositories, pas besoin de Spring ni de base de donnée mémoire. Idem pour les tests de Controller, inutile d’instancier la base. Il faut savoir qu’Hibernate va être initialisé à chaque test et plus vous aurez d’entités dans votre modèle, plus ce sera long. Souvenez-vous du critère de rapidité du feedback.

Extrait du code (lien Gitlab) :

Voir le lien github

Je vois d’ici venir les sceptiques : “Oui mais H2, c’est pas ma base, la syntaxe est différente. Ça peut marcher avec H2 pendant les tests et planter en prod, ces tests n’ont aucune valeur !”. Effectivement. Même si H2 est une solution généralement recommandée, il est probable que vous utilisiez des éléments de syntaxe spécifiques à votre RDBMS (pour des raisons d’optimisation ou simplement car il ne respecte pas complètement les standards SQL). Dans ce cas, H2 ne répond pas au besoin. Il existe alors une solution un peu plus lourde qui vise à démarrer un container Docker contenant votre base au moment des tests d’intégration. La librairie Testcontainers permet de faire ce genre de choses de façon automatique. Pour cela, il vous faudra les dépendances suivantes :

Voir le lien github

Côté code et afin de vous démontrer les deux façons de faire, j’utilise un profile (“testpg”) et un fichier application-testpg.yml contenant la configuration suivante :

Voir le lien github

Au niveau du code (lien Gitlab), rien de spécifique, on active simplement le profil en question. Hormis les annotations, le reste du code est complètement identique au test H2.

Voir le lien github

Lors de l’exécution du test, le framework va se servir de Docker pour récupérer une image Postgres et la démarrer. Vous devriez voir passer des logs de ce type :

2018-06-03 12:44:15.992 INFO 71125 --- [main] ???? [postgres:9.6.8] : Creating container for image: postgres:9.6.8 2018-06-03 12:44:16.153 INFO 71125 --- [main] ???? [postgres:9.6.8] : Starting container with ID: ea2d833e843ef8adc78da779e6b3c62b31e4c1fe8bf851ed82f1bae23f86d0c8 2018-06-03 12:44:16.629 INFO 71125 --- [main] ???? [postgres:9.6.8] : Container postgres:9.6.8 is starting: ea2d833e843ef8adc78da779e6b3c62b31e4c1fe8bf851ed82f1bae23f86d0c8 2018-06-03 12:44:21.361 INFO 71125 --- [main] ???? [postgres:9.6.8] : Container postgres:9.6.8 started

L’exécution est un peu plus lente – 600 ms (une fois l’image téléchargée) contre 400 ms pour le test avec H2 – mais cela reste raisonnable si utiliser une base mémoire n’est pas envisageable dans votre contexte.

Client HTTP

Concernant le client HTTP, le problème est le même : nous ne voulons pas requêter le vrai service pour ne pas subir les éventuels problèmes de latence, d’indisponibilité ou de comportements non souhaités. Pour remédier à cela, nous utiliserons Wiremock qui permet de fournir des stubs pour des appels HTTP.

Voir le lien github

WireMock est configuré au travers d’une règle JUnit puis utilisé pour créer un stub de l’url qui nous intéresse. On peut simuler toutes sortes de choses (aussi bien au niveau de la requête que de la réponse) et notamment le corps de la réponse (ici depuis un fichier situé dans src/main/resources/__files/). Ci-dessous le code (lien Gitlab) :

Voir le lien github

Remarque : Si vous utilisez uniquement l’annotation @SpringBootTest sans paramètre, Spring va instancier tout le contexte (en se basant sur la classe annoté @SpringBootApplication). Il va notamment auto-configurer la base de données (datasource, entityManager, ...), ce qui, dans le cas du test de client, est contre-performant. Pour éviter cela, nous sélectionnons précisément les classes qui nous intéressent et en complément, nous choisissons les éléments d’AutoConfiguration utiles pour le test (grâce à @ImportAutoConfiguration). Cela peut paraître verbeux et compliqué, voire superflu pour quelques tests, mais quand vous aurez plusieurs dizaines ou centaines de tests, c’est en minutes que vous compterez le temps perdu.

Malheureusement, le problème avec ce type de test c’est qu’il continuera de fonctionner le jour où l’API changera d’interface, wiremock faisant son travail de doublure à la perfection. Pour remédier à cela on peut faire des tests d’intégration, mais on tombe dans le travers inverse : dès que l’API ne sera pas disponible, le test échouera. Les tests de contrats vont nous permettre de répondre à cette problématique et nous verrons comment dans le prochain article.