~8 min de lecture

Combien de tests par feature ? 23 pour toute une app

« Il faut au moins un test par méthode. » « Vise 80 % de couverture. » « Une feature, un test. »

Trois conseils qu'on entend partout, et qui ont un point commun : ils comptent la mauvaise chose.

Voici les chiffres réels d'une app Angular développée intégralement en TDD, rapport de couverture à l'appui. Puis ce qu'il faut vraiment compter.


Les chiffres bruts

L'application : une liste de courses. Ajout de produits avec quantité et rayon, suppression, marquage « pris », popup de fin de liste, persistance locale, routing.

 Test Files  2 passed (2)
      Tests  23 passed (23)
   Duration  4.41s

23 tests. 2 fichiers. Et quelques secondes seulement : entre 2 et 4,5 s selon la machine.

Et la couverture :

-------------------|---------|----------|---------|---------|
File               | % Stmts | % Branch | % Funcs | % Lines |
-------------------|---------|----------|---------|---------|
All files          |   93.02 |    97.67 |   78.57 |   93.02 |
 app               |     100 |      100 |     100 |     100 |
 ...ing-list/pages |     100 |      100 |     100 |     100 |
 ...ing-list/ports |     100 |      100 |     100 |     100 |
 ...ports/adapters |   52.27 |    88.88 |      50 |   52.27 |
 ...ing-list/store |     100 |      100 |     100 |     100 |
 ...opping-list/ui |     100 |      100 |     100 |     100 |
 ...list/use-cases |     100 |      100 |     100 |     100 |
-------------------|---------|----------|---------|---------|

⚠️ 93 %, pas 100 %. Et c'est important de le dire, parce que le chiffre rond fait vendre mais renseigne mal.

Le trou qui pèse réellement sur le pourcentage est entièrement localisé dans ports/adapters, et tient à un seul fichier : local-storage-products-adapter.ts, à 19 %. C'est l'implémentation de production, systématiquement remplacée en test par un adaptateur en mémoire. Le choix de tester avec un fake laisse ce fichier découvert. C'est un coût assumé, pas un oubli.

Un second fichier affiche 0 % sans que ça compte : product.ts, rangé dans use-cases/, est un type TypeScript pur. Il ne produit aucune instruction exécutable, donc il n'y a rien à couvrir et il ne pèse rien dans le calcul. Un artefact du rapport, rien de plus.

Tout le reste - pages, store, use cases, composants UI, ports - est à 100 %.

📖 Lire les trois colonnes

Le rapport n'affiche pas « la » couverture mais trois mesures différentes, et c'est là que se cache l'information :

  • % Stmts / % Lines : les instructions et les lignes réellement exécutées au moins une fois. Ici les deux valeurs coïncident (93,02) parce que le code ne met pas plusieurs instructions sur une même ligne.
  • % Branch : les embranchements empruntés. Un if, un ternaire ou un ?? compte deux chemins ; n'en parcourir qu'un laisse la ligne « couverte » alors qu'un cas n'a jamais tourné. À 97,67 %, c'est la mesure la plus flatteuse du lot - et le seul chemin qui lui manque n'est pas dans l'adaptateur de production (dont toutes les branches sont couvertes) mais dans l'adaptateur en mémoire, celui qui sert aux tests. % Branch désigne donc un autre fichier que les autres colonnes.
  • % Funcs : les fonctions appelées au moins une fois. À 78,57 %, c'est de loin la plus basse - ce sont surtout les méthodes de l'adaptateur de production, jamais invoquées en test.

Trois colonnes, trois histoires. C'est exactement pour ça que « la couverture » au singulier ne veut pas dire grand-chose.


La répartition, feature par feature

Ce qui est testé Tests
Ajouter un article (validation, message vide qui disparaît, reset, rayon, rayon par défaut, id, persistance) 7
Marquer un article comme pris (état, rendu, ouverture popup, cas liste vide) 4
Popup de fin de liste (annuler, confirmer, confirmer après « Terminer », liste vide) 4
Relecture depuis le stockage (affichage, suppression, mise à jour) 3
Liste vide (message affiché, liste absente) 2
Quantité affichée 1
Supprimer un article 1
Routing vers la page 1
Total 23

Première observation : « une feature, un test » est faux. L'écart va de 1 à 7.

Deuxième observation, plus intéressante : la feature la plus coûteuse (l'ajout, 7 tests) n'est pas la plus complexe techniquement. C'est celle qui a le plus de règles métier distinctes :

  1. un nom vide ou composé d'espaces est refusé
  2. un article soumis apparaît dans la liste
  3. le message « pas encore d'article » disparaît
  4. les champs du formulaire sont vidés
  5. le rayon saisi est appliqué
  6. sans rayon, c'est « Autres »
  7. l'article est écrit dans le stockage

Sept règles, sept tests. Pas parce qu'une convention l'impose, mais parce que chacune peut casser indépendamment des six autres.


La bonne unité de compte : le comportement

Voilà la règle qui remplace toutes les autres.

Un test par comportement observable. Un comportement, c'est quelque chose qui peut casser tout seul.

Ce n'est ni « par méthode » (une méthode peut porter zéro ou cinq comportements), ni « par classe » (une classe bien découpée en porte un seul), ni « par feature » (une feature, c'est un paquet de comportements).

✅ Deux tests justifiés qui ont l'air redondants

