Leçon 4/9 9 min

Quoi tester, quoi ignorer

Tout tester est une perte de temps ; ne rien tester est dangereux. Le critère clair pour décider quoi tester.

Le dilemme : tout tester ou ne rien tester

Tu viens de finir une nouvelle feature. Tu te demandes si tu dois écrire des tests. Et là, deux voix se disputent dans ta tête.

La première : « Je vais tout tester. Chaque fonction, chaque getter, chaque ligne. » Résultat : tu passes trois fois plus de temps sur les tests que sur le code. Tu n'avances plus. C'est épuisant. Après deux semaines, tu arrêtes. Des tests qui coûtent plus qu'ils ne rapportent, personne ne les tient.

La deuxième : « Je teste rien, c'est trop lent. » Résultat : un bug part en prod un vendredi soir. Le calcul de remise est faux, les commandes sont cassées, et tu passes ton week-end à déboguer.

Les deux extrêmes sont perdants. Ce qu'il te faut, c'est un critère clair pour décider quoi mérite un test et quoi peut s'en passer. Et l'appliquer chaque fois, sans réfléchir trop longtemps.

Le piège de l'IA ici : quand tu lui demandes d'écrire des tests, elle génère en priorité le cas « heureux », l'entrée normale qui marche. Elle oublie presque systématiquement les cas limites. C'est exactement là que les bugs se cachent, et c'est exactement là que tu dois compléter son travail.

Le critère : qu'est-ce qui mérite un test ?

Voilà la règle simple. Elle tient en deux colonnes.

Teste toujours :

  • La logique métier : les calculs, les règles de gestion, les conditions qui font le cœur de ton application. Si c'est là que réside la valeur du produit, c'est là que doit être le filet.
  • Les cas limites : 0, vide, négatif, null, très grand, chaîne vide, liste à un seul élément. C'est là que vivent les bugs. Toujours.
  • Les chemins d'erreur : que se passe-t-il quand l'utilisateur envoie une mauvaise entrée ? Quand le réseau coupe ? Quand le fichier n'existe pas ?
  • Les bugs déjà rencontrés : à chaque bug corrigé, écris un test qui l'aurait attrapé. C'est le test de non-régression. Il garantit que ce bug ne reviendra jamais en silence.

Ignore souvent :

  • Les getters/setters triviaux : si ta méthode fait juste return this.name;, tester ça n'apporte rien. Elle ne peut pas être fausse.
  • Le code du framework ou du langage : tu ne testes pas que + additionne, ni que Array.push ajoute un élément. Ce n'est pas ta logique.
  • Les détails d'implémentation : si tu testes comment c'est fait plutôt que ce que ça fait, tes tests cassent à chaque refacto. Teste le comportement externe, pas les entrailles.

Règle d'or : si tu ne peux pas imaginer un bug qui casserait ce test, c'est que ce test n'a probablement pas besoin d'exister. Teste ce qui peut mal tourner, pas ce qui est évidemment correct.

Avec l'IA : demande-lui d'écrire les tests, puis ajoute toi-même : « Et maintenant génère les cas limites : liste vide, 0, valeur négative, null. » Elle aura oublié. Presque toujours.

À toi : attraper le cas limite que l'IA a oublié

Voici une fonction moyenne qui calcule la moyenne d'une liste de notes. Le test du cas nominal passe au vert. Mais un deuxième test, le cas limite de la liste vide, va passer au rouge. Lis le message d'erreur, trouve le garde-fou, corrige, et relance jusqu'au vert.

Prédis avant de lire

La fonction divise la somme par notes.length. Que renvoie moyenne([]) (liste vide), et pourquoi est-ce un piège classique ? Écris ta réponse avant de dérouler.

Voir la réponse

moyenne([]) renvoie NaN : la somme vaut 0, notes.length vaut 0, donc on fait 0 / 0. En JavaScript, 0 / 0 ne lève pas d'erreur. Il renvoie silencieusement NaN (Not a Number). Ce n'est pas un crash, ce n'est pas un 0, c'est une valeur « empoisonnée » qui va se propager partout où tu l'utilises. Le test attendait 0, il reçoit NaN : rouge. C'est typiquement le cas limite que le code « heureux » oublie, et que l'IA génère rarement d'elle-même.

🧪 Labo de test · édite le code, lance, observe
Code à tester
Tests
Bloqué sur le fix ? Voir le garde-fou

Ajoute en tout début de fonction : if (notes.length === 0) return 0;. Avant de diviser, on vérifie que la liste n'est pas vide. Si elle l'est, on renvoie 0 immédiatement : pas de division par zéro, pas de NaN. Relance : les deux tests passent au vert. Ce guard-clause d'une ligne est exactement ce que l'IA omet quand elle génère le cas nominal : elle pense au chemin heureux, pas aux bords.

