Tests unitaires et qualité de code

Découvrez comment écrire des tests unitaires en PHP avec PHPUnit, améliorer la qualité de votre code et mettre en place l'intégration continue.

Introduction à PHPUnit

PHPUnit est le framework de référence pour les tests unitaires en PHP. Il permet de vérifier automatiquement que chaque partie de votre code fonctionne comme prévu, sans avoir à tester manuellement après chaque modification.

Pourquoi écrire des tests unitaires ?
  • Garantir le bon fonctionnement du code lors des évolutions
  • Faciliter la maintenance et la refactorisation
  • Documenter le comportement attendu
  • Détecter les bugs avant la mise en production
  • Augmenter la confiance dans votre code

Imaginez que vous développez une application et que vous ajoutez régulièrement des fonctionnalités. Sans tests, comment être sûr que vos nouvelles fonctionnalités ne cassent pas des fonctionnalités existantes ? C'est là que les tests unitaires sont précieux.

Sans tests unitaires
  1. Vous modifiez une fonction
  2. Vous devez tester manuellement toutes les parties de l'application qui l'utilisent
  3. Vous oubliez peut-être certains cas d'utilisation
  4. Un bug se glisse en production
Avec tests unitaires
  1. Vous modifiez une fonction
  2. Vous lancez les tests automatiquement
  3. Si un test échoue, vous êtes alerté immédiatement
  4. Vous corrigez avant de mettre en production

Résultat : gain de temps, code plus fiable, développement plus serein.

Écrire ses premiers tests

Un test unitaire vérifie le comportement d'une fonction ou d'une classe de façon isolée. Commençons par un exemple simple :

Installation de PHPUnit
  • Via Composer (recommandé) : composer require --dev phpunit/phpunit
  • Ou globalement : composer global require phpunit/phpunit
  • Pour les débutants : commencez par créer un projet avec composer init puis installez PHPUnit

Étape 1: Créer une fonction à tester

Fichier: Calculator.php
// Calculator.php
class Calculator {
    public function add($a, $b) {
        return $a + $b;
    }
    
    public function subtract($a, $b) {
        return $a - $b;
    }
    
    public function multiply($a, $b) {
        return $a * $b;
    }
    
    public function divide($a, $b) {
        if ($b == 0) {
            throw new InvalidArgumentException('Division par zéro impossible');
        }
        return $a / $b;
    }
}

Étape 2: Créer une classe de test

Fichier: CalculatorTest.php
// tests/CalculatorTest.php
use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase {
    private $calculator;
    
    // Cette méthode s'exécute avant chaque test
    protected function setUp(): void {
        $this->calculator = new Calculator();
    }
    
    // Le nom des méthodes de test doit commencer par "test"
    public function testAdditionReturnsCorrectResult() {
        $result = $this->calculator->add(2, 2);
        $this->assertEquals(4, $result);
    }
    
    public function testSubtractionReturnsCorrectResult() {
        $result = $this->calculator->subtract(5, 2);
        $this->assertEquals(3, $result);
    }
    
    public function testMultiplicationReturnsCorrectResult() {
        $result = $this->calculator->multiply(3, 4);
        $this->assertEquals(12, $result);
    }
    
    public function testDivisionReturnsCorrectResult() {
        $result = $this->calculator->divide(10, 2);
        $this->assertEquals(5, $result);
    }
    
    public function testDivisionByZeroThrowsException() {
        $this->expectException(InvalidArgumentException::class);
        $this->calculator->divide(10, 0);
    }
}

Explication :

  • setUp() : Prépare l'environnement avant chaque test
  • assertEquals() : Vérifie que deux valeurs sont égales
  • expectException() : Vérifie qu'une exception est levée

Étape 3: Exécuter les tests

Dans votre terminal
# Exécuter tous les tests
./vendor/bin/phpunit tests/

# Exécuter un fichier de test spécifique
./vendor/bin/phpunit tests/CalculatorTest.php

# Exécuter une méthode de test spécifique
./vendor/bin/phpunit --filter testAdditionReturnsCorrectResult tests/CalculatorTest.php

Résultat d'exécution :

PHPUnit 9.5.28 by Sebastian Bergmann and contributors.

..... 5 / 5 (100%)

Time: 00:00.003, Memory: 4.00 MB

