01 - La pyramide des tests : unit, intégration, e2e
Ce que tu vas apprendre
- Les trois niveaux de tests et leurs rôles
- Combien de tests a chaque niveau
- Les anti-patterns qui ruinent ta stratégie de test
Prerequisites
Avoir lu l'article 00 - Introduction.
La pyramide de Mike Cohn
En 2009, Mike Cohn a propose une image simple pour organiser ses tests. C'est devenu un classique, et pour une bonne raison : ca marche.
/\
/ \ E2E (10%)
/ \ Lent, fragile, peu nombreux
/------\
/ \ Integration (20%)
/ \ Moderement rapides
/------------\
/ \ Unitaires (70%)
/ \ Rapides, isoles, nombreux
------------------
L'idee est simple : beaucoup de tests unitaires en bas (rapides, pas chers), moins de tests d'intégration au milieu, et tres peu de tests e2e en haut (lents, chers).
Niveau 1 : Tests unitaires
Un test unitaire teste une seule unité de logique, isolee de tout le reste. Pas de base de donnees, pas de réseau, pas de système de fichiers. Juste ta fonction et ses entrees/sorties.
typescriptimport { describe, it, expect } from "bun:test";
function applyDiscount(price: number, percent: number): number {
if (percent < 0 || percent > 100) throw new Error("Invalid discount");
return price * (1 - percent / 100);
}
describe("applyDiscount", () => {
it("applies 20% discount", () => {
expect(applyDiscount(100, 20)).toBe(80);
});
it("throws on negative discount", () => {
expect(() => applyDiscount(100, -5)).toThrow("Invalid discount");
});
});
Vitesse : millisecondes. Tu peux en avoir des milliers et les lancer en 2 secondes.
Quand les utiliser : pour toute logique métier pure. Calculs, validations, transformations de donnees, regles business.
Niveau 2 : Tests d'intégration
Un test d'intégration vérifié que plusieurs composants fonctionnent ensemble. Typiquement : ton code + une base de donnees, ton code + une API externe, ton service + un repository.
typescriptimport { describe, it, expect, beforeEach } from "bun:test";
describe("OrderRepository", () => {
beforeEach(async () => {
await db.query("TRUNCATE orders RESTART IDENTITY CASCADE");
});
it("persists and retrieves an order", async () => {
const repo = new PgOrderRepository(db);
await repo.save(Order.create([{ product: "Biere", qty: 6, price: 3 }]));
const orders = await repo.findAll();
expect(orders).toHaveLength(1);
expect(orders[0].total).toBe(18);
});
});
Vitesse : secondes. Chaque test a besoin d'un setup (connexion DB, nettoyage des donnees).
Quand les utiliser : pour vérifier que tes queries SQL sont correctes, que tes contraintes de base fonctionnent, que tes migrations tournent. Mocker la DB n'attrape pas les vrais bugs SQL.
Niveau 3 : Tests end-to-end (e2e)
Un test e2e simule un vrai utilisateur : il ouvre un navigateur, clique sur des boutons, remplit des formulaires. C'est le test le plus realiste mais aussi le plus lent et le plus fragile.
typescriptimport { test, expect } from "@playwright/test";
test("user completes checkout", async ({ page }) => {
await page.goto("https://paltemps.fr");
await page.getByRole("button", { name: "Ajouter au panier" }).click();
await page.getByRole("link", { name: "Panier" }).click();
await page.getByRole("button", { name: "Commander" }).click();
await expect(page.getByText("Commande confirmee")).toBeVisible();
});
Vitesse : dizaines de secondes par test. Un navigateur se lance, des pages se chargent, des animations se jouent.
Quand les utiliser : pour les parcours utilisateur critiques. Le login, le checkout, l'inscription. Pas pour tester que 2 + 2 = 4.
Ma repartition : 70/20/10
Apres avoir travaille sur plusieurs projets (dont paltemps.fr), voici ce que je recommande :
- 70% unitaires : toute la logique métier, les calculs, les validations, les transformations. C'est le gros du boulot et ca coûte presque rien a exécuter.
- 20% intégration : les repositories, les routes API, les services qui parlent a une DB ou a une API externe.
- 10% e2e : les 5 a 10 parcours les plus critiques de ton app.
Ca ne veut pas dire que tu ecris tes tests dans cet ordre. Souvent, je commence par un test d'intégration pour vérifier qu'une route marche, puis j'extrais la logique dans des fonctions pures que je teste unitairement.
Les anti-patterns
Le cone de glace (ice cream cone)
------------------
\ / E2E (beaucoup)
\ / "On teste tout via le navigateur"
\------------/
\ / Integration (quelques-uns)
\ /
\------/
\ / Unitaires (presque rien)
\ / "Ca sert a rien sans le contexte"
\/
C'est le pattern le plus courant chez les équipes qui decouvrent les tests. Tout passe par Selenium ou Playwright. Les tests prennent 45 minutes. Ils cassent tout le temps a cause d'un changement de CSS. Personne ne les maintient. Au bout de 6 mois, on les désactivé.
Le problème : quand un test e2e echoue, tu ne sais pas ou est le bug. C'est le serveur ? Le frontend ? La DB ? Une race condition ? Tu passes plus de temps a debugger le test que le code.
Le sablier (hourglass)
Beaucoup de tests unitaires, beaucoup de tests e2e, mais rien au milieu. Les tests unitaires passent, les tests e2e echouent, et personne ne comprend pourquoi. Il manque la couche d'intégration qui aurait montre que le repository ne gere pas bien les NULL.
Le trophee (Kent C. Dodds)
Kent C. Dodds propose une alternative a la pyramide : le "Testing Trophy", ou la majorite des tests sont des tests d'intégration. Son argument : les tests d'intégration donnent le meilleur ratio confiance/coût.
Je suis partiellement d'accord. Sur une app full-stack classique, oui. Mais sur un projet avec de la logique métier complexe (calculs financiers, regles business avec plein de cas limites), les tests unitaires restent imbattables. Mon conseil : adapte la repartition a ton projet.
Comment choisir le bon niveau
Pose-toi la question : qu'est-ce que je teste exactement ?
| Ce que tu testes | Niveau | Exemple |
|---|---|---|
| Une formule de calcul | Unitaire | calculateTotal(items) |
| Une validation de donnees | Unitaire | validateEmail(input) |
| Une query SQL | Intégration | OrderRepository.findByDate() |
| Une route API complète | Intégration | GET /api/orders retourne 200 |
| Le flow d'inscription | E2E | Remplir le formulaire, vérifier l'email |
Si la réponse est "une fonction pure sans effet de bord", c'est unitaire. Si ca implique une DB ou un service externe, c'est intégration. Si ca implique un navigateur et un vrai utilisateur, c'est e2e.
Article précédent : 00 - Introduction Article suivant : 02 - Tests unitaires avec bun test