1 / 15

Tests avec PHPUnit et Symfony

Guide pour développeurs débutants

Apprendre à écrire des tests automatisés
pour vos applications Symfony

Objectifs :

  • Comprendre l'importance des tests
  • MaĂźtriser PHPUnit avec Symfony
  • Écrire des tests unitaires et fonctionnels
  • Adopter les bonnes pratiques

Pourquoi écrire des tests ?

Les tests automatisés permettent de :

  • DĂ©tecter les bugs avant la mise en production
  • Refactoriser en confiance sans casser l'existant
  • Documenter le code par l'exemple
  • Faciliter la maintenance et l'Ă©volution
  • AmĂ©liorer la qualitĂ© du code

✅ Avec des tests

  • DĂ©veloppement plus serein
  • Livraisons fiables
  • Code plus robuste
  • Équipe plus confiante

❌ Sans tests

  • Peur de modifier le code
  • Bugs en production
  • RĂ©gression frĂ©quentes
  • Stress lors des livraisons

Types de tests

🔬 Tests unitaires

Testent une fonction ou méthode isolée

Exemple : Tester qu'une fonction de calcul retourne le bon résultat

🔧 Tests d'intĂ©gration

Testent l'interaction entre plusieurs composants

Exemple : Tester qu'un service utilise correctement la base de données

🌐 Tests fonctionnels

Testent une fonctionnalité complÚte du point de vue utilisateur

Exemple : Tester qu'un utilisateur peut se connecter via le formulaire
Conseil : Commencez par les tests unitaires, ils sont plus simples à écrire et plus rapides à exécuter.

Installation et configuration

1. PHPUnit est inclus dans Symfony

Depuis Symfony 4.1, PHPUnit est installé automatiquement :

composer create-project symfony/skeleton mon-projet
cd mon-projet

2. Installation manuelle (si nécessaire)

composer require --dev phpunit/phpunit symfony/test-pack

3. Structure des dossiers

mon-projet/
├── src/
├── tests/           ← Dossier des tests
│   ├── Unit/        ← Tests unitaires
│   ├── Integration/ ← Tests d'intĂ©gration  
│   └── Functional/  ← Tests fonctionnels
├── phpunit.xml.dist ← Configuration PHPUnit
└── .env.test        ← Variables d'environnement pour les tests
Le fichier phpunit.xml.dist configure PHPUnit pour votre projet Symfony.

Votre premier test

1. Créons une classe simple à tester

<?php
// src/Service/Calculator.php
namespace App\Service;

class Calculator
{
    public function add(int $a, int $b): int
    {
        return $a + $b;
    }
    
    public function divide(int $a, int $b): float
    {
        if ($b === 0) {
            throw new \InvalidArgumentException('Division by zero');
        }
        return $a / $b;
    }
}

2. Écrivons le test

<?php
// tests/Unit/Service/CalculatorTest.php
namespace App\Tests\Unit\Service;

use App\Service\Calculator;
use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    public function testAdd(): void
    {
        $calculator = new Calculator();
        $result = $calculator->add(2, 3);
        
        $this->assertEquals(5, $result);
    }
}

Exécuter les tests

1. Commande de base

php bin/phpunit

2. Exécuter un test spécifique

php bin/phpunit tests/Unit/Service/CalculatorTest.php

3. Exécuter avec verbosité

php bin/phpunit --verbose

4. Résultat attendu

PHPUnit 9.5.10 by Sebastian Bergmann and contributors.

.                                    1 / 1 (100%)

Time: 00:00.001, Memory: 6.00 MB

OK (1 test, 1 assertion)
Le point . indique un test réussi. Un F indiquerait un échec.

Assertions principales

ÉgalitĂ©

// ÉgalitĂ© stricte
$this->assertEquals(5, $result);

// Identité (===)
$this->assertSame(5, $result);

// Non égalité
$this->assertNotEquals(3, $result);

Booléens

$this->assertTrue($condition);
$this->assertFalse($condition);

Nullité