OK (5 tests, 5 assertions)
Bonnes pratiques pour nommer vos tests
  • Utilisez des noms descriptifs : testAdditionReturnsCorrectResult()
  • Suivez un format comme test[Méthode][Scenario][Résultat]()
  • Exemple : testDivisionByZeroThrowsException()

Assertions et techniques avancées

PHPUnit propose de nombreuses assertions pour vérifier différents types de conditions. Voici les plus courantes que vous utiliserez régulièrement :

Assertions de base
// Vérifier si une valeur est vraie ou fausse
$this->assertTrue($valeur);
$this->assertFalse($valeur);

// Vérifier une égalité
$this->assertEquals(4, 2 + 2);
$this->assertSame(4, $resultat); // égalité stricte (===)

// Vérifier si une valeur est null
$this->assertNull($valeur);
$this->assertNotNull($valeur);

// Vérifier un tableau
$this->assertCount(3, $tableau);
$this->assertContains('pomme', $fruits);
Assertions pour les chaînes
// Vérifier si une chaîne contient une autre
$this->assertStringContainsString('abc', $texte);

// Vérifier si une chaîne commence par
$this->assertStringStartsWith('Bonjour', $message);

// Vérifier si une chaîne se termine par
$this->assertStringEndsWith('.jpg', $nomFichier);

// Vérifier avec une expression régulière
$this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2}$/', $date);

Astuce : En cas d'échec d'un test, PHPUnit affiche la différence entre la valeur attendue et la valeur obtenue.

Tests avec des mocks (simulacres)

Les mocks permettent de simuler le comportement de dépendances externes (bases de données, API, etc.) pour isoler le code que vous testez.

Exemple de mock
class UserServiceTest extends TestCase {
    public function testGetFullName() {
        // Créer un mock du Repository
        $mockRepository = $this->createMock(UserRepository::class);
        
        // Configurer le comportement du mock
        $mockRepository->method('findById')
            ->with(123) // On s'attend à ce que le paramètre soit 123
            ->willReturn([
                'firstname' => 'Jean',
                'lastname' => 'Dupont'
            ]);
        
        // Injecter le mock dans le service à tester
        $userService = new UserService($mockRepository);
        
        // Tester le service
        $fullName = $userService->getFullNameById(123);
        $this->assertEquals('Jean Dupont', $fullName);
    }
}

Avantages des mocks :

  • Tests plus rapides (pas de vraie requête DB)
  • Tests plus fiables (pas de dépendance externe)
  • Possibilité de simuler des cas particuliers

Couverture de code

La couverture de code mesure quelle proportion de votre code est exécutée par vos tests. C'est un indicateur précieux pour savoir si tous les chemins d'exécution sont testés.

Générer un rapport de couverture
# Vous aurez besoin de Xdebug ou PCOV installé
./vendor/bin/phpunit --coverage-html coverage/ tests/

Cette commande générera un dossier coverage/ avec un rapport HTML détaillé montrant :

  • Le pourcentage global de couverture
  • Les lignes couvertes (vert) et non couvertes (rouge)
  • Les chemins d'exécution manquants

Visez une couverture > 80% pour un code de qualité, mais souvenez-vous que la qualité prime sur la quantité.

Intégration continue (CI)

L'intégration continue automatise l'exécution de vos tests à chaque modification du code. C'est comme avoir un assistant qui vérifie constamment la qualité de votre code.

Principe de la CI
Schéma CI

Les tests sont exécutés automatiquement à chaque commit ou pull request, avant même que le code ne soit déployé.

Outils populaires
  • GitHub Actions : intégré à GitHub
  • GitLab CI/CD : intégré à GitLab
  • Jenkins : solution auto-hébergée
  • Travis CI : service en ligne

Pour les débutants, GitHub Actions est le plus facile à mettre en place.

Configuration avec GitHub Actions

Pour configurer GitHub Actions, créez un fichier .github/workflows/tests.yml à la racine de votre projet :

Exemple de configuration GitHub Actions
# Nom du workflow
name: Tests PHP

# Déclencheurs
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

