TestsDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Génie logiciel · C5 Tests et outillage · Chapitre 1 · 5 h

Tests

Pourquoi « ça marche chez moi » ne suffit pas ; tests unitaires, d'intégration, de recette ; jeux de tests et cas limites ; framework de test ; couverture ; tests de non-régression.

« Ça marche chez moi. » Cette phrase, tout développeur l'a prononcée — juste avant que ça ne marche plus chez le client. Le bloc V attaque les deux outils qui transforment les principes des chapitres précédents en pratique fiable : les tests et Git. Ce chapitre traite les tests — et il faut être honnête sur un point : leur valeur ne se croit pas, elle s'éprouve. Tant qu'on n'a pas vu une modification innocente casser silencieusement un programme, on tient les tests pour une corvée. L'exercice est là pour provoquer cette prise de conscience.

Pourquoi tester, et pourquoi « ça marche chez moi » ne suffit pas

Tester, c'est exécuter le logiciel pour vérifier qu'il fait ce qu'on attend — systématiquement, et pas seulement sur les quelques cas qu'on a en tête. Le piège de « ça marche chez moi » est double :

Un exemple, celui de l'exercice : une fonction « année bissextile » testée sur 2020 (vrai) et 2021 (faux) semble correcte. Mais elle peut se tromper complètement sur 1900 et 2000 — les siècles, où la règle est subtile. Deux essais bien choisis ne couvrent pas un comportement. Il faut des tests pensés pour débusquer les cas où la logique est fragile.

Le retour au chapitre 2 est direct : un bug attrapé par un test coûte 1 ; le même bug découvert par l'utilisateur en production coûte 50 à 100. Les tests sont l'outil qui rapproche la détection du moment de l'erreur.

Les niveaux de tests

On teste à plusieurs échelles, chacune répondant à une question différente :

NiveauCe qu'il vérifieQuestion
Unitaireune fonction, une classe, isoléece morceau est-il correct ?
Intégrationplusieurs modules ensembles'assemblent-ils bien ?
Recettele logiciel entier, du point de vue du clientfait-il ce qui était demandé ?

Ces niveaux se complètent, ils ne se remplacent pas. Deux fonctions parfaitement correctes séparément (tests unitaires au vert) peuvent mal fonctionner ensemble (test d'intégration au rouge) : mauvais format d'échange, hypothèses incompatibles — c'est exactement le scénario du Mars Climate Orbiter (chapitre 1). Et un logiciel dont tous les modules s'intègrent bien peut malgré tout ne pas répondre au besoin : les tests de recette ferment la boucle avec le cahier des charges (chapitre 4) — ils vérifient chaque besoin bien formulé, transformé en test qui répond par oui ou par non.

Quiz · 1 question

Deux fonctions passent tous leurs tests unitaires, mais le logiciel échoue quand elles sont utilisées ensemble. Quel niveau de test aurait dû l'attraper, et qu'est-ce que cela illustre ?

  • Rien : si les tests unitaires passent, le logiciel est forcément correctunitaire suffit
  • Les tests d'intégration : des modules corrects séparément peuvent mal fonctionner ensemble (format d'échange, hypothèses incompatibles) — les niveaux de tests se complètentintégration : l'assemblage
  • Les tests de recette, qui remplacent les tests unitairesrecette remplace unitaire

Réponse : Les tests unitaires vérifient chaque morceau ISOLÉMENT ; ils ne disent rien de leur ASSEMBLAGE. Deux fonctions correctes séparément peuvent échouer ensemble — unités incompatibles (Mars Climate Orbiter), format d'échange mal accordé, hypothèses divergentes. C'est le rôle des tests d'INTÉGRATION. Les niveaux se complètent et ne se remplacent pas : unitaire (le morceau), intégration (l'assemblage), recette (le besoin du client). Passer l'un ne dispense pas des autres.

Jeux de tests et cas limites

Un jeu de tests est l'ensemble des cas qu'on éprouve. Sa qualité ne tient pas au nombre, mais au choix des cas. Un bon jeu couvre trois familles :

La règle empirique, vérifiée par l'expérience : les bugs se cachent aux frontières, pas au milieu. Un développeur qui n'écrit que des tests nominaux passe précisément à côté des cas où le code se trompe. Chercher activement « qu'est-ce qui pourrait mal tourner ? » est le bon état d'esprit — l'inverse de celui qui écrit le code.

Frameworks, couverture, non-régression

En pratique, on n'écrit pas le mécanisme de test à la main : un framework le fournit — JUnit en Java, pytest en Python. Il offre des assertions (assertEqual(obtenu, attendu)), exécute tous les tests d'une commande, et produit un rapport clair : combien passent, lesquels échouent, et pourquoi. Écrire un test devient aussi rapide qu'écrire la fonction.

La couverture mesure quelle part du code les tests exécutent (quelles lignes, quelles branches). Utile pour repérer le code jamais testé — mais un piège si on en fait un but : 100 % de couverture ne prouve pas l'absence de bug. Dans l'exemple bissextile, tester 2020 et 2021 exécute toutes les lignes de la version buguée sans jamais révéler l'erreur des siècles. La couverture dit ce qui est exercé, pas ce qui est correct.

Enfin, le test de non-régression est le plus précieux pour un logiciel qui dure : rejouer automatiquement tous les tests après chaque modification, pour garantir qu'on n'a pas re-cassé ce qui marchait. C'est lui qui rend le changement sûr — et donc qui rend possibles le refactoring (chapitre 7) et la maintenance (chapitre 1). Sans tests de non-régression, tout logiciel qui vit finit par se figer, chacun ayant peur d'y toucher.

Quiz · 1 question

Une équipe atteint 100 % de couverture de code par ses tests. Peut-elle en conclure que son logiciel est sans bug ?

  • Oui : 100 % de couverture signifie que tout le code est testé et correctcouverture = correction
  • Non : la couverture dit quelles lignes sont EXÉCUTÉES par les tests, pas si les cas testés sont les bons — on peut exécuter tout le code sans tester les cas limites où le bug se cacheexécuté ≠ correct
  • Non, car la couverture à 100 % est techniquement impossibleimpossible

Réponse : La couverture mesure ce qui est EXÉCUTÉ par les tests, pas ce qui est CORRECT. On peut atteindre 100 % en exécutant toutes les lignes sans jamais éprouver les cas limites : tester la fonction bissextile sur 2020 et 2021 couvre toutes ses lignes tout en manquant l'erreur des siècles. La couverture est utile pour repérer le code jamais testé, mais en faire un but donne une fausse assurance. La qualité d'un jeu de tests tient au CHOIX des cas (nominal, limites, erreur), pas au pourcentage de lignes touchées.

À vous

Voici l'exercice qui rend les tests crédibles — celui que le cours recommande de faire vivre plutôt que de raconter. Un collègue « simplifie » une fonction bissextile et introduit, sans le voir, une régression sur les siècles. Sans tests, ce bug part en production.

À vous d'écrire les tests — cas limites compris — qui l'attrapent avant la livraison. Vous constaterez de vos yeux la différence entre « ça marche chez moi » (deux cas nominaux) et un vrai jeu de tests qui débusque l'erreur là où elle se cache.

Exercice de code

Un collègue « simplifie » une fonction bissextile et introduit une régression sur les siècles. Écrivez les tests — cas limites compris — qui l'attrapent avant la production. Constatez de vos yeux pourquoi « ça marche chez moi » ne suffit pas, et ce qu'est un test de non-régression.

Point de départ

// La fonction ORIGINALE, correcte : une année est bissextile si elle est
// divisible par 4, SAUF les siècles non divisibles par 400 (1900 non, 2000 oui).
function estBissextileOriginale(a) {
  if (a % 400 === 0) return true;
  if (a % 100 === 0) return false;
  return a % 4 === 0;
}

// Un collègue "simplifie" la fonction, persuadé que « ça marche » — il a
// testé sur 2020 (vrai) et 2021 (faux), ça passe, il livre.
function estBissextileModifiee(a) {
  return a % 4 === 0;   // RÉGRESSION : oublie la règle des siècles !
}

// ── À VOUS : écrire des tests qui ATTRAPENT la régression ───────────────────
// Un bon jeu de tests couvre le cas nominal ET les CAS LIMITES. Ici, les cas
// limites sont les siècles : 1900 (non bissextile), 2000 (bissextile).
// Complétez CAS avec des couples [annee, resultatAttendu] qui révèlent le bug.
const CAS = [
  [2020, true],
  [2021, false],
  // à compléter : ajoutez les cas limites qui distinguent les deux versions
];

// Le "harnais" de test : compare chaque version à l'attendu.
function tester(fn, nom) {
  let echecs = 0;
  for (const [a, attendu] of CAS) {
    const obtenu = fn(a);
    if (obtenu !== attendu) { echecs++; console.log("   ✗ " + nom + "(" + a + ") = " + obtenu + ", attendu " + attendu); }
  }
  console.log(nom + " : " + (CAS.length - echecs) + "/" + CAS.length + " tests passés");
  return echecs === 0;
}

console.log("Tests sur la version originale :");
tester(estBissextileOriginale, "originale");
console.log("\nMêmes tests sur la version modifiée :");
const okModif = tester(estBissextileModifiee, "modifiée");
console.log("\n" + (okModif
  ? "⚠ Vos tests ne détectent PAS la régression : ajoutez les cas limites (siècles)."
  : "✓ Vos tests ATTRAPENT la régression — elle n'atteindra pas la production."));

Solution

