Lesson 3/9 9 min

Arrange-Act-Assert

The universal grammar of a test: arrange, act, assert. And the rule that separates a good test from a bad one: test behavior, not implementation.

Le test que personne ne comprend à 2h du mat

Il est 2h du matin. Un test vient de passer au rouge en CI. Tu ouvres le fichier, et tu tombes sur ça :

test('panier', () => {
  expect(totalPanier([{prix:10,quantite:2},{prix:5,quantite:1}])).toBe(25);
});

Tout sur une ligne, aucun commentaire, les données noyées dans l'appel. Tu dois comprendre : qu'est-ce qu'on teste ? Pourquoi 25 ? Qu'est-ce qui a cassé ? Ça te prend trois minutes rien que pour démêler ce que le test veut vérifier. Et cinq de plus pour trouver pourquoi il échoue.

Maintenant imagine que ce test était structuré. Qu'en un coup d'œil tu voyais : voilà les données de départ, voilà ce qu'on appelle, voilà ce qu'on attendait. Tu aurais compris en dix secondes. C'est exactement ce que donne la structure Arrange-Act-Assert.

Quand un test échoue à 2h du mat, tu dois le comprendre en 3 secondes. Un test illisible n'est pas seulement inconfortable : il te coûte du temps à chaque régression, et il décourage de maintenir la suite de tests. Un test bien structuré se lit comme une phrase : « avec ces données, quand j'appelle ça, j'attends ça. »

Les 3 actes : préparer, agir, vérifier

AAA, c'est la grammaire universelle d'un test. Trois blocs, toujours dans le même ordre, séparés par une ligne vide ou un commentaire :

  • Arrange : prépare le terrain. Tu crées les données, tu instancies les objets, tu configures le contexte. Rien n'est encore appelé.
  • Act : effectue l'action. Tu appelles une seule fonction, une seule fois. C'est le cœur du test.
  • Assert : vérifie le résultat. Tu compares la sortie à ce que tu attendais. Si ça ne colle pas → rouge.

La métaphore juste : c'est les trois temps d'une expérience scientifique. Tu prépares tes éprouvettes (Arrange), tu ajoutes le réactif (Act), tu observes la couleur (Assert). Ou les trois actes d'une pièce de théâtre : on installe le décor, le personnage agit, on voit les conséquences.

Voici le même test, celui qui était illisible plus haut, réécrit en AAA :

test('additionne prix × quantité de chaque article', () => {
  // Arrange
  const panier = [
    { prix: 10, quantite: 2 },
    { prix: 5,  quantite: 1 },
  ];

  // Act
  const total = totalPanier(panier);

  // Assert
  expect(total).toBe(25);
});

Même logique, même résultat. Mais maintenant le test raconte quelque chose. En cas d'échec, tu vois immédiatement les données d'entrée, ce qu'on testait, et l'écart entre le reçu et l'attendu. Zéro ambiguïté.

Règle d'or AAA : un seul appel de fonction dans le bloc Act. Si tu en mets deux, tu ne sais plus lequel a causé l'échec. Un test = une action = un résultat à vérifier.

Les 3 zones d'un test AAA : Arrange (bleu, prépare les données), Act (orange, appelle la fonction), Assert (vert, vérifie le résultat). Arrange Prépare les données const panier = [ {prix:10, quantite:2}, {prix:5, quantite:1}, ]; Act Appelle la fonction const total = totalPanier(panier); Assert Vérifie le résultat expect(total) .toBe(25);
Trois blocs, trois couleurs, une seule lecture possible. Le test devient une phrase : « avec ce panier, quand je calcule le total, j'attends 25. »

À toi : trouve le bug dans le panier

Le code ci-dessous calcule le total d'un panier… mais il y a un bug. Les tests sont déjà en AAA et ils t'expliquent exactement ce qu'on attend. Lance-les, lis le message d'erreur, corrige le code, relance jusqu'au vert.

Prédis avant de lancer

Le test attend 25 pour un panier [{prix:10,quantite:2},{prix:5,quantite:1}]. Avec le code qui fait total += article.prix, que va-t-il calculer, et pourquoi le test passe-t-il au rouge ? Écris ta réponse avant de dérouler.

Voir la réponse

Le code additionne article.prix sans tenir compte de la quantité : 10 + 5 = 15. Le test attendait 25 (car 10×2 + 5×1 = 25). La sortie de la fonction est 15, l'attendu est 25 → le test passe au rouge avec « attendu 25, reçu 15 ». Le fix : multiplier par article.quantite.

🧪 Labo de test · édite le code, lance, observe
Code à tester
Tests (en AAA)
Bloqué sur la correction ? Voir le fix

