Tester (2) Mise en pratique

2022-05-16 | Benoît Petitcollot

image article

Tester (2) : Mise en pratique

Après la présentation des grandes lignes de notre stratégie de test, Il est temps d'aborder plus précisément leur traduction concrète : quels types de tests nous écrivons, en quelle quantité et avec quels outils.

Les différents types de tests

La traditionnelle pyramide de tests distingue trois principaux types de tests automatisés et indique la proportion que chacun d'eux devrait représenter sur l'ensemble des tests.

La pyramide de tests

  • Les tests unitaires, en bas de la pyramide, devraient être les plus nombreux. Chacun teste une portion minimale de code, le plus souvent une fonction ou une classe. Ces tests sont très rapides puisque le chargement du code et l'exécution du calcul sont minimaux. De plus ce sont les tests les moins fragiles car ils ne risquent d'échouer qu'en cas de modification de la petite partie de code les concernant. Nous utilisons pour les tests unitaires la bibliothèque PHPUnit
  • Les tests d'intégration, à l'étage intermédiaire, ont pour rôle de valider le fonctionnement de modules applicatifs plus larges que les tests unitaires sans pour autant simuler un utilisateur réel. Ils représentent un compromis en vitesse d'exécution ainsi qu'en robustesse. Pour ces tests, nous utilisons la bibliothèque Behat.
  • Les tests de bout-en-bout (end-to-end), au sommet de la pyramide, sont les moins nombreux. Ce sont des tests qui reproduisent le plus fidèlement possible les actions d'un utilisateur réel de l'application. Ils mettent donc en jeu l'ensemble des couches applicatives depuis le modèle métier jusqu'à l'interface utilisateur en passant par les appels réseau et les éventuelles bibliothèques utilisées à ces différents niveaux. Ces tests sont donc beaucoup plus lents à exécuter et aussi plus fragiles car ils risqueront d'être mis en échec dès lors qu'une modification est introduite dans un des nombreux modules qu'ils exécutent. Nous utilisons les bibliothèques Panther et Cypress pour ces tests. Nous avons dernièrement publié un article sur Panther.

Cette pyramide a été critiquée notamment sur la répartition des tests. Des représentations alternatives ont été proposées. Ce qu'il faut en retenir, c'est que si un cas d'usage peut être validé par des tests à différents niveaux, il vaut mieux privilégier le plus bas car il sera plus rapide à exécuter et plus robuste. Cependant, on ne pourra pas faire l'économie de tests de plus hauts niveaux dans les cas les plus complexes.

Concrètement, nous écrivons peu de tests de bout-en-bout et davantage de tests d'intégration que de tests unitaires.

La couverture de test

En réalité la quantité et le type de tests à écrire dépendent de l'évaluation du rapport bénéfice/investissement que représente chaque test. Voici à titre d'exemple quelques éléments pour comprendre la couverture de tests que nous avons adopté pour le projet IFprofs (projet type réseau social).

IFprofs est architecturé de la manière suivante :

  • Une API en PHP / Symfony.
  • Un front en ReactJS / NextJS.
  • Un serveur RabbitMQ permettant à l'API de déclencher des actions (envoi d'email, indexation...).
  • Un mailer JavaScript qui construit les mails en ReactJS et délègue l'envoi à un serveur SMTP externe.
  • Des workers PHP qui gèrent l'indexation du contenu du site pour la fonctionnalité de recherche.
  • Un serveur de websockets JavaScript pour la messagerie instantanée.
  • Des CRONs en PHP pour l'envoi de newsletters, le calcul de scores de pertinences (listes de recommandations personnalisées pour chaque utilisateur), et quelques opérations de maintenance récurrentes sur les données.

Il est difficile de comparer précisément les quantités des différents types de tests, ceux-ci étant écrits à l'aide de langages et de bibliothèques différentes. Les tests sur IFprofs sont grosso-modo répartis ainsi : 75% de tests d'intégration, 25% de tests unitaires et une quantité négligeables de tests de bout-en-bout.

Cette répartition peut sembler curieuse au vu des explications théoriques précédentes...

Peu de bout-en-bout. Ces tests sont trop coûteux à maintenir. Nous en écrivons donc très peu.

Davantage de tests d'intégration que de tests unitaires. Ce constat est encore amplifié par le fait que certains tests classés comme unitaires (car écrits avec PHPUnit) ressemblent plutôt à des tests d'intégration. Cependant, nous utilisons un certain nombre de bibliothèques PHP ou Javascript qui sont elles-mêmes largement testées essentiellement par des tests unitaires. En fait la pyramide de tests prend tout son sens dans le cadre de l'ensemble du code d'une application. Pour une application web, les calculs spécifiques au métier peuvent représenter une part relativement faible de l'ensemble du fonctionnement. Les tâches "lire le contenu de la requête", "stocker et récupérer des données", "contrôler les droits d'accès", "envoyer une réponse", etc. sont gérées par ces bibliothèques. Kent C. Dodds a proposé une représentation alternative, le testing trophy, avec laquelle il explique pourquoi à son avis les tests d'intégration doivent être privilégiés.

Beaucoup de tests PHP et peu de tests javascript. Nous pratiquons une stratégie de tests centrée sur les données, c'est à dire que nos tests visent essentiellement trois objectifs : les données doivent être correctement modifiées au cours des actions des utilisateurs; elles doivent être correctement récupérées en lecture (les données récupérées doivent être celles demandées, ni plus, ni moins); enfin les droits d'accès des différents utilisateurs aux données doivent être respectés. Cet enjeu nous pousse à écrire beaucoup de tests sur l'API qui est le point central d'échange entre la base de données et l'utilisateur. (Ce sont les 800 tests d'intégration écrits avec Behat). Nous testons aussi les CRONs qui effectuent des opérations de maintenance des données.

Les tests unitaires concernent finalement les quelques composants de l'application que nous avons écrits nous-même et qui mettent en œuvre des aspects de logique complexe qui ne pouvaient pas être traités par une bibliothèque. Ce sont d'abord, de manière exhaustive, tous les filtres et ordres utilisés dans nos requêtes en base de données. Ce sont aussi des calculs de pertinence de contenu basés sur le profil des utilisateurs, des algorithmes de composition de formules algébriques de recherche destinées au moteur d'indexation à partir des actions réalisées dans l'interface utilisateur.

Conclusion

On voit que la structure des tests est fortement liée au type d'application. Dans le cas d'IFprofs, il s'agit d'une plateforme d'échange de données entre utilisateurs. Le besoin en calculs est limité et l'accent est mis sur le nœud principal d'échange des données que constitue l'API grâce aux tests Behat. Dans un prochain article, je vous montrerai la physionomie de ces tests et pourquoi ils peuvent éventuellement servir aussi à autre chose que tester le code...

Tests Qualité Workflow