it('should mark a product as picked up when I click on it', async () => {
  await pageModel.addProductToList({ name: 'Lait' });
  await pageModel.checkProduct(0);
  expect(store.products()[0].pickedUp).toBe(true);
});

it('should set product checkbox checked when picked up the product', async () => {
  await pageModel.addProductToList({ name: 'Lait' });
  await pageModel.checkProduct(0);
  const compiled = fixture.nativeElement.querySelector(
    '[data-testid=product-item] input[type=checkbox]'
  );
  expect(compiled.checked).toBe(true);
});

Même action, deux tests. Redondant ? Non : deux causes de panne distinctes. Le premier tombe si la logique de bascule casse, le second si le binding du template casse. Tu peux casser l'un sans l'autre.

❌ Deux tests réellement redondants

// ❌ Même comportement, même niveau, deux formulations
it('should add a product', async () => {
  await pageModel.addProductToList({ name: 'Lait' });
  expect(store.products().length).toBe(1);
});

it('should have one product in the list after adding one', async () => {
  await pageModel.addProductToList({ name: 'Lait' });
  expect(store.products()).toHaveLength(1);
});

Là, aucun scénario de bug ne fait tomber l'un sans l'autre : les deux lisent la même valeur au même endroit, à l'opérateur d'assertion près. Le second ne rapporte rien.

⚠️ C'est le « même niveau » qui fait la redondance, pas la « même action ». Deux tests qui partent de la même action mais interrogent l'un l'état et l'autre le DOM ne sont pas redondants - c'est le cas juste au-dessus. Le critère n'est jamais « ça se ressemble », c'est « un seul bug peut-il faire tomber l'un en laissant l'autre vert ? ».

Le test du test : « quel bug ce test attrape-t-il, que les autres laissent passer ? » Si tu ne sais pas répondre en une phrase, supprime-le.


Pourquoi viser un pourcentage se retourne contre toi

La couverture est un bon indicateur et une mauvaise cible. C'est la loi de Goodhart : dès qu'une mesure devient un objectif, elle cesse d'être une bonne mesure.

Concrètement, ce test amène 100 % de couverture sur un composant et ne garde rien :

// ❌ Couverture parfaite, valeur nulle
it('should create', () => {
  const fixture = TestBed.createComponent(MyComponent);
  expect(fixture.componentInstance).toBeTruthy();
});

Il exécute le constructeur, donc les lignes sont « couvertes ». Il ne vérifie aucun comportement. Multiplie-le par quarante composants et tu obtiens un joli 85 % qui ne détectera jamais une régression.

💡 Comment lire un rapport de couverture, alors ?

À l'envers. Ne regarde pas le pourcentage global, regarde les lignes rouges et pose une question par ligne :

  • « Ce code peut-il casser sans que personne le voie ? » ➜ il manque un test.
  • « Ce code est-il remplacé en test par un fake ? » ➜ normal, mais il faut le couvrir ailleurs.
  • « Ce code n'émet rien à l'exécution ? » ➜ bruit du rapport, ignore.

Dans le tableau plus haut, les trois cas sont présents. Aucun ne se déduit du chiffre 93.02.

Et souviens-toi du tableau : l'adaptateur non testé saute aux yeux ligne par ligne (19,23 %), alors que le 93,02 % global le noie. Ce n'est pas telle colonne qui est meilleure qu'une autre, c'est le détail par fichier qui informe et l'agrégat qui rassure à tort.


Et le temps d'exécution ?

Quelques secondes pour la suite complète. C'est le chiffre dont on parle le moins, et c'est peut-être le plus important.

En TDD, tu lances les tests des dizaines de fois par heure. Une suite à 4 secondes, tu la laisses tourner en watch et elle t'informe en continu. Une suite à 3 minutes, tu la lances avant de partir déjeuner - et le feedback instantané, qui est la moitié de l'intérêt de la méthode, disparaît.

Ça se protège :

  • des fakes en mémoire plutôt que du vrai I/O
  • pas de setTimeout réel dans les tests
  • un seul composant monté par test, et pas plus d'arbre que le scénario n'en demande
  • Vitest en mode watch, qui ne relance que ce qui est impacté

Conclusion

23 tests pour une app complète, ça paraît peu quand on a l'habitude de compter les tests. Ça paraît juste quand on compte les comportements.

La différence entre les deux, c'est la différence entre une suite qui te ralentit et une suite qui te protège.

Ce que tu dois retenir :

Un test par comportement, pas par méthode ni par classe

✅ Un comportement = quelque chose qui peut casser tout seul

La couverture est un indicateur, jamais un objectif

Lis les lignes rouges, pas le pourcentage

✅ Un fake laisse un trou assumé sur l'implémentation de production

Le temps d'exécution est une contrainte de conception, pas un détail

La règle d'or :

Avant d'écrire un test, demande-toi quel bug il attrapera. Pas de réponse en une phrase ? Ce n'est pas un test que tu écris, c'est du pourcentage.


Pour aller plus loin


Envie de découvrir EasyAngularKit et son programme ?

Découvre EasyAngularKit : https://www.easyangularkit.com/easyangularkit

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

AngularKit

Suite d'outils pour développeurs Angular francophones. Apprends, modernise tes réflexes, audite ta codebase.

Produits

Contact

Légal

© 2026 AngularKit. Tous droits réservés.