$this->assertNull($value);
$this->assertNotNull($value);

Tableaux et chaĂźnes

$this->assertContains('item', $array);
$this->assertStringContains('text', $string);
$this->assertCount(3, $array);

Exceptions

$this->expectException(
    \InvalidArgumentException::class
);
$calculator->divide(5, 0);
Utilisez assertEquals pour les valeurs et assertSame pour l'identité stricte.

Tester les exceptions

Ajoutons un test pour notre méthode divide

<?php
// tests/Unit/Service/CalculatorTest.php
namespace App\Tests\Unit\Service;

use App\Service\Calculator;
use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    private Calculator $calculator;
    
    protected function setUp(): void
    {
        $this->calculator = new Calculator();
    }
    
    public function testAdd(): void
    {
        $result = $this->calculator->add(2, 3);
        $this->assertEquals(5, $result);
    }
    
    public function testDivide(): void
    {
        $result = $this->calculator->divide(10, 2);
        $this->assertEquals(5.0, $result);
    }
    
    public function testDivideByZeroThrowsException(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Division by zero');
        
        $this->calculator->divide(10, 0);
    }
}
La méthode setUp() s'exécute avant chaque test. Utilisez-la pour initialiser vos objets.

Tests avec le container Symfony

Créons un service avec des dépendances

<?php
// src/Service/UserService.php
namespace App\Service;

use App\Repository\UserRepository;

class UserService
{
    public function __construct(
        private UserRepository $userRepository
    ) {}
    
    public function getUserCount(): int
    {
        return count($this->userRepository->findAll());
    }
    
    public function isEmailTaken(string $email): bool
    {
        return $this->userRepository->findOneBy(['email' => $email]) !== null;
    }
}

Test d'intégration avec le container

<?php
// tests/Integration/Service/UserServiceTest.php
namespace App\Tests\Integration\Service;

use App\Service\UserService;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class UserServiceTest extends KernelTestCase
{
    public function testGetUserCount(): void
    {
        self::bootKernel();
        $container = static::getContainer();
        
        $userService = $container->get(UserService::class);
        $count = $userService->getUserCount();
        
        $this->assertIsInt($count);
        $this->assertGreaterThanOrEqual(0, $count);
    }
}

Mocks et Stubs

Qu'est-ce qu'un Mock ?

Un mock est un objet fictif qui simule le comportement d'un objet réel. Utile pour isoler le code testé.

Test unitaire avec mock

<?php
// tests/Unit/Service/UserServiceTest.php
namespace App\Tests\Unit\Service;

use App\Entity\User;
use App\Repository\UserRepository;
use App\Service\UserService;
use PHPUnit\Framework\TestCase;

class UserServiceTest extends TestCase
{
    public function testIsEmailTaken(): void
    {
        // Créer un mock du repository
        $mockRepository = $this->createMock(UserRepository::class);
        
        // Définir le comportement attendu
        $mockRepository->method('findOneBy')
            ->with(['email' => 'test@example.com'])
            ->willReturn(new User());
            
        $userService = new UserService($mockRepository);
        
        $result = $userService->isEmailTaken('test@example.com');
        
        $this->assertTrue($result);
    }
}
Les mocks permettent de tester votre code sans dépendre de la base de données ou d'autres services externes.

Tests fonctionnels

Tester une page web complĂšte

<?php
// tests/Functional/Controller/HomeControllerTest.php
namespace App\Tests\Functional\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class HomeControllerTest extends WebTestCase
{
    public function testHomePage(): void
    {
        $client = static::createClient();
        
        $crawler = $client->request('GET', '/');
        
        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Bienvenue');
    }
    
    public function testContactForm(): void
    {
        $client = static::createClient();
        
        $crawler = $client->request('GET', '/contact');
        
        $form = $crawler->selectButton('Envoyer')->form([
            'contact[name]' => 'John Doe',
            'contact[email]' => 'john@example.com',
            'contact[message]' => 'Hello world'
        ]);
        
        $client->submit($form);
        
        $this->assertResponseRedirects('/contact/success');
    }
}
WebTestCase simule un navigateur et permet de tester vos contrĂŽleurs de bout en bout.