L'illusion du 100 % de couverture

Tu as entendu parler de la couverture de code (code coverage). L'outil mesure quelles lignes de ton code ont été exécutées au moins une fois pendant les tests. Atteindre 100 % semble être le Graal : si toutes les lignes tournent, le code est sûr, non ?

Non. Et c'est l'une des illusions les plus tenaces du testing.

La couverture te dit qu'une ligne a été exécutée. Elle ne te dit pas qu'elle a été exécutée avec les bons cas, ni que le résultat a été vérifié. Un test sans assertion (ou avec une assertion volontairement fausse) peut « couvrir » 100 % de tes lignes et ne détecter absolument rien.

Exemple concret : imagine ce test :

test('moyenne', () => {
  moyenne([10, 20]);
  expect(true).toBe(true);
});

Ce test appelle moyenne : toutes les lignes de la fonction sont exécutées, la couverture monte à 100 %. Mais expect(true).toBe(true) passe toujours, peu importe ce que la fonction renvoie. Ce test ne détecterait jamais un résultat faux. 100 % de couverture, valeur nulle.

Faux confort du 100 % : la couverture est un indicateur de ce qui n'est pas testé : les lignes jamais exécutées sont clairement hors couverture. Mais des lignes à 100 % ne prouvent rien sur la qualité des assertions ni sur les cas utilisés. Ne vise jamais le 100 % pour le 100 % : vise des tests qui vérifient les bons cas avec les bonnes assertions.

Où vivent les bugs : le cas nominal est large et bien couvert (zone verte), mais les bugs se concentrent aux cas limites (0, vide, négatif, null, très grand) dans la zone rouge/orange aux bords. Cas nominal / Happy path bien couvert · l'IA génère ça Cas limites 0, vide, négatif, null, très grand 🐛 🐛 🐛 🐛 ← Entrées « normales » Bords ← L'IA et les devs pressés couvrent le centre · les bugs se cachent aux bords

Utilise la couverture comme un radar du non-testé, pas comme un certificat de qualité. Si tu vois une ligne jamais exécutée, demande-toi pourquoi : c'est utile. Mais si tu atteins 100 %, ne t'arrête pas là. Demande-toi si tes tests vérifient vraiment les cas qui comptent.

The dilemma: test everything or test nothing

You just shipped a new feature. You're wondering whether you should write tests. Two voices argue in your head.

The first: "I'll test everything. Every function, every getter, every line." Result: you spend three times longer on tests than on code. Nothing moves forward. It's exhausting. After two weeks you quit. Tests that cost more than they're worth, nobody keeps up.

The second: "I'm testing nothing, it's too slow." Result: a bug ships on a Friday evening. The discount calculation is wrong, orders are broken, and you spend your weekend debugging.

Both extremes lose. What you need is a clear rule to decide what deserves a test and what can skip it — and apply that rule every time, without overthinking.

The AI trap here: when you ask it to write tests, it generates the "happy" case first — the normal input that works. It almost always forgets the edge cases. That's exactly where bugs hide, and exactly where you need to fill in the gap.

The rule: what deserves a test?

Here's the simple rule. It fits in two columns.

Always test:

  • Business logic — calculations, business rules, conditions that form the core of your app. If that's where the value lives, that's where the net must be.
  • Edge cases — 0, empty, negative, null, very large, empty string, single-element list. That's where bugs live. Always.
  • Error paths — what happens when the user sends bad input? When the network drops? When the file doesn't exist?
  • Bugs you've already seen — every time you fix a bug, write a test that would have caught it. That's a regression test: it guarantees the bug never comes back silently.

Often skip:

  • Trivial getters/setters — if your method just does return this.name;, testing it adds nothing. It can't be wrong.
  • Framework or language code — you don't test that + adds numbers, or that Array.push appends an item. That's not your logic.
  • Implementation details — if you test how something is done rather than what it does, your tests break on every refactor. Test external behavior, not internals.

Golden rule: if you can't imagine a bug that would break this test, the test probably doesn't need to exist. Test what can go wrong, not what is obviously correct.

With the AI: ask it to write the tests, then add: "Now generate the edge cases — empty list, 0, negative value, null." It will have forgotten. Almost always.

Your turn: catch the edge case the AI forgot

Here's an average function that calculates the average of a list of numbers. The happy-path test goes green. But a second test — the edge case of the empty list — will turn red. Read the error, find the guard, fix it, and rerun until green.

Predict before reading on

The function divides the sum by numbers.length. What does average([]) (empty list) return, and why is this a classic trap? Write your answer before expanding.

Show the answer

