Utilisez les tests

"Legacy code, c’est simplement du code sans tests." Michael Feathers

Trop souvent négligés, les tests vous apportent non seulement la confiance que votre code fait ce qu'il est censé faire, mais également la sérénité lorsque vous y apportez des modifications. Dans les autres chapitres de ce cours, on a évoqué certaines typologies de tests. Dans ce chapitre, nous allons tout consolider et y apporter les éléments manquants pour présenter une stratégie globale de tests.

Une image qui est souvent associée aux stratégies de tests est la Testing Pyramid, qui ressemble un peu à ça :

Pyramide de tests montrant trois niveaux : Unit en base (tests unitaires), Integration au milieu (tests d'intégration) et E2E au sommet (tests de bout en bout).
Testing Pyramid

Le principe derrière la Testing Pyramid, c'est qu'on écrit beaucoup beaucoup beaucoup de tests unitaires, pas mal de tests d'intégration, et très peu de tests end-to-end. Le raisonnement est que les tests unitaires sont très rapides et très peu coûteux à écrire à maintenir ; les tests d'intégration moins rapides et plus chers ; les tests E2E très lents et très chers.

Toutes ces observations sont vraies, et la finalité de votre stratégie de tests peut ressembler de très près à cette pyramide.

Mais ?

L'objectif d'une stratégie de tests n'est pas d'avoir un pourcentage pré-défini de tests d'intégration ! 

Pourquoi on teste ?

  • réduire les bugs — c'est la raison la plus flagrante, mais ce n'est pas la seule et (selon moi) ce n'est pas la plus importante

  • augmenter la qualité du code — généralement, si votre code est testable, il est mieux structuré

  • faciliter la refactorisation — un codebase est vivant et a souvent besoin d'être restructuré : on peut s'être trompé au départ, les évolutions des besoins métier peuvent nous pousser à changer la structure…sans tests automatisés, refactoriser est terrifiant et dangereux, mais avec les tests, on a confiance qu'on n'a rien cassé

  • documenter le code — les meilleurs tests sont ceux qui décrivent ce que le code est censé faire, souvent d'un point de vue métier : vos suites de tests serviront donc de documentation vivante (on parle souvent de "spec exécutable")

  • déployer plus souvent et mieux — les recherches DORA montrent qu'avoir des suites de tests automatisés améliore la fréquence et la qualité des déploiements, générant donc du chiffre d'affaires plus rapidement

"Le principal avantage ne réside pas simplement dans le fait d’éviter les bugs en production, mais dans la confiance que l’on acquiert pour modifier le système." - Martin Fowler

Un argument que l'on entend très souvent est : "on n'a pas le temps d'écrire les tests, il faut qu'on sorte ces features !". Sachez que le développement va plus vite avec les tests, pas moins vite, parce que vous passez moins de temps à debug, à valider manuellement tout changement, à avoir peur de casser des choses…et plus de temps à apporter de la valeur à vos utilisateurs.

Avec tout ça en tête, à quoi ressemble notre stratégie de tests ?

Schéma de stratégie de tests montrant les interactions entre Components, Façades, Store, Services API et Services wrapper, avec des appels externes vers HTTP et des Dépendances tierces.
La stratégie de tests

Structurez les tests unitaires

On l'a déjà évoqué à plusieurs reprises dans ce cours, mais consolidons la stratégie que je propose, et voyons comment elle peut se justifier.

La couche component (rectangle vert)

Le but de cette couche est de valider les comportements visuels et interactifs des components :

  • est-ce qu'ils affichent les données correctement ? 

  • est-ce qu'ils réagissent correctement aux actions utilisateurs ?

  • est-ce qu'ils sont accessibles ?

Pour que ces tests soient faciles et rapides à écrire, l'approche globale est d'utiliser le vrai arbre de components et une fausse pour la façade. La fausse vous permet de générer des scénarios de test très spécifiques afin de valider les comportements attendus.

On peut, si le besoin se ressent, ajouter des tests directs sur des components spécifiques. Ça peut améliorer la documentation d'un component partagé, et tester un même comportement à différents niveaux n'est pas un problème en soi : il faut simplement savoir justifier ce besoin.

La couche métier (rectangle violet)

L'objectif ici est de valider le code métier de l'application. On utilise la vraie implémentation des façades, stores, et services API. On crée des fausses ou des stubs pour les services wrapper des dépendances tierces, et on utilise HttpTestingController pour les appels HTTP.

Chaque test appelle une ou plusieurs méthodes publiques des façades et vérifie :

  • les requêtes HTTP générées

  • les données renvoyées par la façade

  • les appels aux dépendances tierces

L'un des énormes avantages d'avoir des façades 100% métier est que les tests que vous écrirez ici sont obligatoirement liés à des situations métier.

La couverture

On entend un peu tout et n'importe quoi au sujet de la couverture de tests "minimum" à atteindre. 70%, 80%, 100% ? Il est tout à fait possible d'avoir 100% de couverture sans que les suites de tests soient réellement utiles.

Je conseille de voir la couverture comme un indicateur négatif : s'il est bas, on sait qu'on manque de tests.

Il est possible, surtout en pratiquant le test-first ou le TDD, d'arriver à 100% de couverture de tests avec des tests utiles ; ça demande beaucoup de discipline, mais c'est tout à fait envisageable.

La proportion

Comme la Testing Pyramid le montre, on a globalement beaucoup plus de tests unitaires que de tests d'intégration ou e2e. C'est normal, car ce sont les tests les moins chers à écrire et les plus rapides à exécuter. La granularité de ces tests les place idéalement au carrefour entre métier et technique. Je trouve que se concentrer essentiellement sur les tests unitaires apporte le plus grand retour sur investissement en stabilité, facilité à modifier, et documentation. Une fois que vous aurez une couverture qui vous satisfait, vous pouvez commencer à regarder les autres types de test.

Structurez les tests d'intégration

En continuant avec les définitions sélectionnées pour ce cours, un test d'intégration teste comment au moins deux systèmes distincts fonctionnent ensemble.

Dans le schéma ci-dessus, l'endroit qui bénéficie le plus de tests d'intégration est le rectangle bleu : on veut vérifier que nos wrappers de dépendances tierces fonctionnent comme on le pense.

Prenons l'exemple du chapitre sur les dépendances : on a une classe wrapper  MaterialDialogs  (qui implémente notre interface  Dialogs  ) qui appelle des méthodes du système de Dialog venant de  @angular/material  . Les tests d'intégration les plus simples vérifieront que lorsqu'on appelle la méthode  open()  de notre wrapper, la boîte de dialogue s'ouvre correctement, avec le bon contenu etc.

Ces tests sont nécessairement plus lents (car ils embarquent plus de dépendances) et peuvent être plus complexes à écrire (comment on valide la boite de dialogue en question ?), mais vous donnent la confiance que vos wrappers fonctionnent comme vous le pensez (et vous sauvent lorsqu'une library publie des breaking changes !).

Par la nature-même de l'architecture de votre application, ces tests seront moins nombreux que les tests unitaires, car ils interviennent seulement aux frontières entre votre code et du code tiers.

Structurez les tests end-to-end (e2e)

Ces tests sont généralement exécutés soit contre votre environnement de production, soit sur un environnement qui y ressemble le plus possible. Ils valident des parcours utilisateurs entier :

  • enregistrement -> connexion -> déconnexion

  • ajouter un produit au panier -> checkout -> paiement -> confirmation

  • déposer une demande de congé -> confirmation par le supérieur -> confirmation

Malgré des outils de plus en plus fiables et précis (notamment Playwright) qui facilitent l'écriture de ces tests, le coût relatif aux autres types de tests est très élevé. Ces tests peuvent également être peu fiables (ils échouent à cause d'un hic navigateur et non parce qu'il y a un bug) et fragiles (un changement dans l'interface peut les casser).

Cependant, ces tests peuvent avoir une vraie valeur de confiance, surtout lorsqu'ils sont exécutés contre votre environnement de production : vous validez non seulement votre code, mais également l'intégration, l'infrastructure, la configuration, le réseau… toutes les choses que les tests unitaires ne peuvent pas vérifier.

Pour équilibrer coût et bénéfices pour ces tests :

  • dans un premier temps, à moins que vous ayez un parcours spécifique qui est fragile, n'en écrivez pas

  • lorsque vos tests unitaires et d'intégration commencent à vous apporter de la confiance, écrivez des tests e2e seulement pour les parcours les plus critiques de votre application

Quand les exécuter ?

Il y a au moins trois contextes différents pour l'exécution de vos tests :

  • sur votre machine (et celles de votre équipe)

    • les tests unitaires affectés peuvent tourner à chaque save — Jest permet notamment d'exécuter seulement les tests affectés par chaque changement

    • avant de push, la suite unitaire entière

  • dans un pipeline CI/CD

    • tous les tests unitaires

    • tous les tests d'intégration

    • pour les tests e2e, une suite très minimale avec seulement quelques parcours critiques, avec en option un pipeline parallèle non bloquant qui exécute la suite entière

  • à intervalles réguliers, souvent toutes les nuits

    • tous les tests e2e

N'oubliez pas que l'objectif principal des tests est de vous apporter confiance et sérénité : si vous avez des tests qui augmentent votre niveau de stress, ce sont certainement de mauvais tests !

En résumé

  • Les tests servent avant tout à faciliter la refactorisation et à documenter le code, pas seulement à réduire les bugs ; la Testing Pyramid reste valide mais l'objectif est de tester ce qui compte avec confiance, pas d'atteindre des pourcentages prédéfinis

  • Les tests unitaires en deux couches comportent une couche component (comportements visuels et accessibilité avec fakes de façades) et couche métier (logique business avec vraies implémentations, HttpTestingController, et fausses pour dépendances tierces) — ces tests constituent la majeure partie de votre filet de sécurité

  • Réalisez des tests d’intégration ciblés uniquement là où votre application communique avec des dépendances externes — par exemple des wrappers Material ou des services tiers.
    L’objectif est de valider que vos abstractions se comportent correctement face aux vraies bibliothèques utilisées.

  • Adoptez une stratégie d’exécution E2E équilibrée : exécutez une petite suite de tests essentiels (5 à 10 parcours critiques) de manière bloquante sur la branche main, et laissez la suite complète tourner en parallèle ou lors d’une exécution nocturne (nightly). Cette approche garantit la confiance dans les livraisons tout en préservant la rapidité du développement.

Après ce dernier chapitre, il est temps de valider vos connaissances avec un quiz !

Ever considered an OpenClassrooms diploma?
  • Up to 100% of your training program funded
  • Flexible start date
  • Career-focused projects
  • Individual mentoring
Find the training program and funding option that suits you best