Concepts de tests
Les tests automatisés vérifient que le code se comporte comme attendu, maintenant et après chaque modification. Ils sont l'un des investissements les plus rentables en développement logiciel.
Pourquoi tester ?
| Sans tests | Avec tests |
|---|---|
| Peur de modifier le code existant | Confiance pour refactorer |
| Bugs découverts en production | Bugs détectés en développement |
| Régression silencieuse | Régression détectée immédiatement |
| Documentation obsolète | Tests = documentation vivante |
| Déploiements risqués | Déploiements continus possibles |
Les tests ne garantissent pas l'absence de bugs — ils garantissent que les comportements testés fonctionnent. Un code non testé est un code dont on ne sait pas s'il fonctionne.
Les types de tests
Tests unitaires
Testent une unité isolée de code (fonction, méthode, classe) sans ses dépendances externes.
[Fonction] ← [Test unitaire]
- Vitesse : très rapides (millisecondes)
- Isolation : les dépendances sont remplacées par des mocks/stubs
- Quantité : la majorité des tests d'un projet
- Exemples : tester qu'une fonction de calcul retourne le bon résultat, qu'un validateur rejette les entrées invalides
Tests d'intégration
Testent l'interaction entre plusieurs composants : un service et sa base de données, deux modules qui collaborent, une API et son ORM.
[Module A] + [Module B] ← [Test d'intégration]
- Vitesse : plus lents (requêtes réelles, I/O)
- Isolation : partielle — on peut mocker certaines parties mais pas toutes
- Quantité : moins nombreux que les tests unitaires
- Exemples : tester qu'un appel à l'API retourne bien les données de la base, qu'un service d'email envoie vraiment un message
Tests end-to-end (e2e)
Testent l'application entière du point de vue de l'utilisateur, via un navigateur ou un client HTTP.
[Navigateur/Client] → [Frontend] → [API] → [BDD] ← [Test e2e]
- Vitesse : lents (plusieurs secondes par test)
- Isolation : aucune — l'environnement doit être complet
- Quantité : peu nombreux, couvrent les parcours critiques
- Exemples : tester qu'un utilisateur peut s'inscrire, se connecter et passer une commande
Autres types
| Type | Description | Déclenchement |
|---|---|---|
| Smoke tests | Vérifications minimales que l'app démarre et répond | Après chaque déploiement |
| Tests de régression | Vérifient qu'un bug corrigé ne revient pas | À chaque PR |
| Tests de performance | Mesurent les temps de réponse sous charge | Périodiquement |
| Tests de snapshot | Comparent le rendu actuel à une référence | À chaque PR (UI) |
| Tests de mutation | Vérifient la qualité des tests eux-mêmes | Audit qualité |
La pyramide des tests
La pyramide définit l'équilibre recommandé entre les types de tests :
/\
/e2e\ ← peu nombreux, lents, coûteux
/──────\
/intégra-\ ← nombre modéré
/ tion \
/────────────\
/ unitaires \ ← nombreux, rapides, bon marché
/────────────────\
Règles d'équilibre
- Tests unitaires : 70-80% — rapides, fiables, faciles à maintenir
- Tests d'intégration : 15-20% — vérifient les contrats entre composants
- Tests e2e : 5-10% — couvrent uniquement les parcours critiques
L'anti-pattern : le cône de glace
/────────────────\
/ e2e \ ← trop nombreux → lents, fragiles, coûteux
/──────────────────────\
\ intégration /
\────────────────────/
\ unitaires /
\────────────────/ ← trop peu → peu de valeur
Une suite de tests trop orientée e2e est lente, fragile (un changement UI casse tout) et difficile à déboguer.
TDD — Test Driven Development
Le TDD inverse l'ordre naturel : on écrit le test avant le code.
Le cycle Red / Green / Refactor
┌─────────────────────────────────────┐
│ │
▼ │
RED │
Écrire un test qui échoue │
(le code n'existe pas encore) │
│ │
▼ │
GREEN │
Écrire le minimum de code │
pour faire passer le test │
│ │
▼ │
REFACTOR ────────────────────────────────┘
Améliorer le code sans
changer son comportement
Exemple de workflow TDD
1. Je dois implémenter une fonction calculateTax(price, rate)
2. RED — j'écris d'abord :
test('calcule la TVA à 20%', () => {
expect(calculateTax(100, 0.20)).toBe(20)
})
→ Le test échoue car calculateTax n'existe pas
3. GREEN — j'écris le minimum :
function calculateTax(price, rate) {
return price * rate
}
→ Le test passe
4. REFACTOR — j'améliore si nécessaire :
function calculateTax(price, rate) {
return Math.round(price * rate * 100) / 100 // arrondi à 2 décimales
}
→ Le test passe toujours
Avantages
- Force à réfléchir à l'interface avant l'implémentation
- Garantit que tout le code est couvert par des tests
- Facilite le refactoring en toute confiance
- Documentation automatique du comportement attendu
Quand l'utiliser
✅ Logique métier complexe, algorithmes, validations
✅ Code qui sera modifié fréquemment
✅ APIs publiques et contrats d'interface
❌ Prototypes et explorations rapides
❌ Code d'infrastructure / configuration
❌ Interfaces utilisateur (préférer les tests e2e)
BDD — Behaviour Driven Development
Le BDD étend le TDD en formulant les tests en langage naturel, compréhensible par tous (développeurs, PO, QA, clients).
La structure Given / When / Then
Given [contexte initial — l'état du système]
When [action réalisée — l'événement déclencheur]
Then [résultat attendu — le comportement observable]
Exemple
# Langage Gherkin (utilisé avec Cucumber, Behat…)
Feature: Authentification utilisateur
Scenario: Connexion avec des identifiants valides
Given un utilisateur avec l'email "alice@exemple.com" et le mot de passe "secret"
When il soumet le formulaire de connexion
Then il est redirigé vers le tableau de bord
And un token JWT est retourné
Scenario: Connexion avec un mauvais mot de passe
Given un utilisateur avec l'email "alice@exemple.com"
When il soumet le mot de passe "mauvais"
Then il reçoit une erreur "Identifiants invalides"
And aucun token n'est retourné
BDD sans Gherkin
On peut appliquer la structure Given/When/Then sans outil dédié, directement dans les noms de tests :
// Jest / Vitest
describe('Authentification', () => {
describe('quand les identifiants sont valides', () => {
it('redirige vers le tableau de bord', () => { ... })
it('retourne un token JWT', () => { ... })
})
describe('quand le mot de passe est incorrect', () => {
it('retourne une erreur 401', () => { ... })
it('ne retourne pas de token', () => { ... })
})
})
TDD vs BDD
| TDD | BDD | |
|---|---|---|
| Focus | Comment le code fonctionne | Ce que le système fait |
| Langage | Technique | Naturel / métier |
| Public | Développeurs | Développeurs + PO + QA |
| Granularité | Unitaire | Comportement fonctionnel |
| Outils | Jest, Vitest, pytest… | Cucumber, Behat, Jest… |
| Complémentaires ? | Oui — BDD s'appuie sur TDD |
Bonnes pratiques
Écrire de bons tests
F.I.R.S.T.
| Lettre | Principe | Description |
|---|---|---|
| F | Fast | Un test doit s'exécuter en millisecondes |
| I | Independent | Les tests ne doivent pas dépendre les uns des autres |
| R | Repeatable | Même résultat à chaque exécution, quel que soit l'environnement |
| S | Self-validating | Le test dit lui-même s'il passe ou échoue (pas de vérification manuelle) |
| T | Timely | Écrit au bon moment (idéalement avant ou avec le code) |
Nommage des tests
Un bon nom de test décrit le comportement attendu, pas l'implémentation :
❌ test('fonction login')
❌ test('test 1')
✅ test('retourne une erreur si le mot de passe est vide')
✅ test('envoie un email de confirmation après inscription')
Ce qu'il faut tester
✅ La logique métier (calculs, règles, validations)
✅ Les cas limites (valeurs nulles, vides, limites max/min)
✅ Les cas d'erreur (que se passe-t-il quand ça échoue ?)
✅ Les contrats d'interface (ce que retourne une fonction)
❌ Les détails d'implémentation (comment c'est fait en interne)
❌ Le code de librairies tierces
❌ Les getters/setters triviaux
Coverage : indicateur, pas objectif
La couverture de code mesure le pourcentage de code exécuté par les tests. Un taux élevé est rassurant, mais :
- 100% de coverage ≠ absence de bugs
- Un test mal écrit peut couvrir sans tester
- Viser 80% est souvent plus pragmatique que 100%
Un bon test qui échoue quand le comportement change vaut mieux que 10 tests qui passent toujours.
Mocks, stubs et spies
| Terme | Rôle | Exemple |
|---|---|---|
| Mock | Remplace une dépendance et vérifie les appels | Vérifier qu'un service email a bien été appelé |
| Stub | Remplace une dépendance et retourne une valeur fixe | Simuler une réponse d'API |
| Spy | Observe un vrai objet sans le remplacer | Vérifier qu'une méthode a été appelée N fois |
| Fake | Implémentation simplifiée (ex. base en mémoire) | Base de données in-memory pour les tests |