Aller au contenu principal
PHP

Tests Unitaires et TDD en PHP : Guide Pratique pour Débutants

person Jean-Luc SALLARSAIB calendar_today 30 janv. 2026 schedule 7 min de lecture visibility 4 vues

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 :

  1. 🔴 Rouge : Écrire un test qui échoue (le code n'existe pas encore)
  2. 🟢 Vert : Écrire le minimum de code pour faire passer le test
  3. 🔵 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 !

Partager cet article