Skip to content
✍🏼  Publié par Mathieu BESSON  📅  le 25/09/2024

Tests d'intégration avec Symfony

Pré-requis

Pour bien comprendre cet article et notamment pour mettre en application les exemples techniques, il peut être nécéssaire d'avoir au préalable consulté l'article sur la configuration et la mise en place d'un environnement de test avec Symfony.

C'est quoi ?

Au delà des tests unitaires testant simplement les entrées, sorties et comportemment de manière isolé des méthodes simples des classes d'une application, il est parfois nécéssaire de vérifier le fonctionnement d'un module ou d'un composant (ex : intéraction avec la BDD, méthodes dites "procédures", intéraction avec des services externes...).

Dans cette optique, les tests d'intégration permettent de vérifier le bon fontionnement d'un ensemble de modules ensemble, de manière isolé du reste de l'application.

Définition

Un test d'intégration vérifie le fonctionnement de plusieurs modules d'un logiciel ensemble.

Objectifs

  • Éviter les régressions.
  • Vérifier la communication inter-modulaire.
  • Valider les échanges de données entre composants

Indices de qualité

  • Fiable : Résultats similaires à chaque exécution, même avec plusieurs composants impliqués.
  • Représentatif : Reproduction de cas réels pour vérifier les intégrations dans des conditions proches de la production.
  • Automatisé : Exécutable sans actions manuelles.

Notions importantes

Exemples avec Symfony et PHPUnit

Les exemples suivants permettent de mettre en place des tests d'intégration dans une application Symfony à l'aide du framework de test PHPUnit. Les paragraphes suivants présentent donc les notions de base de ce framework de test (dans le cadre des tests d'intégration).

La classe KernelTestCase

La classe KernelTestCase issue du bundle symfony/test-pack de Symfony offre de multiples fonctionnalités tel que :

  • La possibilité d'accès au contenur de services de Symfony,
  • L'initialisation du noyau pour configuration de l'environnement (services, routes, bundles, injection de dépendance, variables d'env, )
  • L'intéraction avec la base de donnée via Doctrine

Accèder à un service dans un test

L'exemple suivant détail l'accès à un service depuis un test d'intégration.

  • Dans un premier temps, il est nécéssaire de démarrer le kernel de Symfony
  • Après récupération du contenur d'injection de dépendance, on peut initialiser le service souhaité
  • On peut ensuite effectuer des tests sur le service
<?php

class ArticleServiceTest extends KernelTestCase
{
    public function testAddArticle(): void
    {
        // Démarrage du kernel de Symfony
        self::bootKernel();

        // Récupération du contenur de service
        $container = static::getContainer();

        // Initialisation du service 
        $articleService = $container->get(ArticleService::class);
        $article = $articleService->addArticle("...", "...");

        $this->assertEquals("...", $article->getContent());
    }
}

Mocker un service ?

Dans certains cas, on peut avoir besoin de mocker un service utilisé par le service à tester.

Par exemple si ArticleService se sert de SlugGenerator, il sera nécéssaire de mocker SlugGenerator dans le test pour isoler le test des intéraction avec ce service.

Dans ce cas, la création du mock du service peut se faire de la manière suivante :

<?php

class ArticleServiceTest extends KernelTestCase
{
    public function testAddArticle(): void
    {
        // ...

        // Création du mock de SlugGenerator
        $slugGenerator = $this->createMock(SlugGenerator::class);
        $slugGenerator->expects(self::once())
            ->method('slugify')
            ->with($this->equalTo("How to make pancakes"))
            ->willReturn("how-to-make-pancakes");

        // Injection du mock du service SlugGenerator à la place du vrai service
        $container->set(SlugGenerator::class, $slugGenerator);

        // Récupération du service ArticleService
        $articleService = $container->get(ArticleService::class);

        // ...
    }
}

Exemple final de test d'intégration

En reprenant l'exemple précédant et les principes détaillés plus haut, le test suivant est en charge de vérifier le bon fonctionnement de la méthode addArticle().

Par étape, il est donc nécéssaire de vérifier que :

  • L'ajout de l'article fait bien appel au service SlugGenerator pour associer un slug à l'article.
  • Une entité Article est correctement hydraté et sauvegardé en base de donnée par la méthode.
<?php

// Service à tester
class ArticleService
{
    public function __construct(
        private EntityManagerInterface $entityManager, 
        private SlugGenerator $slugGenerator
    ) {}

    public function addArticle(string $title, string $content): Article
    {
        $article = new Article();
        $article->setTitle($title);
        $article->setContent($content);
        $article->setSlug($this->slugGenerator->slugify($title));

        $this->entityManager->persist($article);
        $this->entityManager->flush();

        return $article;
    }
}

// Classe de tests du service
class ArticleServiceTest extends KernelTestCase
{
    public function testAddArticle()
    {
        // Démarrage du kernel
        self::bootKernel();

        // Récupération du contenur d'injection de dépendance
        $container = self::$kernel->getContainer();
        $articleExpected = [
            "title" => "How to make pancakes", 
            "content" => "See in marmiton.com please, this is not a cooking site here",
            "slug" => "how-to-make-pancakes"
        ];

        // Création du mock de SlugGenerator
        $slugGenerator = $this->createMock(SlugGenerator::class);
        $slugGenerator->expects(self::once())
            ->method('slugify')
            ->with($this->equalTo($articleExpected["title"]))
            ->willReturn($articleExpected["slug"]);

        // Injection du mock du service SlugGenerator à la place du vrai service
        $container->set(SlugGenerator::class, $slugGenerator);

        // Récupération du service ArticleService
        $articleService = $container->get(ArticleService::class);

        // Test de la fonction addArticle()
        $article = $articleService->addArticle($articleExpected["title"], $articleExpected["content"]);

        // Vérification de l'ajout de l'article
        $this->assertInstanceOf(Article::class, $article);
        $this->assertEquals($articleExpected["title"], $article->getTitle());
        $this->assertEquals($articleExpected["content"], $article->getContent());

        // Vérification de la présence de l'article dans la base de donnée
        $entityManager = $container->get('doctrine')->getManager();
        $foundArticle = $entityManager->getRepository(Article::class)->find($article->getId());

        $this->assertNotNull($foundArticle);
        $this->assertEquals($title, $foundArticle->getTitle());
    }
}

Le test précédant test donc l'intégration de la méthode addArticle() avec la base de donnée et le service SlugGenerator. L'objectif établi est ici atteint, ce test permettant de valider le bon fontionnement de cette méthode dans son environnement proche, de manière isolé du reste de l'application.

La suite ? 🚀

Valider une fonctionnalité complète : Les tests fonctionnelles

À plus haut niveau, il est possible de tester et vérifier que des comportements ou fonctionnalités entières répondent correctement au besoin métier. Une doc est prévu à ce sujet sur l'implémentation des tests fonctionnelles dans une application Symfony. 🚀