Jour 10 Day 10 · mardi 11 août 2026 Tuesday 11 August 2026 Qualité Fondamental
Tester son code : de l'unitaire au E2E Testing your code: from unit to E2E
Pyramide de tests, mocks, TDD, couverture : les recruteurs adorent demander « comment testes-tu ton code ? » — voici de quoi répondre avec précision plutôt qu'avec des généralités. Test pyramid, mocks, TDD, coverage: recruiters love asking "how do you test your code?" — here's how to answer with precision instead of generalities.
L’essentiel
Les tests automatisés ont un seul but : détecter les régressions avant les utilisateurs, et le faire assez vite pour qu’on ose modifier le code. Un code sans tests n’est pas « pas encore testé », il est impossible à refactorer sereinement — chaque changement est un pari.
Le modèle mental de référence est la pyramide de tests : beaucoup de tests unitaires (rapides, isolés, précis), moins de tests d’intégration (plusieurs composants ensemble, avec de vraies dépendances), très peu de tests E2E (l’application entière pilotée comme un utilisateur). Plus on monte, plus le test est réaliste — et plus il est lent, cher à maintenir et difficile à diagnostiquer quand il échoue. Un test unitaire qui casse pointe la fonction fautive ; un test E2E qui casse dit juste « quelque chose ne va pas quelque part ».
╱╲
╱E2E╲ peu — lents, réalistes
╱──────╲ (parcours critiques)
╱ Intégra-╲ quelques-uns — vraies
╱ tion ╲ dépendances (DB, API)
╱─────────────╲
╱ Unitaires ╲ beaucoup — rapides,
╱─────────────────╲ isolés, logique métier
💡 La bonne question — jamais « unitaire ou E2E ? » dans l’absolu, mais « quel est le test le plus bas de la pyramide qui attraperait ce bug ? ». Plus c’est bas, plus le feedback est rapide et le diagnostic précis.
En entretien, on n’attend pas d’un stagiaire qu’il récite Kent Beck, mais qu’il sache dire quoi tester à quel niveau et pourquoi.
Comment ça marche
Un bon test unitaire suit le pattern AAA : Arrange (préparer les données et l’objet testé), Act (appeler la fonction), Assert (vérifier le résultat).
import { describe, it, expect } from "vitest"; // API identique à Jest
import { computeTotal } from "./cart";
describe("computeTotal", () => {
it("applique 10 % de remise au-delà de 100 €", () => {
// Arrange — préparer les données
const cart = [{ price: 80 }, { price: 40 }];
// Act — une seule action
const total = computeTotal(cart);
// Assert — vérifier le comportement observable, pas l'interne
expect(total).toBe(108); // 120 € − 10 %
});
});
Trois qualités non négociables :
- Isolation : le test ne dépend ni du réseau, ni de la base, ni de l’horloge, ni d’un autre test. Les tests doivent pouvoir tourner en parallèle et dans n’importe quel ordre.
- Déterminisme : même code = même résultat, à chaque fois. Les ennemis :
Date.now(),Math.random(), les timeouts, l’ordre d’itération non garanti. On injecte le temps et l’aléa comme des dépendances. - Tester le comportement, pas l’implémentation : on vérifie ce que fait la fonction (entrées → sorties, effets observables), pas comment elle le fait. Un refactoring interne ne doit pas casser les tests — sinon ils freinent le changement au lieu de le protéger.
Pour isoler, on remplace les dépendances par des doubles de test — et le vocabulaire précis fait bonne impression :
| Double | Ce qu’il fait | Sert à | Exemple |
|---|---|---|---|
| Stub | renvoie des réponses préprogrammées | alimenter le test en données | getUser renvoie toujours Alice |
| Mock | attend des interactions précises | vérifier un effet de bord | le test échoue si sendEmail n’est pas appelé avec les bons arguments |
| Spy | enregistre les appels sans changer le comportement | inspecter après coup | combien d’appels, avec quels arguments |
| Fake | vraie implémentation, simplifiée | tester sans l’infra | repository en mémoire (Map au lieu de Postgres), plus robuste qu’un mock |
Le TDD (Test-Driven Development) inverse l’ordre d’écriture : red (écrire un test qui échoue), green (le code minimal qui le fait passer), refactor (nettoyer, les tests au vert). Ce que ça apporte vraiment : on définit le comportement attendu avant de coder (ce qui force à clarifier l’API), chaque ligne de code existe pour faire passer un test, et on obtient un filet de sécurité gratuit. Ce que ça n’est pas : une religion — beaucoup d’excellents devs pratiquent un TDD partiel (sur la logique métier complexe, pas sur le code de glue).
Concepts clés à maîtriser
- Tests d’intégration avec de vraies dépendances : mocker un SQL complexe teste surtout votre imagination. Testcontainers lance un vrai Postgres (ou Redis, Kafka…) dans un conteneur Docker jetable le temps de la suite : on teste les vraies requêtes, les vraies contraintes, les vraies transactions. Compromis : quelques secondes de démarrage contre un réalisme incomparable.
- E2E avec Playwright ou Cypress : le navigateur est piloté comme un utilisateur (remplir le formulaire, cliquer, vérifier la page). Playwright s’est imposé : multi-navigateurs, parallélisation native, auto-waiting (il attend que l’élément soit actionnable au lieu d’exiger des
sleep). Réserver le E2E aux parcours critiques : signup, login, checkout — pas à chaque variation de formulaire. - La flakiness E2E : un test E2E échoue parfois sans qu’aucun code n’ait changé — course entre le test et le rendu, données résiduelles, réseau. Antidotes : attentes explicites sur l’état (jamais de
sleep(2000)), isolation des données par test, sélecteurs stables (data-testidplutôt que le CSS). - Couverture de code : le pourcentage de lignes exécutées par les tests. Utile en tendance (la couverture s’effondre = on a arrêté de tester) et pour repérer les zones mortes. Piégeuse en objectif : exécuter une ligne n’est pas la vérifier — un test sans assertion produit 100 % de couverture et 0 % de valeur. Loi de Goodhart : quand la couverture devient la cible, on écrit des tests pour couvrir, pas pour vérifier.
- Tests en CI : les tests n’existent vraiment que s’ils tournent à chaque push et bloquent le merge en cas d’échec. Unitaires sur chaque commit, intégration sur chaque PR, E2E éventuellement sur main ou en nightly s’ils sont lents.
En entretien
🎤 En entretien — « comment testes-tu ton code ? » appelle une réponse structurée par niveaux : unitaires sur la logique métier (AAA, doubles de test), intégration avec de vraies dépendances (Testcontainers), E2E sur les parcours critiques (Playwright), le tout en CI qui bloque le merge. Quatre phrases, question pliée.
« C’est quoi un bon test unitaire ? » — Rapide (millisecondes), isolé (ni réseau, ni base, ni ordre d’exécution), déterministe, structuré en Arrange-Act-Assert, et qui vérifie un comportement observable — pas les détails d’implémentation. Bonus : un bon test échoue pour une seule raison, et son nom dit laquelle.
« Mock vs stub ? » — Le stub fournit des données au test (réponses préprogrammées) ; le mock vérifie des interactions (le test échoue si la méthode attendue n’est pas appelée correctement). En une phrase : stub = état, mock = comportement. Ajouter fake (implémentation simplifiée mais réelle) et spy (enregistre sans modifier) montre que le vocabulaire est maîtrisé.
« Tu pratiques le TDD ? Ça apporte quoi ? » — Réponse honnête : red-green-refactor, pratiqué surtout sur la logique métier non triviale. Apports concrets : l’API se dessine du point de vue de l’appelant, le code est testable par construction, le refactoring est protégé. Dire qu’on ne l’applique pas mécaniquement partout (code de glue, prototypes) est un plus, pas un aveu.
« Comment tester du code qui parle à une base de données ? » — Deux niveaux : la logique métier en unitaire avec un fake du repository ; les requêtes réelles en intégration contre un vrai Postgres lancé par Testcontainers. Mocker le driver SQL est le piège : le test passe, la requête est fausse.
« 100 % de couverture, c’est un bon objectif ? » — Non : la couverture mesure ce qui est exécuté, pas ce qui est vérifié. Viser 100 % pousse à tester du code trivial et à écrire des tests sans assertions. Mieux : une couverture élevée sur la logique métier critique, surveillée en tendance, et des mutations testing si on veut vraiment mesurer la qualité des assertions.
Pièges & idées reçues
⚠️ L’abus de mocks — un test qui mocke cinq dépendances et vérifie chaque appel interne ne teste plus le comportement : il recopie l’implémentation. Le moindre refactoring casse dix tests qui étaient tous verts pour de mauvaises raisons. Préférer les fakes, et tester des unités un peu plus grosses.
- Tester l’implémentation : vérifier qu’une méthode privée est appelée, asserter sur l’état interne… Ces tests gênent le refactoring qu’ils étaient censés permettre.
- Le
sleep()dans les tests E2E : la cause n°1 de flakiness. Toujours attendre une condition (élément visible, requête terminée), jamais une durée. - « Les tests ralentissent le développement » — vrai la première semaine, faux dès le premier refactoring ou le premier bug de régression évité. Le coût réel, c’est la maintenance de mauvais tests (couplés à l’implémentation, flaky).
- La pyramide inversée (le « cornet de glace ») : une suite dominée par des E2E lents et fragiles avec peu d’unitaires — feedback en 40 minutes, échecs inexploitables. Symptôme classique d’un code non testable unitairement.
Pour aller plus loin
- Martin Fowler — The Practical Test Pyramid : l’article de référence, exemples inclus
- Playwright — documentation et Testcontainers pour les tests d’intégration réalistes
- Vitest ou pytest selon votre stack : lire au moins la page sur les fixtures
- Exercice : reprendre un de vos projets, écrire les tests unitaires de la logique métier centrale puis un test d’intégration avec Testcontainers — et regarder combien de bugs remontent
The essentials
Automated tests have one purpose: catching regressions before users do, and fast enough that you dare to change the code. Untested code isn’t “not tested yet”, it’s impossible to refactor with confidence — every change is a gamble.
The reference mental model is the test pyramid: many unit tests (fast, isolated, precise), fewer integration tests (several components together, with real dependencies), very few E2E tests (the whole application driven like a user). The higher you climb, the more realistic the test — and the slower, the more expensive to maintain, and the harder to diagnose when it fails. A broken unit test points at the guilty function; a broken E2E test just says “something is wrong somewhere”.
╱╲
╱E2E╲ few — slow, realistic
╱──────╲ (critical journeys)
╱ Integra-╲ some — real
╱ tion ╲ dependencies (DB, API)
╱─────────────╲
╱ Unit tests ╲ many — fast, isolated,
╱─────────────────╲ business logic
💡 The right question — never “unit or E2E?” in the abstract, but “what is the lowest test in the pyramid that would catch this bug?”. The lower it sits, the faster the feedback and the sharper the diagnosis.
In an interview, nobody expects an intern to recite Kent Beck — but you should be able to say what to test at which level and why.
How it works
A good unit test follows the AAA pattern: Arrange (prepare the data and the object under test), Act (call the function), Assert (check the result).
import { describe, it, expect } from "vitest"; // same API as Jest
import { computeTotal } from "./cart";
describe("computeTotal", () => {
it("applies a 10% discount above €100", () => {
// Arrange — prepare the data
const cart = [{ price: 80 }, { price: 40 }];
// Act — a single action
const total = computeTotal(cart);
// Assert — check observable behavior, not internals
expect(total).toBe(108); // €120 − 10%
});
});
Three non-negotiable qualities:
- Isolation: the test depends neither on the network, nor the database, nor the clock, nor another test. Tests must be able to run in parallel and in any order.
- Determinism: same code = same result, every time. The enemies:
Date.now(),Math.random(), timeouts, unspecified iteration order. Inject time and randomness as dependencies. - Test behavior, not implementation: verify what the function does (inputs → outputs, observable effects), not how it does it. An internal refactoring must not break the tests — otherwise they slow change down instead of protecting it.
To isolate, you replace dependencies with test doubles — and precise vocabulary makes a good impression:
| Double | What it does | Used for | Example |
|---|---|---|---|
| Stub | returns pre-programmed answers | feeding the test with data | getUser always returns Alice |
| Mock | expects precise interactions | verifying a side effect | the test fails if sendEmail isn’t called with the right arguments |
| Spy | records calls without changing behavior | inspecting after the fact | how many calls, with what arguments |
| Fake | a real but simplified implementation | testing without the infra | in-memory repository (Map instead of Postgres), more robust than a mock |
TDD (Test-Driven Development) reverses the writing order: red (write a failing test), green (the minimal code that makes it pass), refactor (clean up with tests green). What it actually brings: you define the expected behavior before coding (which forces you to clarify the API), every line of code exists to make a test pass, and you get a safety net for free. What it isn’t: a religion — plenty of excellent developers practice partial TDD (on complex business logic, not on glue code).
Key concepts to master
- Integration tests with real dependencies: mocking a complex SQL query mostly tests your imagination. Testcontainers starts a real Postgres (or Redis, Kafka…) in a throwaway Docker container for the duration of the suite: you test real queries, real constraints, real transactions. Trade-off: a few seconds of startup for incomparable realism.
- E2E with Playwright or Cypress: the browser is driven like a user (fill the form, click, check the page). Playwright has become the default: multi-browser, native parallelization, auto-waiting (it waits for the element to be actionable instead of requiring
sleepcalls). Reserve E2E for critical journeys: signup, login, checkout — not every form variation. - E2E flakiness: an E2E test sometimes fails with zero code changes — a race between test and rendering, leftover data, network. Antidotes: explicit waits on state (never
sleep(2000)), per-test data isolation, stable selectors (data-testidrather than CSS). - Code coverage: the percentage of lines executed by tests. Useful as a trend (coverage collapsing = testing has stopped) and to spot dead zones. Treacherous as a target: executing a line is not verifying it — an assertion-free test yields 100% coverage and 0% value. Goodhart’s law: when coverage becomes the target, you write tests to cover, not to verify.
- Tests in CI: tests only truly exist if they run on every push and block the merge on failure. Unit tests on every commit, integration on every PR, E2E possibly on main or nightly if they’re slow.
In an interview
🎤 In an interview — “how do you test your code?” calls for an answer structured by level: unit tests on business logic (AAA, test doubles), integration with real dependencies (Testcontainers), E2E on critical journeys (Playwright), all in a CI that blocks the merge. Four sentences, question closed.
“What makes a good unit test?” — Fast (milliseconds), isolated (no network, no database, no execution-order dependency), deterministic, structured as Arrange-Act-Assert, and verifying observable behavior — not implementation details. Bonus: a good test fails for exactly one reason, and its name says which.
“Mock vs stub?” — The stub provides data to the test (pre-programmed answers); the mock verifies interactions (the test fails if the expected method wasn’t called correctly). In one sentence: stub = state, mock = behavior. Adding fake (a simplified but real implementation) and spy (records without altering) shows you own the vocabulary.
“Do you practice TDD? What does it bring?” — Honest answer: red-green-refactor, mostly on non-trivial business logic. Concrete benefits: the API gets designed from the caller’s point of view, the code is testable by construction, refactoring is protected. Saying you don’t apply it mechanically everywhere (glue code, prototypes) is a plus, not a confession.
“How do you test code that talks to a database?” — Two levels: business logic in unit tests with a fake repository; real queries in integration tests against a real Postgres started by Testcontainers. Mocking the SQL driver is the trap: the test passes, the query is wrong.
“Is 100% coverage a good goal?” — No: coverage measures what is executed, not what is verified. Chasing 100% pushes you to test trivial code and write assertion-free tests. Better: high coverage on critical business logic, watched as a trend, and mutation testing if you genuinely want to measure assertion quality.
Pitfalls & misconceptions
⚠️ Mock overuse — a test that mocks five dependencies and verifies every internal call no longer tests behavior: it transcribes the implementation. The smallest refactoring breaks ten tests that were all green for the wrong reasons. Prefer fakes, and test slightly larger units.
- Testing the implementation: verifying that a private method gets called, asserting on internal state… These tests obstruct the very refactoring they were supposed to enable.
sleep()in E2E tests: the number one cause of flakiness. Always wait for a condition (element visible, request finished), never for a duration.- “Tests slow development down” — true the first week, false from the first refactoring or the first regression bug avoided. The real cost is maintaining bad tests (implementation-coupled, flaky).
- The inverted pyramid (the “ice cream cone”): a suite dominated by slow, fragile E2E tests with few unit tests — 40-minute feedback, unusable failures. A classic symptom of code that can’t be unit tested.
Going further
- Martin Fowler — The Practical Test Pyramid: the reference article, examples included
- Playwright — documentation and Testcontainers for realistic integration tests
- Vitest or pytest depending on your stack: read at least the fixtures page
- Exercise: take one of your projects, write unit tests for the core business logic then one integration test with Testcontainers — and count how many bugs surface