average([]) returns NaN: the sum is 0, numbers.length is 0, so we compute 0 / 0. In JavaScript, 0 / 0 throws no error — it silently returns NaN (Not a Number). It's not a crash, it's not a 0, it's a "poisoned" value that spreads wherever you use it. The test expected 0, it got NaN: red. This is exactly the edge case the "happy" code forgets — and that the AI rarely generates on its own.

🧪 Test lab · edit the code, run, observe
Code under test
Tests
Stuck on the fix? Show it

Add at the very start of the function: if (numbers.length === 0) return 0;. Before dividing, check that the list is not empty. If it is, return 0 immediately — no division by zero, no NaN. Rerun: both tests go green. This one-line guard clause is exactly what the AI omits when it generates the happy path: it thinks about normal input, not the edges.

The illusion of 100% coverage

You've heard of code coverage. The tool measures which lines of your code were executed at least once during the tests. Reaching 100% sounds like the holy grail: if every line runs, the code is safe, right?

No. And this is one of the most persistent illusions in testing.

Coverage tells you a line was executed. It doesn't tell you it was executed with the right cases, or that the result was verified. A test with no assertion — or a deliberately weak assertion — can "cover" 100% of your lines while detecting absolutely nothing.

Concrete example:

test('average', () => {
  average([10, 20]);
  expect(true).toBe(true);
});

This test calls average: every line of the function runs, coverage reaches 100%. But expect(true).toBe(true) passes always — regardless of what the function returns. This test would never catch a wrong result. 100% coverage, zero value.

The false comfort of 100%: coverage is an indicator of what is not tested — lines never executed are clearly out of scope. But 100% lines say nothing about assertion quality or the cases used. Never chase 100% for the sake of 100%: aim for tests that check the right cases with the right assertions.

Where bugs live: the happy path is wide and well covered (green zone), but bugs concentrate at edge cases — 0, empty, negative, null, very large — in the red/orange zone at the edges. Happy path / Normal case well covered · AI generates this Edge cases 0, empty, negative, null, very large 🐛 🐛 🐛 🐛 ← Normal inputs Edges ← AI and rushed devs cover the centre · bugs hide at the edges

Use coverage as a radar for the untested, not as a quality certificate. If you see a line never executed, ask yourself why — that's useful. But if you hit 100%, don't stop there: ask whether your tests actually check the cases that matter.

🎯 Pratique

S'entraîner (clique pour ouvrir) :

💬 Ré-explique sans regarder
Ré-explique sans regarder

Avec tes mots : qu'est-ce qui mérite vraiment un test, et qu'est-ce qu'on peut ignorer ?

Une bonne explication couvre : tester la logique métier (calculs, règles), les cas limites (0, vide, négatif, null), les chemins d'erreur, et les bugs déjà vus (non-régression). Ignorer les getters/setters triviaux, le code du langage ou du framework, et les détails d'implémentation internes. Le critère : si tu ne peux pas imaginer un bug qui casserait ce test, il n'a probablement pas besoin d'exister.
🧠 Rappel libre
Rappel libre

Sans remonter : pourquoi 100 % de couverture ne veut pas dire que le code est sans bug ?

La couverture dit que la ligne a été exécutée, pas qu'elle a été vérifiée avec les bons cas ni que les assertions sont correctes. Un test avec expect(true).toBe(true) peut couvrir 100 % sans rien détecter. C'est un indicateur du non-testé (les lignes jamais exécutées), jamais une preuve de qualité.
⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

L'IA dit : « J'ai 100 % de couverture, le code est donc fiable. » Son unique test : test('moyenne', () => { moyenne([10,20]); expect(true).toBe(true); }). Tu acceptes, ou tu rejettes ?

À rejeter. Le test appelle bien moyenne([10,20]), donc toutes les lignes de la fonction sont exécutées, et la couverture monte à 100 %. Mais expect(true).toBe(true) passe toujours, peu importe ce que la fonction renvoie. Ce test ne détecterait jamais un résultat faux. Couverture maximale, valeur nulle : la couverture ne remplace pas des assertions pertinentes et les cas limites.
Où se cachent le plus souvent les bugs ?
Que mesure réellement la couverture de code (code coverage) ?
Lequel ne vaut généralement PAS la peine d'être testé ?
L'IA te génère une fonction et son test, et le test passe au vert du premier coup. Quel réflexe ?
Prochaine étape

Tu as vu un test passer du rouge au vert. Mais as-tu remarqué qu'un test qui n'a jamais été rouge ne prouve rien ? À la leçon 5, on explore le rouge d'abord : pourquoi commencer par le test qui échoue est la seule façon de savoir qu'il teste vraiment quelque chose.

Leçon 5 : Le rouge d'abord →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement