Introduction : Pourquoi Tester son Code ?
Vous avez écrit une fonction, vous l'avez testée manuellement, elle fonctionne. Parfait ! Mais que se passe-t-il quand :
- Vous modifiez cette fonction 3 mois plus tard ?
- Un collègue la modifie sans connaître tous les cas d'usage ?
- Vous devez prouver à votre client que le code fonctionne ?
C'est là qu'interviennent les tests unitaires : des programmes qui vérifient automatiquement que votre code fonctionne comme prévu.
Dans ce guide, nous allons découvrir :
- Ce que sont les tests unitaires (sans jargon compliqué)
- Le TDD (Test-Driven Development) : écrire les tests AVANT le code
- Comment créer vos propres tests en PHP pur (sans PHPUnit)
Qu'est-ce qu'un Test Unitaire ?
Définition simple
Un test unitaire est un petit programme qui vérifie automatiquement qu'une partie de votre code (une fonction, une méthode) fonctionne correctement.
Imaginez un test unitaire comme un contrôle qualité automatisé :
// Votre fonction
function additionner($a, $b) {
return $a + $b;
}
// Le test unitaire vérifie que :
// additionner(2, 3) retourne bien 5
// additionner(-1, 1) retourne bien 0
// additionner(0, 0) retourne bien 0
Tester manuellement vs Tests unitaires
| Test manuel | Test unitaire |
|---|---|
| Vous exécutez le code à la main | Le test s'exécute automatiquement |
| Vous vérifiez visuellement le résultat | Le test vérifie pour vous |
| Fastidieux à répéter | Se lance en 1 seconde |
| On oublie de tester certains cas | Tous les cas sont toujours testés |
Créer un Mini-Framework de Test
Avant d'utiliser PHPUnit, créons notre propre système de test simple. Cela vous aidera à comprendre comment ça fonctionne !
La fonction assert()
PHP possède une fonction assert() native, mais créons la nôtre pour mieux comprendre :
// test_helper.php - Notre mini-framework de test
function assertEquals($expected, $actual, $message = "") {
if ($expected === $actual) {
echo "✅ PASS: $message\n";
return true;
} else {
echo "❌ FAIL: $message\n";
echo " Attendu : " . var_export($expected, true) . "\n";
echo " Obtenu : " . var_export($actual, true) . "\n";
return false;
}
}
function assertTrue($condition, $message = "") {
return assertEquals(true, $condition, $message);
}
function assertFalse($condition, $message = "") {
return assertEquals(false, $condition, $message);
}
function runTests($testFunctions) {
$passed = 0;
$failed = 0;
echo "\n🧪 EXÉCUTION DES TESTS\n";
echo "======================\n\n";
foreach ($testFunctions as $testName => $testFunction) {
echo "📋 $testName\n";
if ($testFunction()) {
$passed++;
} else {
$failed++;
}
echo "\n";
}
echo "======================\n";
echo "Résultat : $passed passé(s), $failed échoué(s)\n";
return $failed === 0;
}
Le TDD : Test-Driven Development
Le principe : Rouge → Vert → Refactor
Le TDD inverse l'approche classique. Au lieu d'écrire le code puis les tests, on fait l'inverse :
- 🔴 Rouge : Écrire un test qui échoue (le code n'existe pas encore)
- 🟢 Vert : Écrire le minimum de code pour faire passer le test
- 🔵 Refactor : Améliorer le code en gardant les tests verts
Exemple pratique : Calculatrice de Prix
Créons une fonction qui calcule le prix TTC avec différentes règles.
Étape 1 : Écrire les tests d'abord (🔴 Rouge)
// tests/PrixTest.php
require_once 'test_helper.php';
// Les tests définissent le COMPORTEMENT attendu
function testCalculTTCBasique() {
// 100€ HT avec 20% de TVA = 120€ TTC
$resultat = calculerPrixTTC(100, 20);
return assertEquals(120, $resultat, "100€ HT + 20% TVA = 120€ TTC");
}
function testCalculTTCAvecTVAReduite() {
// 100€ HT avec 5.5% de TVA = 105.5€ TTC
$resultat = calculerPrixTTC(100, 5.5);
return assertEquals(105.5, $resultat, "100€ HT + 5.5% TVA = 105.5€ TTC");
}
function testCalculTTCAvecPrixZero() {
// 0€ HT = 0€ TTC (peu importe la TVA)
$resultat = calculerPrixTTC(0, 20);
return assertEquals(0, $resultat, "0€ HT = 0€ TTC");
}
function testCalculTTCAvecTVAZero() {
// Pas de TVA = prix HT = prix TTC
$resultat = calculerPrixTTC(100, 0);
return assertEquals(100, $resultat, "100€ HT + 0% TVA = 100€ TTC");
}
// Exécuter les tests
runTests([
'Calcul TTC basique' => 'testCalculTTCBasique',
'Calcul TTC TVA réduite' => 'testCalculTTCAvecTVAReduite',
'Calcul TTC prix zéro' => 'testCalculTTCAvecPrixZero',
'Calcul TTC TVA zéro' => 'testCalculTTCAvecTVAZero',
]);
Si vous exécutez ce fichier maintenant, vous obtenez des erreurs car calculerPrixTTC() n'existe pas. C'est normal ! C'est l'étape Rouge.
Étape 2 : Écrire le code minimal (🟢 Vert)
// src/prix.php
function calculerPrixTTC($prixHT, $tauxTVA) {
$montantTVA = $prixHT * $tauxTVA / 100;
return $prixHT + $montantTVA;
}
Maintenant, exécutez les tests :
php tests/PrixTest.php
🧪 EXÉCUTION DES TESTS
======================
📋 Calcul TTC basique
✅ PASS: 100€ HT + 20% TVA = 120€ TTC
📋 Calcul TTC TVA réduite
✅ PASS: 100€ HT + 5.5% TVA = 105.5€ TTC
📋 Calcul TTC prix zéro
✅ PASS: 0€ HT = 0€ TTC
📋 Calcul TTC TVA zéro
✅ PASS: 100€ HT + 0% TVA = 100€ TTC
======================
Résultat : 4 passé(s), 0 échoué(s)
Tous les tests passent ! C'est l'étape Vert.
Étape 3 : Ajouter des cas limites
Et si quelqu'un passe un prix négatif ? Ajoutons un test :
function testCalculTTCAvecPrixNegatif() {
// Un prix négatif devrait lever une exception ou retourner une erreur
try {
$resultat = calculerPrixTTC(-100, 20);
return assertFalse(true, "Devrait lever une exception pour prix négatif");
} catch (InvalidArgumentException $e) {
return assertTrue(true, "Exception levée pour prix négatif");
}
}
Ce test échoue ! Modifions notre fonction :
function calculerPrixTTC($prixHT, $tauxTVA) {
// Validation des entrées
if ($prixHT < 0) {
throw new InvalidArgumentException("Le prix ne peut pas être négatif");
}
if ($tauxTVA < 0) {
throw new InvalidArgumentException("Le taux de TVA ne peut pas être négatif");
}
$montantTVA = $prixHT * $tauxTVA / 100;
return $prixHT + $montantTVA;
}
Exemple Complet : Validateur d'Email
Appliquons le TDD pour créer un validateur d'email.
Les tests d'abord
// tests/EmailTest.php
require_once 'test_helper.php';
function testEmailValide() {
return assertTrue(
estEmailValide("jean@example.com"),
"jean@example.com est valide"
);
}
function testEmailAvecSousDomaine() {
return assertTrue(
estEmailValide("contact@mail.example.com"),
"contact@mail.example.com est valide"
);
}
function testEmailSansArobase() {
return assertFalse(
estEmailValide("jeanexample.com"),
"jeanexample.com (sans @) est invalide"
);
}
function testEmailSansDomaine() {
return assertFalse(
estEmailValide("jean@"),
"jean@ (sans domaine) est invalide"
);
}
function testEmailSansNom() {
return assertFalse(
estEmailValide("@example.com"),
"@example.com (sans nom) est invalide"
);
}
function testEmailVide() {
return assertFalse(
estEmailValide(""),
"Email vide est invalide"
);
}
function testEmailAvecEspaces() {
return assertFalse(
estEmailValide("jean @example.com"),
"Email avec espaces est invalide"
);
}
runTests([
'Email valide' => 'testEmailValide',
'Email avec sous-domaine' => 'testEmailAvecSousDomaine',
'Email sans arobase' => 'testEmailSansArobase',
'Email sans domaine' => 'testEmailSansDomaine',
'Email sans nom' => 'testEmailSansNom',
'Email vide' => 'testEmailVide',
'Email avec espaces' => 'testEmailAvecEspaces',
]);
L'implémentation
// src/email.php
function estEmailValide($email) {
// Cas de base : email vide
if (empty($email)) {
return false;
}
// Pas d'espaces autorisés
if (strpos($email, ' ') !== false) {
return false;
}
// Doit contenir exactement un @
$parties = explode('@', $email);
if (count($parties) !== 2) {
return false;
}
$nom = $parties[0];
$domaine = $parties[1];
// Le nom et le domaine ne doivent pas être vides
if (empty($nom) || empty($domaine)) {
return false;
}
// Le domaine doit contenir au moins un point
if (strpos($domaine, '.') === false) {
return false;
}
return true;
}
Les Avantages des Tests Unitaires
1. Documentation vivante
Vos tests décrivent le comportement attendu de votre code :
// En lisant ces tests, on comprend immédiatement le comportement :
testCalculTTCBasique() // Cas normal
testCalculTTCAvecTVAReduite() // TVA différente
testCalculTTCAvecPrixNegatif() // Gestion des erreurs
2. Refactoring en confiance
Vous pouvez modifier votre code sans crainte. Si les tests passent toujours, le comportement est préservé :
// Version originale
function calculerPrixTTC($prixHT, $tauxTVA) {
return $prixHT + ($prixHT * $tauxTVA / 100);
}
// Version optimisée (même comportement)
function calculerPrixTTC($prixHT, $tauxTVA) {
return $prixHT * (1 + $tauxTVA / 100);
}
// Les tests passent ? La modification est sûre !
3. Détection rapide des régressions
Une régression = un bug introduit en modifiant du code existant.
// Quelqu'un modifie la fonction par erreur
function calculerPrixTTC($prixHT, $tauxTVA) {
return $prixHT * $tauxTVA / 100; // Oups ! Oublié le prix HT
}
// Les tests détectent immédiatement le problème :
❌ FAIL: 100€ HT + 20% TVA = 120€ TTC
Attendu : 120
Obtenu : 20
Bonnes Pratiques
1. Un test = un comportement
// ❌ Mauvais : teste plusieurs choses
function testCalculPrix() {
assertEquals(120, calculerPrixTTC(100, 20));
assertEquals(105.5, calculerPrixTTC(100, 5.5));
assertEquals(0, calculerPrixTTC(0, 20));
}
// ✅ Bon : un test par comportement
function testCalculTTCBasique() {
return assertEquals(120, calculerPrixTTC(100, 20), "TVA standard");
}
function testCalculTTCReduite() {
return assertEquals(105.5, calculerPrixTTC(100, 5.5), "TVA réduite");
}
2. Noms de tests explicites
// ❌ Mauvais
function test1() { ... }
function testEmail() { ... }
// ✅ Bon
function testEmailValideAvecFormatStandard() { ... }
function testEmailInvalideCarSansArobase() { ... }
3. Tester les cas limites
// Ne pas oublier de tester :
- Les valeurs nulles ou vides
- Les nombres négatifs
- Les chaînes avec caractères spéciaux
- Les tableaux vides
- Les valeurs aux limites (0, -1, MAX_INT)
Exercice : Créez vos propres tests
Mettez en pratique ce que vous avez appris en créant des tests pour cette fonction :
/**
* Calcule la remise applicable selon le montant du panier
* - Moins de 50€ : pas de remise
* - Entre 50€ et 100€ : 5% de remise
* - Plus de 100€ : 10% de remise
*/
function calculerRemise($montant) {
// À vous de jouer !
}
Indice : Commencez par écrire les tests, puis implémentez la fonction !
Conclusion
Les tests unitaires et le TDD peuvent sembler contraignants au début, mais ils deviennent vite indispensables :
- ✅ Votre code est plus fiable
- ✅ Vous codez avec confiance
- ✅ Les bugs sont détectés immédiatement
- ✅ Le refactoring devient sans risque
Pour aller plus loin
- Découvrez PHPUnit : le framework de test standard en PHP
- Explorez Pest : une alternative moderne et élégante
- Apprenez les Mocks pour tester du code avec des dépendances
- Intégrez les tests dans votre CI/CD (GitHub Actions, GitLab CI)
Commencez petit : testez une seule fonction dans votre prochain projet. Vous verrez rapidement les bénéfices !