# Jobs à exécuter
jobs:
  tests:
    name: Tests unitaires
    runs-on: ubuntu-latest
    
    steps:
      # Récupérer le code
      - uses: actions/checkout@v3
      
      # Configurer PHP
      - name: Configurer PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, dom, filter
          coverage: xdebug
      
      # Installer les dépendances
      - name: Installer les dépendances
        run: composer install --prefer-dist
      
      # Exécuter les tests
      - name: Exécuter les tests
        run: ./vendor/bin/phpunit
      
      # Générer un rapport de couverture
      - name: Générer la couverture de code
        run: ./vendor/bin/phpunit --coverage-clover coverage.xml
      
      # Publier la couverture (optionnel)
      - name: Publier la couverture sur Codecov
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage.xml

Ce workflow :

  1. Se déclenche à chaque push sur main/develop ou lors d'une pull request
  2. Configure PHP 8.2 avec Xdebug pour la couverture de code
  3. Installe les dépendances via Composer
  4. Exécute les tests PHPUnit
  5. Génère et publie un rapport de couverture de code
Badges de statut

Ajoutez un badge dans votre README.md pour afficher l'état de vos tests :

[![Tests PHP](https://github.com/username/repo/actions/workflows/tests.yml/badge.svg)](https://github.com/username/repo/actions/workflows/tests.yml)
[![Couverture de code](https://codecov.io/gh/username/repo/branch/main/graph/badge.svg)](https://codecov.io/gh/username/repo)

Ces badges s'afficheront ainsi :

Badge GitHub Actions Badge couverture Codecov

Bonnes pratiques et conseils pour débutants

Les principes FIRST pour des tests de qualité

  • Fast (Rapide) : Les tests doivent s'exécuter rapidement pour être utilisés souvent.
  • Independent (Indépendant) : Chaque test doit pouvoir s'exécuter seul, sans dépendre d'autres tests.
  • Repeatable (Reproductible) : Les tests doivent donner le même résultat à chaque exécution.
  • Self-validating (Auto-validant) : Les tests doivent déterminer eux-mêmes s'ils réussissent ou échouent.
  • Timely (Opportun) : Idéalement, écrivez les tests avant ou en même temps que le code.

Conseils pratiques pour débutants

Par où commencer ?
  1. Commencez petit : Testez d'abord des fonctions simples
  2. Testez les bugs : Chaque bug corrigé mérite un test
  3. Pratiquez le TDD : Test → Code → Refactoring
  4. Utilisez des fixtures : Pour préparer les données de test
Structure recommandée
mon-projet/
├── src/
│   └── Calculator.php
├── tests/
│   └── CalculatorTest.php
├── composer.json
└── phpunit.xml

Configuration minimale dans phpunit.xml :

<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true">
    <testsuites>
        <testsuite name="Mon Application">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

Éviter les pièges courants

🚫 Ce qu'il faut éviter
  • Tests fragiles : Qui échouent pour des raisons externes (dates, données aléatoires)
  • Tests lents : Comme les tests qui font des requêtes réelles à une base de données
  • Tests interdépendants : Qui dépendent d'autres tests ou d'un état global
  • Tests obscurs : Difficiles à comprendre ou à maintenir

Ressources pour approfondir

💡 À retenir

Les tests unitaires sont un investissement : ils demandent du temps au début, mais vous en font gagner beaucoup plus sur le long terme en réduisant les bugs et en facilitant la maintenance.

Commencez petit, soyez cohérent, et intégrez progressivement les tests à votre routine de développement !

Exercices pratiques

Pour vous familiariser avec PHPUnit, voici quelques exercices à réaliser :

Exercice 1 : Testez une classe Panier

Créez une classe ShoppingCart avec les méthodes :

  • addItem($item, $price, $quantity)
  • removeItem($item)
  • getTotal()
  • clear()

Puis écrivez des tests unitaires pour vérifier le bon fonctionnement.

Exercice 2 : Testez un validateur

Créez une classe Validator qui vérifie :

  • Emails valides
  • Mots de passe forts (8+ caractères, mixte)
  • Numéros de téléphone

Testez avec différentes entrées valides et invalides.

💻 Code de départ pour l'exercice 1
// src/ShoppingCart.php
class ShoppingCart {
    private $items = [];
    
    public function addItem($item, $price, $quantity = 1) {
        if ($price <= 0) {
            throw new InvalidArgumentException("Le prix doit être positif");
        }
        
        if ($quantity <= 0) {
            throw new InvalidArgumentException("La quantité doit être positive");
        }
        
        $this->items[$item] = [
            'price' => $price,
            'quantity' => $quantity
        ];
    }
    
    // À compléter : removeItem(), getTotal(), clear()
}