Remplace total += article.prix par total += article.prix * article.quantite. La boucle devient : 10×2 = 20, puis 20 + 5×1 = 25. Relance : le test passe au vert. Tu viens de voir AAA à l'œuvre : le bloc Arrange t'a dit exactement quelles données créer le bug, le bloc Assert t'a dit exactement quel résultat était attendu : trouver le fix était presque mécanique.

Quand tu demandes à l'IA d'écrire un test, exige toujours la structure AAA avec des commentaires. Un test sans ces trois blocs identifiés clairement est un test que tu vas regretter de relire.

Tester le comportement, pas l'implémentation

AAA te donne la forme. Maintenant, la règle qui détermine ce que tu mets dans le bloc Assert.

Un test doit se comporter comme une boîte noire : tu fournis des entrées (Arrange), tu déclenches la fonction (Act), et tu regardes ce qui sort (Assert). Tu ne regardes pas ce qui se passe à l'intérieur de la fonction : ses variables locales, son état interne, son algorithme.

Exemple : imagine que tu renommes la variable interne total en somme dans totalPanier. Le comportement observable n'a pas changé : la fonction renvoie toujours le même résultat. Un bon test reste vert. Un mauvais test, lui, accède à panier.totalCalculé (un état interne imaginaire) et casse alors que rien n'a vraiment bougé.

// ❌ Mauvais : couplé à un détail interne
expect(panier._lignesInternes.length).toBe(2);

// ✅ Bon : teste le comportement observable (la sortie)
expect(calculerTotal(panier)).toBe(25);

Tests fragiles = tests couplés à l'implémentation. Si renommer une variable privée, changer une structure de données interne ou réorganiser une boucle casse un test, ce test est un frein à tout refactoring. Il punit le code propre. Un test ne doit dépendre que des entrées et des sorties de ta fonction, jamais de ses rouages internes.

La règle est simple à mémoriser : dans le bloc Assert, tu ne mets que des choses qu'un utilisateur extérieur de ta fonction pourrait observer. La valeur retournée, les effets de bord visibles (un message envoyé, un fichier créé). Pas la façon dont c'est calculé en coulisses.

Pourquoi c'est crucial avec l'IA : quand tu demandes à l'IA d'écrire des tests, elle a tendance à tester ses propres structures internes. C'est ce qu'elle connaît, puisqu'elle vient d'écrire le code. Vérifie toujours que ses assertions portent sur des sorties, pas sur des variables qu'on pourrait rebaptiser demain.

The test nobody understands at 2 a.m.

It's 2 a.m. A test just turned red in CI. You open the file and see this:

test('cart', () => {
  expect(cartTotal([{price:10,quantity:2},{price:5,quantity:1}])).toBe(25);
});

Everything on one line, no comments, data buried inside the call. You need to figure out: what exactly are we testing? Why 25? What broke? It takes you three minutes just to untangle what the test wants to check — and another five to find why it fails.

Now imagine the test had been structured. That at a glance you could see: here's the starting data, here's what we're calling, here's what we expected. Ten seconds to understand. That's exactly what Arrange-Act-Assert gives you.

When a test fails at 2 a.m., you need to understand it in 3 seconds. An unreadable test isn't just uncomfortable: it costs time on every regression, and it discourages keeping the test suite alive. A well-structured test reads like a sentence: "with this data, when I call this, I expect that."

The 3 acts: arrange, act, assert

AAA is the universal grammar of a test. Three blocks, always in the same order, separated by a blank line or a comment:

  • Arrange — set the stage: create the data, instantiate objects, configure the context. Nothing is called yet.
  • Act — perform the action: call one single function, once. This is the heart of the test.
  • Assert — check the result: compare the output to what you expected. If they don't match → red.

The right metaphor: it's the three stages of a scientific experiment. You prepare your test tubes (Arrange), you add the reagent (Act), you observe the colour (Assert). Or the three acts of a play: you set the scene, the character acts, you see the consequences.

Here's the same test — unreadable above — rewritten in AAA:

test('sums price × quantity of each item', () => {
  // Arrange
  const cart = [
    { price: 10, quantity: 2 },
    { price: 5,  quantity: 1 },
  ];

  // Act
  const total = cartTotal(cart);

  // Assert
  expect(total).toBe(25);
});

Same logic, same result. But now the test tells a story. When it fails, you immediately see the inputs, what was being tested, and the gap between received and expected. Zero ambiguity.

AAA golden rule: only one function call in the Act block. If you put two, you no longer know which one caused the failure. One test = one action = one result to verify.

The 3 zones of an AAA test: Arrange (blue, prepares data), Act (orange, calls the function), Assert (green, checks the result). Arrange Prepare the data const cart = [ {price:10, quantity:2}, {price:5, quantity:1}, ]; Act Call the function const total = cartTotal(cart); Assert Check the result expect(total) .toBe(25);
Three blocks, three colours, one unambiguous reading. The test becomes a sentence: "with this cart, when I calculate the total, I expect 25."