Tests avec base de données

Configuration de l'environnement de test

# .env.test
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_test.db"
# ou
DATABASE_URL="mysql://user:pass@127.0.0.1:3306/myapp_test"

Préparer la base pour les tests

php bin/console doctrine:database:create --env=test
php bin/console doctrine:schema:create --env=test

Test avec données

<?php
// tests/Functional/Repository/UserRepositoryTest.php
namespace App\Tests\Functional\Repository;

use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Doctrine\ORM\EntityManagerInterface;

class UserRepositoryTest extends KernelTestCase
{
    private EntityManagerInterface $entityManager;
    private UserRepository $userRepository;
    
    protected function setUp(): void
    {
        $kernel = self::bootKernel();
        $this->entityManager = $kernel->getContainer()
            ->get('doctrine')->getManager();
        $this->userRepository = $this->entityManager
            ->getRepository(User::class);
    }
    
    public function testFindByEmail(): void
    {
        $user = new User();
        $user->setEmail('test@example.com');
        $user->setName('Test User');
        
        $this->entityManager->persist($user);
        $this->entityManager->flush();
        
        $foundUser = $this->userRepository
            ->findOneBy(['email' => 'test@example.com']);
        
        $this->assertNotNull($foundUser);
        $this->assertEquals('Test User', $foundUser->getName());
    }
}

Fixtures pour les tests

Installation

composer require --dev doctrine/doctrine-fixtures-bundle

Créer des fixtures de test

<?php
// src/DataFixtures/TestFixtures.php
namespace App\DataFixtures;

use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

class TestFixtures extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        for ($i = 1; $i <= 10; $i++) {
            $user = new User();
            $user->setEmail("user{$i}@example.com");
            $user->setName("User {$i}");
            $manager->persist($user);
        }
        
        $manager->flush();
    }
}

Utiliser les fixtures dans les tests

// Dans votre test
protected function setUp(): void
{
    parent::setUp();
    
    // Charger les fixtures
    $this->loadFixtures([TestFixtures::class]);
}
Les fixtures permettent d'avoir des données cohérentes pour tous vos tests.

Bonnes pratiques

✅ À faire

  • Tests indĂ©pendants : chaque test doit pouvoir s'exĂ©cuter seul
  • Noms explicites : testUserCanLoginWithValidCredentials
  • Une assertion par test (idĂ©alement)
  • Arrange, Act, Assert : structure claire
  • Tests rapides : privilĂ©gier les mocks

Structure AAA

public function testCalculateTotal(): void
{
    // Arrange
    $cart = new Cart();
    $cart->addItem(new Item(10.0));
    
    // Act
    $total = $cart->getTotal();
    
    // Assert
    $this->assertEquals(10.0, $total);
}

❌ À Ă©viter

  • Tests dĂ©pendants entre eux
  • Tests trop longs ou complexes
  • Tester l'implĂ©mentation plutĂŽt que le comportement
  • Ignorer les tests qui Ă©chouent
  • Pas de tests pour le code critique
Attention : Un test qui échoue parfois (flaky test) est souvent pire que pas de test du tout car il fait perdre confiance.
Écrivez vos tests AVANT ou en mĂȘme temps que votre code (TDD - Test Driven Development).

Aller plus loin

🔧 Outils complĂ©mentaires

📊 Couverture de code

php bin/phpunit --coverage-html coverage/

🚀 IntĂ©gration continue

# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'
      - run: composer install
      - run: php bin/phpunit

🎯 Prochaines Ă©tapes

  • Commencez par tester vos services mĂ©tier
  • Ajoutez des tests fonctionnels pour vos contrĂŽleurs
  • Visez 80% de couverture de code
  • IntĂ©grez les tests dans votre workflow
🎉 Vous ĂȘtes maintenant prĂȘts Ă  tester vos applications Symfony ! 🎉