Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 321 minIntermédiaire

Écrire un bon test unitaire : AAA, nommage et cas à tester

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • structurer un test en trois temps : préparer, agir, vérifier ;
  • nommer un test pour qu'il se lise comme une phrase ;
  • rendre un code testable en sortant ses dépendances cachées ;
  • trouver les cas à tester avec les classes d'équivalence et les valeurs limites ;
  • construire une table de décision pour des règles combinées.

5 exercices corrigés3 schémas

Un bon test unitaire vérifie un seul comportement d'une unité de code, dans une situation précise, avec un nom qui se lit comme une phrase et une structure en trois temps : préparer (Arrange), agir (Act), vérifier (Assert). Écrire le test est la partie facile ; la vraie compétence consiste à choisir les cas qui ont le plus de chances de révéler un défaut. Ce chapitre, valable dans tous les langages, donne les règles d'écriture et les techniques pour trouver les cas : classes d'équivalence, valeurs limites et tables de décision.

Qu'est-ce qu'une « unité » ?

L'unité est le plus petit morceau de code qui a un comportement observable : une fonction, une méthode, parfois une classe entière quand ses méthodes n'ont de sens qu'ensemble. Un test unitaire l'appelle par son interface publique (ce que le reste du programme utilise) et vérifie ce qu'elle renvoie ou ce qu'elle change, sans regarder comment elle s'y prend.

Un test unitaire vérifie…Un test unitaire ne vérifie pas…
Ce qu'une fonction renvoie pour des entrées donnéesLe nom des variables internes, l'ordre des instructions
L'état d'un objet après un appel de méthodeUne méthode privée (elle est testée à travers les méthodes publiques qui l'utilisent)
L'exception levée pour une entrée invalideLe code du framework ou de la bibliothèque (Express, React, PDO sont déjà testés)
Les appels faits à une dépendance, quand c'est le comportement attendu (envoyer un courriel)Des getters et setters sans logique

La structure AAA : préparer, agir, vérifier

Tout test unitaire, dans tous les langages, suit le même schéma en trois temps.

1. Arrange : prépareraujourdhui = '2027-03-08'PC-07 est réservé du 10 au 14 marsla demande porte sur PC-07, du 12 au 18 mars2. Act : agir, une seule foisresultat = verifierReservation(demande, …)3. Assert : vérifierresultat.acceptee vaut falsele motif cite les dates 10/03/2027 et 14/03/2027l'attendu est calculé à la main, à partir de la règleEn langage BDDÉtant donné…Quand…Alors…
Un test unitaire en trois temps : préparer, agir, vérifier
js
test('refuse un matériel déjà réservé sur une partie de la période', () => {
  // Arrange : le contexte et les données
  const aujourdhui = '2027-03-08';
  const reservations = [{ materiel: 'PC-07', debut: '2027-03-10', fin: '2027-03-14' }];
  const demande = { materiel: 'PC-07', debut: '2027-03-12', fin: '2027-03-18' };

  // Act : un seul appel au code testé
  const resultat = verifierReservation(demande, reservations, aujourdhui);

  // Assert : ce qu'on attend
  expect(resultat.acceptee).toBe(false);
  expect(resultat.motif).toBe('PC-07 est déjà réservé du 10/03/2027 au 14/03/2027.');
});

Trois règles d'usage :

  • Une seule action dans la partie Act. Si tu appelles deux fois le code testé avec des données différentes, ce sont deux tests.
  • Pas de logique dans un test : ni if, ni boucle, ni calcul de l'attendu. Un test qui contient un if peut lui-même être faux. Pour plusieurs jeux de données, on utilise un test paramétré (test.each, [Theory], #[DataProvider]).
  • Les commentaires // Arrange, // Act, // Assert sont utiles quand on débute ; une ligne vide entre les trois parties suffit ensuite.

Le même découpage s'appelle Étant donné / Quand / Alors (Given / When / Then) dans le développement piloté par le comportement (BDD) et dans les critères d'acceptation des user stories. C'est la même idée : un contexte, un événement, un résultat observable.

Nommer un test pour qu'il se lise comme une phrase

Quand un test échoue, son nom est la première chose que tu lis, souvent dans un journal de CI, des semaines après l'avoir écrit. Il doit dire la situation et le résultat attendu, sans qu'on ait besoin d'ouvrir le code.

Nom faiblePourquoiNom clair
test1Ne dit rienrefuse un prêt de 16 jours
verifierReservation fonctionneQuel cas ? Quel résultat ?accepte un matériel libre sur toute la période
test dateTrop vaguerefuse une date de début passée
cas limiteQuelle limite ?accepte un prêt de 15 jours, la durée maximale

Les conventions varient selon les langages, l'idée reste la même :

LangageConvention couranteExemple
JavaScript (Vitest, Jest)Une phrase dans describe et test : « verifierReservation › refuse un prêt de 16 jours »describe('verifierReservation', () => { test('refuse un prêt de 16 jours', …) })
C# (xUnit)Methode_Situation_ResultatAttenduVerifier_DureeDeSeizeJours_Refuse
PHP (PHPUnit)Une méthode test… ou marquée #[Test], en camelCaserefuseUnPretDeSeizeJours
Java (JUnit, sujets d'E7)Une méthode marquée @Test, souvent testMethodeSituationtestPossedeDroitTableNonAutorisee

Un test, un comportement

Un test vérifie un comportement, ce qui ne veut pas dire une seule ligne expect. Vérifier qu'une réservation refusée a acceptee à false et le bon motif, c'est vérifier un seul comportement (le refus pour chevauchement) sous deux angles. En revanche, ceci mélange trois comportements :

js
// À éviter : trois comportements dans un test
test('verifierReservation', () => {
  expect(verifierReservation(demandeLibre, reservations, aujourdhui).acceptee).toBe(true);
  expect(verifierReservation(demandeEnConflit, reservations, aujourdhui).acceptee).toBe(false);
  expect(verifierReservation(demandeTropLongue, reservations, aujourdhui).acceptee).toBe(false);
});

Si la deuxième assertion échoue, la troisième n'est jamais exécutée : tu ne sais pas si la règle de durée marche. Et le nom ne dit pas lequel des trois cas a cassé. Trois tests, trois noms, trois diagnostics.

Les qualités d'un bon test : FIRST

LettreQualitéCe qu'elle exigeCe qui la viole
FFast, rapideQuelques millisecondes : tu dois pouvoir lancer toute la suite à chaque modificationUn test unitaire qui se connecte à MySQL
IIndependent, indépendantChaque test prépare ses propres données et ne dépend ni de l'ordre ni des autresUn test qui suppose qu'un autre a déjà créé la réservation n° 2
RRepeatable, reproductibleMême résultat sur ton poste, celui d'un collègue et dans la CI, aujourd'hui et dans un anUn test qui utilise la date du jour réelle, ou un service sur Internet
SSelf-validating, auto-vérifiantVert ou rouge, sans qu'un humain lise une sortieUn test qui fait console.log du résultat
TTimely, écrit au bon momentÉcrit avec le code, ou juste avant (TDD)Des tests écrits six mois après, pour « faire de la couverture »

Rendre le code testable : sortir les dépendances cachées

Imagine que verifierReservation lise elle-même la date du jour :

js
// Difficile à tester : la date du jour est cachée dans la fonction
export function verifierReservation(demande, reservations) {
  const aujourdhui = new Date().toISOString().slice(0, 10);
  // …
}

Le test « accepte une réservation qui commence aujourd'hui » devrait changer de date chaque jour, et le test « refuse une date de début passée » deviendrait faux au fil du temps. La solution est de passer la dépendance en paramètre : la fonction reçoit aujourdhui, l'application lui donne la vraie date, le test lui donne une date fixe.

js
// Facile à tester : la date du jour est une entrée comme les autres
export function verifierReservation(demande, reservations, aujourdhui) { /* … */ }

C'est la même idée que l'injection de dépendances en programmation objet (injection de dépendances en C#) : une classe reçoit ses dépendances (l'horloge, la base, la messagerie) au lieu de les créer. Les dépendances qu'il faut sortir ainsi sont toujours les mêmes : l'heure, le hasard, la base de données, le réseau, le système de fichiers, les variables d'environnement. Le chapitre 7 montre comment les remplacer par des doublures quand on ne peut pas simplement passer une valeur.

Choisir les cas : les classes d'équivalence

Puisqu'on ne peut pas tout tester, on découpe les entrées possibles en classes d'équivalence : des groupes de valeurs que le programme doit traiter de la même façon. Si le programme marche pour une valeur d'une classe, il marche très probablement pour toutes les autres. On teste alors une valeur par classe, valide ou invalide.

Pour la durée d'un prêt, la règle « un prêt dure au plus 15 jours, premier et dernier jour compris » donne trois classes :

ClasseDuréeRésultat attenduUne valeur représentative
Fin avant le début0 jour ou moinsRefus : « La date de fin précède la date de début. »Du 20 au 19 mars
Durée valideDe 1 à 15 joursAccepté (si le reste est bon)Du 15 au 18 mars (4 jours)
Trop long16 jours ou plusRefus : « Un prêt dure au plus 15 jours. »Du 15 mars au 10 avril (27 jours)

Choisir les cas : les valeurs limites

Les défauts se cachent aux frontières entre les classes : un < à la place d'un <=, un + 1 oublié. L'analyse des valeurs limites consiste à tester, pour chaque frontière, la dernière valeur d'une classe et la première de la suivante.

fin avant débutdurée valide : acceptéetrop long : refuséjours01234567891011121314151617du 20 au 19 mars0 jourrefusédu 20 au 20 mars1 jouracceptédu 15 au 29 mars15 joursacceptédu 15 au 30 mars16 joursrefusé
Les classes d'équivalence de la durée d'un prêt et les quatre valeurs limites à tester
FrontièreValeurs à testerDéfaut qu'elles détectent
Entre « fin avant début » et « durée valide »0 jour (fin = veille du début) et 1 jour (fin = début)fin <= debut au lieu de fin < debut : un prêt d'un jour serait refusé
Entre « durée valide » et « trop long »15 jours et 16 jours> 15 remplacé par >= 15, ou un + 1 oublié dans le calcul de la durée

On applique la même méthode à la date de début : hier (refusé), aujourd'hui (accepté), demain (accepté). Le test « aujourd'hui » est celui que l'on oublie le plus souvent, et c'est lui qui trouve le <= à la place du <.

Choisir les cas : les positions de deux périodes

Le chevauchement de deux périodes est un cas d'école. Une réservation existe du 10 au 14 mars ; la nouvelle demande peut se placer de dix façons différentes par rapport à elle. Chacune est une classe d'équivalence, et plusieurs sont des valeurs limites.

mars151014202531PC-07 réservédu 10 au 141 bien avantdu 1er au 82 finit le 9du 5 au 9valeur limite3 finit le 10du 5 au 10valeur limite4 chevauche le débutdu 8 au 125 inclusedu 11 au 136 identiquedu 10 au 147 englobedu 1er au 318 chevauche la findu 12 au 189 commence le 14du 14 au 18valeur limite10 commence le 15du 15 au 20valeur limiteacceptéerefusée
Les dix positions d'une demande par rapport à une réservation existante : les cas 2, 3, 9 et 10 sont les valeurs limites

Les cas 2 et 3, puis 9 et 10, sont les frontières : un jour d'écart fait passer le résultat d'« acceptée » à « refusée ». Le cas 9, « commence le jour où l'autre finit », est exactement celui qui manquait à Prêt'Matos dans l'histoire du chapitre 1.

Choisir les cas : la table de décision

Quand le résultat dépend de plusieurs conditions combinées, on dresse une table de décision : une colonne par combinaison utile, une ligne par condition, puis le résultat attendu. Chaque colonne devient un test.

Prenons la route POST /api/reservations/:id/sortie, qui enregistre la sortie d'un matériel :

ConditionT1T2T3T4
L'appelant a un jeton validenonouiouioui
L'appelant a le rôle « logistique »–nonouioui
La réservation existe––nonoui
Résultat attendu401403404200, sortie enregistrée

Le tiret signifie « peu importe » : sans jeton, la réponse est 401 quel que soit le reste. Quatre combinaisons suffisent là où huit seraient possibles. Les tests T1 et T2 sont des tests de sécurité : ils prouvent que la route refuse ce qu'elle doit refuser.

La liste de contrôle des cas oubliés

Une fois les classes et les limites trouvées, passe en revue cette liste. Chaque ligne a déjà caché un bug dans un projet d'étudiant :

CasPour Prêt'Matos
Zéro, vide, absentAucune réservation existante ; une liste de prêts vide ; un champ materiel absent
Un seul élémentUne seule réservation, un seul prêt à rappeler
Plusieurs élémentsPlusieurs réservations, dont une seule en conflit ; plusieurs prêts dont un échoue à l'envoi
Les limites15 et 16 jours ; aujourd'hui et hier ; périodes contiguës
Le format inattendu14/03/2027 au lieu de 2027-03-14 ; un nombre au lieu d'une chaîne
Les dates piègesFin février, année bissextile, passage d'année, changement d'heure
Les droitsLe bon rôle, le mauvais rôle, pas de jeton, la ressource d'un autre utilisateur
L'erreur d'une dépendanceLa base ne répond pas, le serveur de courriel est injoignable

Exercices

Exercice 1 sur 5 Réécrire un test en AAA Réussi À revoir

Réorganise ce test selon la structure AAA et donne-lui un nom clair.

js
test('test retard', () => {
  expect(joursDeRetard('2027-03-14', '2027-03-17')).toBe(3);
  const r = joursDeRetard('2027-03-14', '2027-03-14');
  expect(r).toBe(0);
});
Voir la réponseMasquer la réponse de l'exercice 1

Le test mélange deux comportements : on le coupe en deux tests, chacun en trois temps.

js
test('compte les jours après le retour prévu', () => {
  const retourPrevu = '2027-03-14';
  const aujourdhui = '2027-03-17';

  const retard = joursDeRetard(retourPrevu, aujourdhui);

  expect(retard).toBe(3);
});

test('vaut 0 le jour du retour prévu', () => {
  const retourPrevu = '2027-03-14';

  const retard = joursDeRetard(retourPrevu, retourPrevu);

  expect(retard).toBe(0);
});

Tu avais trouvé ?

Exercice 2 sur 5 Trouver le cas manquant Réussi À revoir

Un développeur a écrit ces tests pour periodesSeChevauchent, avec la réservation existante du 10 au 14 mars :

DemandeAttendu
du 12 au 18chevauche
du 5 au 10chevauche
du 1er au 31chevauche
du 15 au 20ne chevauche pas
du 1er au 9ne chevauche pas
  1. Quelles positions de la figure des dix positions sont testées ?
  2. Quelle valeur limite manque ? Écris le test correspondant.
  3. Quel défaut ce test aurait-il détecté dans l'histoire du chapitre 1 ?
Voir la réponseMasquer la réponse de l'exercice 2
  1. Les positions 8 (chevauche la fin), 3 (finit le premier jour), 7 (englobe), 10 (commence le lendemain) et 2 (finit la veille).
  2. Il manque la position 9 : une demande qui commence le dernier jour de la réservation (du 14 au 18). C'est la limite symétrique de la position 3, testée à gauche mais pas à droite.
js
test('une période qui commence le dernier jour chevauche', () => {
  const pc07 = { debut: '2027-03-10', fin: '2027-03-14' };

  expect(periodesSeChevauchent(pc07, { debut: '2027-03-14', fin: '2027-03-18' })).toBe(true);
});
  1. Le changement de <= en < dans la seconde moitié de la condition (b.debut <= a.fin). Avec ce test, la modification de Yanis passait au rouge immédiatement. Le chapitre 12 montre qu'un outil peut trouver ce genre de trou automatiquement.

Tu avais trouvé ?

Exercice 3 sur 5 Classes d'équivalence et valeurs limites Réussi À revoir

L'équipe ajoute la prolongation d'un prêt : « Un salarié peut prolonger son prêt d'un nombre de jours compris entre 1 et 7, à condition que la durée totale du prêt ne dépasse pas 15 jours. » Julie a un prêt du 10 au 14 mars (5 jours).

  1. Donne les classes d'équivalence du nombre de jours demandés.
  2. Donne les valeurs limites à tester pour le prêt de Julie, avec le résultat attendu de chacune.
Voir la réponseMasquer la réponse de l'exercice 3
  1. Trois classes selon la première règle : moins de 1 jour (0 ou négatif : refusé), de 1 à 7 jours (possible), plus de 7 jours (refusé). La seconde règle découpe encore la classe du milieu : la durée totale doit rester de 15 jours au plus.
  2. Pour un prêt de 5 jours, la durée totale reste valide tant que la prolongation fait au plus 10 jours : c'est donc la règle des 7 jours qui est la plus restrictive ici.
Jours demandésDurée totaleAttenduPourquoi tester
05refusélimite basse, côté invalide
16acceptélimite basse, côté valide
712acceptélimite haute, côté valide
813refusélimite haute, côté invalide

Pour tester la règle des 15 jours, il faut un autre prêt, plus long : avec un prêt de 10 jours, prolonger de 5 jours donne 15 (accepté) et de 6 jours donne 16 (refusé). Une règle combinée demande des données choisies pour atteindre chaque frontière.

Tu avais trouvé ?

Exercice 4 sur 5 Table de décision Réussi À revoir

La route DELETE /api/reservations/:id annule une réservation. Règles : il faut être connecté (sinon 401) ; la réservation doit exister (sinon 404) ; seul son auteur peut l'annuler, ou un membre de la logistique (sinon 403) ; une réservation dont le matériel est déjà sorti ne peut pas être annulée (409). Sinon, la réponse est 204.

Dresse la table de décision et donne le nombre de tests nécessaires.

Voir la réponseMasquer la réponse de l'exercice 4
ConditionT1T2T3T4T5T6
Connecténonouiouiouiouioui
La réservation existe–nonouiouiouioui
Auteur ou logistique––nonoui (auteur)oui (logistique)oui
Matériel déjà sorti–––nonnonoui
Attendu401404403204204409

Six tests. T4 et T5 donnent le même résultat mais testent deux chemins d'autorisation différents (l'auteur, puis la logistique) : les deux sont nécessaires, car un défaut peut toucher l'un sans l'autre.

Tu avais trouvé ?

Exercice 5 sur 5 Le test qui dépend du jour Réussi À revoir

Ce test passait en mars et échoue en avril. Explique pourquoi et corrige le code et le test.

js
export function estEnRetard(pret) {
  const aujourdhui = new Date().toISOString().slice(0, 10);
  return aujourdhui > pret.fin;
}

test("un prêt qui finit le 30 mars n'est pas en retard", () => {
  expect(estEnRetard({ fin: '2027-03-30' })).toBe(false);
});
Voir la réponseMasquer la réponse de l'exercice 5

La fonction lit la date réelle : le résultat dépend du jour où l'on lance le test. Après le 30 mars 2027, le prêt est en retard et le test échoue, alors que le code n'a pas changé. Le test n'est pas reproductible (le R de FIRST).

On passe la date en paramètre :

js
export function estEnRetard(pret, aujourdhui) {
  return aujourdhui > pret.fin;
}

test("un prêt n'est pas en retard le jour de son retour", () => {
  expect(estEnRetard({ fin: '2027-03-30' }, '2027-03-30')).toBe(false);
});

test('un prêt est en retard le lendemain de son retour', () => {
  expect(estEnRetard({ fin: '2027-03-30' }, '2027-03-31')).toBe(true);
});

La comparaison de chaînes > fonctionne ici parce que le format AAAA-MM-JJ se trie dans l'ordre chronologique. Et toISOString() donne la date en temps universel (UTC) : entre minuit et 1 h ou 2 h du matin à Paris, elle renvoie encore la veille. Encore une raison de ne pas lire l'horloge au cœur d'une règle métier.

Tu avais trouvé ?

Chapitre terminé !

Fais les 5 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Tests unitaires en JavaScript avec Vitest (et Jest)Revoir le sommaire du cours