Your turn: find the bug in the cart

The code below calculates a cart total… but there's a bug. The tests are already in AAA and tell you exactly what's expected. Run them, read the error, fix the code, rerun until green.

Predict before running

The test expects 25 for a cart [{price:10,quantity:2},{price:5,quantity:1}]. With the code doing total += item.price, what will it compute — and why does the test go red? Write your answer before expanding.

Show the answer

The code adds item.price without considering quantity: 10 + 5 = 15. The test expected 25 (because 10×2 + 5×1 = 25). The function output is 15, the expected is 25 → the test goes red with "expected 25, received 15". The fix: multiply by item.quantity.

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

Replace total += item.price with total += item.price * item.quantity. The loop becomes: 10×2 = 20, then 20 + 5×1 = 25. Rerun: the test goes green. You've just seen AAA in action: the Arrange block told you exactly what data to trace, the Assert block told you exactly what result was expected — finding the fix was almost mechanical.

A tip: when you ask the AI to write a test, always ask for AAA structure with comments. A test without those three clearly labelled blocks is a test you'll regret rereading.

Test behavior, not implementation

AAA gives you the shape. Now, the rule that determines what you put in the Assert block.

A test should behave like a black box: you provide inputs (Arrange), you trigger the function (Act), and you look at what comes out (Assert). You do not look at what happens inside the function — its local variables, its internal state, its algorithm.

Example: imagine you rename the internal variable total to sum inside cartTotal. The observable behaviour hasn't changed — the function still returns the same result. A good test stays green. A bad test, however, accesses cart._internalLines (some imaginary internal state) and breaks even though nothing really changed.

// ❌ Bad: coupled to an internal detail
expect(cart._internalLines.length).toBe(2);

// ✅ Good: tests observable behavior (the output)
expect(calculateTotal(cart)).toBe(25);

Fragile tests = tests coupled to implementation. If renaming a private variable, changing an internal data structure, or reorganising a loop breaks a test, that test is a barrier to all refactoring. It punishes clean code. A test should only depend on the inputs and outputs of your function — never on its internal workings.

The rule is easy to remember: in the Assert block, only put things an outside user of your function could observe. The returned value, visible side-effects (a message sent, a file created). Not how it's calculated behind the scenes.

Why this matters with AI: when you ask the AI to write tests, it tends to test its own internal structures — what it knows, since it just wrote the code. Always check that its assertions target outputs, not variables that could be renamed tomorrow.

🎯 Pratique

S'entraîner (clique pour ouvrir) :

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

Décris les 3 temps d'un test AAA avec tes mots, et donne un exemple d'une ligne pour chacun.

Une bonne explication couvre les trois temps : Arrange = préparer les données et le contexte (ex. const panier = [...]) ; Act = appeler la fonction testée une seule fois (ex. const total = totalPanier(panier)) ; Assert = comparer le résultat à l'attendu (ex. expect(total).toBe(25)). Le tout dans cet ordre, un seul appel dans Act.
🧠 Rappel libre
Rappel libre

Sans remonter : pourquoi un test qui lit une variable interne (ex. obj._cache) est-il fragile ?

Il dépend d'un détail d'implémentation : si tu renommes ou supprimes _cache lors d'un refactoring, le test casse, alors que le comportement observable (ce que la fonction retourne) n'a pas changé. On teste les sorties, pas les rouages. Un test fragile punit le code propre et décourage les améliorations.
⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

L'IA écrit ce test : test('ça marche', () => { const p = creerPanier(); p.articles.push({prix:10}); expect(p.articles[0].prix).toBe(10); }). Tu acceptes, ou tu rejettes ?

À rejeter (ou au moins à considérer très faible) : ce test ne teste pas un comportement de ton code. Il vérifie juste que push de JavaScript fonctionne, et lit une structure interne (articles). Il ne respecte pas AAA proprement (rien n'est "agi" sur ta logique métier) et se couple à la forme interne du panier. Un bon test appellerait une fonction métier (par exemple ajouterAuPanier ou calculerTotal) et vérifierait son résultat.
Que veut dire AAA ?
Dans un test AAA, quelle ligne appelle la fonction testée ?
Pourquoi tester le comportement plutôt que l'implémentation ?
Un test vérifie panier._lignesInternes.length === 2 au lieu de compterArticles(panier) === 2. Quel est le problème ?
Next step

You now know how to structure a test. But testing everything is a mistake. In lesson 4, we look at what to test and what to skip: testing everything wastes time, testing nothing is dangerous. The rule for finding the right balance.

Lesson 4: What to test, what to skip →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement