Tests automatisés : la stratégie qui évite les régressions
Tests unitaires, d'intégration, E2E... Comment construire une pyramide de tests efficace sans perdre de temps.
"Ça marchait avant la mise en prod." Cette phrase, je l'ai entendue trop souvent. La solution ? Une stratégie de tests adaptée à votre projet.
La pyramide des tests
Concept fondamental : plus un test est haut dans la pyramide, plus il est lent et coûteux à maintenir.
/\
/ \ E2E (peu, critiques)
/----\
/ \ Intégration (modéré)
/--------\
/ \ Unitaires (beaucoup)
--------------
Tests unitaires : la base solide
Testent une fonction ou un composant isolé, sans dépendances externes.
// utils/formatPrice.test.ts
import { formatPrice } from './formatPrice';
describe('formatPrice', () => {
it('formate un prix en euros', () => {
expect(formatPrice(1234.5)).toBe('1 234,50 €');
});
it('gère les prix à zéro', () => {
expect(formatPrice(0)).toBe('0,00 €');
});
it('arrondit correctement', () => {
expect(formatPrice(10.999)).toBe('11,00 €');
});
});
Caractéristiques :
- Exécution instantanée (< 10ms par test)
- Faciles à écrire et maintenir
- Feedback immédiat pendant le développement
Quoi tester : fonctions utilitaires, logique métier, transformations de données.
Tests d'intégration : les connexions
Vérifient que plusieurs modules fonctionnent ensemble.
// api/users.test.ts
import { createUser, getUser } from './users';
import { db } from './database';
beforeEach(async () => {
await db.clear();
});
describe('User API', () => {
it('crée et récupère un utilisateur', async () => {
const created = await createUser({
email: 'test@example.com',
name: 'Test User'
});
const retrieved = await getUser(created.id);
expect(retrieved.email).toBe('test@example.com');
});
});
Caractéristiques :
- Plus lents (accès base de données, réseau)
- Détectent les problèmes d'interface entre modules
- Nécessitent un environnement de test
Quoi tester : routes API, interactions avec la base de données, services externes mockés.
Tests E2E : le parcours utilisateur
Simulent un utilisateur réel interagissant avec l'application.
// e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';
test('parcours d\'achat complet', async ({ page }) => {
// Ajouter un produit au panier
await page.goto('/products/123');
await page.click('button:text("Ajouter au panier")');
// Aller au checkout
await page.click('a:text("Voir le panier")');
await page.click('button:text("Commander")');
// Remplir les informations
await page.fill('[name="email"]', 'client@example.com');
await page.fill('[name="address"]', '123 Rue Example');
await page.click('button:text("Payer")');
// Vérifier la confirmation
await expect(page.locator('h1')).toContainText('Commande confirmée');
});
Caractéristiques :
- Lents (plusieurs secondes par test)
- Fragiles (sensibles aux changements UI)
- Haute confiance (testent le système complet)
Quoi tester : parcours critiques (inscription, achat, fonctionnalités clés).
Ma stratégie en pratique
Ratio recommandé
| Type | Proportion | Temps d'exécution | |------|------------|-------------------| | Unitaires | 70% | < 30 secondes | | Intégration | 20% | < 2 minutes | | E2E | 10% | < 10 minutes |
Quand écrire des tests ?
- Avant de coder (TDD) : pour la logique métier complexe
- Après un bug : le test empêche la régression
- Pour les parcours critiques : ce qui fait perdre de l'argent si ça casse
Quand NE PAS écrire de tests ?
- Prototypes jetables
- Code UI qui change constamment
- Wrappers triviaux sans logique
Outils recommandés
Pour React/Next.js
{
"devDependencies": {
"vitest": "^1.0.0",
"@testing-library/react": "^14.0.0",
"playwright": "^1.40.0"
}
}
- Vitest : rapide, compatible Jest, watch mode efficace
- Testing Library : tests orientés comportement utilisateur
- Playwright : E2E fiable, multi-navigateurs
Structure de fichiers
src/
components/
Button/
Button.tsx
Button.test.tsx # Tests unitaires
lib/
api/
users.ts
users.test.ts # Tests d'intégration
e2e/
checkout.spec.ts # Tests E2E
auth.spec.ts
Intégration CI/CD
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pnpm install
- run: pnpm test:unit
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pnpm install
- run: pnpm build
- run: pnpm test:e2e
Les tests unitaires bloquent le merge immédiatement. Les E2E tournent en parallèle.
Les pièges à éviter
-
Tester l'implémentation, pas le comportement
// ❌ Fragile : teste les détails internes expect(component.state.isLoading).toBe(true); // ✅ Robuste : teste ce que voit l'utilisateur expect(screen.getByText('Chargement...')).toBeInTheDocument(); -
Tests trop couplés aux données
// ❌ Casse si l'ID change expect(user.id).toBe(123); // ✅ Vérifie la structure expect(user).toHaveProperty('id'); -
100% de couverture comme objectif
La couverture mesure les lignes exécutées, pas la qualité des assertions. 80% de couverture pertinente vaut mieux que 100% de tests superficiels.
Conclusion
Une bonne stratégie de tests n'est pas d'avoir le plus de tests possible, mais les bons tests aux bons endroits.
Commencez petit : quelques tests unitaires sur la logique critique, un test E2E sur le parcours principal. Ajoutez des tests quand vous corrigez des bugs. En quelques mois, vous aurez un filet de sécurité solide.