const CAS = [
  [2020, true],    // divisible par 4 : bissextile (cas nominal)
  [2021, false],   // non divisible par 4 : non (cas nominal)
  [1900, false],   // SIÈCLE non divisible par 400 : NON bissextile  <- cas limite
  [2000, true],    // siècle divisible par 400 : bissextile          <- cas limite
  [2100, false],   // autre siècle non /400 : non                    <- cas limite
];
// Sur la version originale : 5/5 passés.
// Sur la version modifiée : 1900 et 2100 échouent (elle les croit bissextiles).
// -> les tests ATTRAPENT la régression.

// ── Ce que l'exercice enseigne ──────────────────────────────────────────────
//
// 1. « ÇA MARCHE CHEZ MOI » NE SUFFIT PAS. Le collègue a testé 2020 et 2021,
//    a vu « ça marche », et a livré un bug. Deux essais bien choisis ne
//    couvrent pas le comportement — il faut les CAS LIMITES, là où la logique
//    est subtile (ici, la règle des siècles).
//
// 2. LES TESTS SONT UN FILET. Sans eux, la régression part en production et
//    c'est l'utilisateur qui la découvre (au coût 50-100×, chapitre 2). Avec
//    eux, elle est attrapée AVANT la livraison, en une seconde. C'est
//    exactement pourquoi on ne refactore jamais sans tests (chapitre 7).
//
// 3. UN BON JEU DE TESTS couvre : le cas nominal, les CAS LIMITES (frontières,
//    zéro, valeurs spéciales) et les cas d'ERREUR. Les bugs se cachent aux
//    frontières, pas au milieu.
//
// 4. TEST DE NON-RÉGRESSION : rejouer automatiquement TOUS les anciens tests
//    après chaque modification, pour garantir qu'on n'a pas re-cassé ce qui
//    marchait. C'est ce qui rend le changement SÛR sur la durée — le socle de
//    la maintenance (chapitre 1).
//
// 5. En vrai, on n'écrit pas le harnais à la main : un FRAMEWORK (JUnit en
//    Java, pytest en Python) le fournit — assertions, exécution, rapport — et
//    mesure la COUVERTURE (quelle part du code les tests exercent). Mais 100 %
//    de couverture ne prouve pas l'absence de bug : ici, couvrir 2020/2021
//    exécute toutes les lignes de la version modifiée sans voir l'erreur.

Ce que la suite en fait

Les tests protègent votre code des régressions. Reste l'autre grand risque d'un projet d'équipe : se marcher dessus à plusieurs sur le même code. C'est ce que règle la gestion de versions.

Le chapitre 9 traite Git — comment plusieurs personnes modifient le même projet sans se détruire le travail, comment revenir en arrière, et comment résoudre calmement le conflit qui survient inévitablement quand deux coéquipiers touchent la même ligne. Comme pour les tests, sa valeur s'éprouve : vous provoquerez un conflit pour apprendre à le résoudre à froid, avant de le rencontrer en pleine échéance de projet.

À retenir

Flashcards · 4 cartes

Pourquoi « ça marche chez moi » ne suffit-il pas comme test ?
Parce qu'on teste alors seulement le CAS NOMINAL (celui auquel on pensait) et une seule fois, à la main. On oublie les CAS LIMITES où les bugs se cachent (les siècles pour une fonction bissextile), et on ne revérifie pas après modification. Deux essais bien choisis ne couvrent pas un comportement. Un bug non testé part en production, au coût 50-100× (chapitre 2) : les tests rapprochent la détection du moment de l'erreur.
Quels sont les trois niveaux de tests, et pourquoi se complètent-ils ?
UNITAIRE (une fonction/classe isolée : est-elle correcte ?), INTÉGRATION (plusieurs modules ensemble : s'assemblent-ils bien ?), RECETTE (le logiciel entier vu du client : fait-il ce qui était demandé ?). Ils se complètent : des modules corrects séparément peuvent échouer ensemble (intégration), et un logiciel bien intégré peut ne pas répondre au besoin (recette, qui ferme la boucle avec le cahier des charges).
Que doit couvrir un bon jeu de tests, et où se cachent les bugs ?
Trois familles : le cas NOMINAL (usage normal), les CAS LIMITES (frontières : zéro, max, liste vide, premier/dernier, valeurs spéciales) et les cas d'ERREUR (entrées invalides). Règle empirique : les bugs se cachent aux FRONTIÈRES, pas au milieu. La qualité d'un jeu de tests tient au choix des cas, pas à leur nombre — chercher « qu'est-ce qui pourrait mal tourner ? ».
Que mesure la couverture, et qu'est-ce qu'un test de non-régression ?
La COUVERTURE mesure quelle part du code les tests EXÉCUTENT — utile pour repérer le code jamais testé, mais 100 % ne prouve pas l'absence de bug (exécuté ≠ correct : on peut couvrir toutes les lignes sans tester les cas limites). Le test de NON-RÉGRESSION rejoue automatiquement tous les tests après chaque modification, pour garantir qu'on n'a pas re-cassé ce qui marchait : c'est lui qui rend le changement sûr, donc le refactoring et la maintenance possibles.