Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Les tests dans une application Symfony

Les tests dans une application Symfony

Écrire des tests pour son projet est une assurance pour l'efficacité de développement et pour la chaîne de livraison du logiciel. Dans Symfony, on utilise PHPUnit pour cela, et on dispose de facilités pour écrire des tests fonctionnels.

En mettant en pratique la documentation, on se retrouve confronté à des questions comme : comment avoir une suite de tests maintenable ? dois-je faire des tests unitaires ou fonctionnels ? comment gérer les dépendances aux services tiers dans les tests ? comment je teste l'envoi de mails ? comment je dois organiser mes données de tests ?

Cette conférence est le résumé de plus de 10 ans de pratique de tests automatisés avec Symfony.

Alexandre Salomé

March 27, 2023
Tweet

More Decks by Alexandre Salomé

Other Decks in Programming

Transcript

  1. Les tests dans une
    application Symfony
    Alexandre Salomé

    View Slide

  2. Notre cas pratique
    Name:
    Email:
    Alice
    [email protected]
    Register
    Register

    View Slide

  3. Notre cas pratique
    Hello Alice,
    Thank you for registering! To confirm your
    registration, please click on the link below:
    Happy conferences,
    The speaker
    Click here to activate your account
    From: The App
    Subject: Finish your registration

    View Slide

  4. Application
    Exemple
    Request Response

    View Slide

  5. Application
    Service
    Exemple
    User
    Repository
    Request Response
    User

    View Slide

  6. Application
    Service
    Exemple
    User
    Repository
    Service
    Mailer
    Request Response
    User

    View Slide

  7. Application
    Service
    Exemple
    User
    Repository
    Service
    Mailer
    Service
    User
    Manager
    Request Response
    User

    View Slide

  8. Les différents types de tests
    3 types de test définis par Symfony :
    1. Les test unitaires
    2. Les tests d’intégration
    3. Les tests applicatifs
    Tests
    applicatifs
    Tests
    d’intégration
    Tests
    unitaires

    View Slide

  9. Les tests
    unitaires

    View Slide

  10. Un test unitaire garantit le fonctionnement d’une unité logicielle
    (une classe, une méthode).

    View Slide

  11. Application
    Service
    Les tests unitaires
    User
    Repository
    Service
    Mailer
    Service
    User
    Manager
    Request Response
    User

    View Slide

  12. Les tests unitaires
    use PHPUnit\Framework\TestCase;
    class UserTest extends TestCase
    {
    public function testActivate(): void
    {
    $user = new User();
    $this→assertFalse($user→isActivated());
    $user→activate();
    $this→assertTrue($user→isActivated());
    }
    }

    View Slide

  13. Les tests unitaires
    use PHPUnit\Framework\TestCase;
    class UserTest extends TestCase
    {
    public function testActivate(): void
    {
    $user = new User();
    $this→assertFalse($user→isActivated());
    $user→activate();
    $this→assertTrue($user→isActivated());
    }
    }

    View Slide

  14. Les tests unitaires
    use PHPUnit\Framework\TestCase;
    class UserTest extends TestCase
    {
    public function testActivate(): void
    {
    $user = new User();
    $this→assertFalse($user→isActivated());
    $user→activate();
    $this→assertTrue($user→isActivated());
    }
    }

    View Slide

  15. Les tests unitaires
    use PHPUnit\Framework\TestCase;
    class UserTest extends TestCase
    {
    public function testActivate(): void
    {
    $user = new User();
    $this→assertFalse($user→isActivated());
    $user→activate();
    $this→assertTrue($user→isActivated());
    }
    }

    View Slide

  16. Les tests unitaires
    use PHPUnit\Framework\TestCase;
    class UserTest extends TestCase
    {
    public function testActivate(): void
    public function testActivateTwice(): void
    public function testActivateWhenLocked(): void
    }

    View Slide

  17. Les tests unitaires
    Avantages
    - Rapides à exécuter
    - Simples et fondamental
    Inconvénients
    - Bouchons pour les dépendances
    17

    View Slide

  18. Les tests
    d’intégration

    View Slide

  19. Les tests d’intégration testent le bon fonctionnement
    d’une combinaison de services.

    View Slide

  20. Application
    Service
    Les tests d’intégration
    User
    Repository
    Service
    Mailer
    Service
    User
    Manager
    Request Response
    User

    View Slide

  21. Application
    Service
    Les tests d’intégration
    User
    Repository
    Service
    Mailer
    Service
    User
    Manager
    Request Response
    User

    View Slide

  22. use App\Entity\User;
    use App\Manager\UserManager;
    use App\Test\UserActionsTrait ;
    use Symfony\Bundle\FrameworkBundle \Test\KernelTestCase ;
    class RegistrationManagerTest extends KernelTestCase
    {
    use UserActionsTrait ; // Helper methods used below
    public function testRegisterExistingUser (): void
    {
    $this->createUser('alice');
    $user = new User();
    $user->setEmail('[email protected] ');
    $user->setName('Alice');
    $manager = self::getContainer()->get(UserManager::class);
    $manager->register($user);
    $this->assertAlreadyRegisteredMailSent ();
    }
    }

    View Slide

  23. use App\Entity\User;
    use App\Manager\UserManager;
    use App\Test\UserActionsTrait ;
    use Symfony\Bundle\FrameworkBundle \Test\KernelTestCase ;
    class RegistrationManagerTest extends KernelTestCase
    {
    use UserActionsTrait ; // Helper methods used below
    public function testRegisterExistingUser (): void
    {
    $this->createUser('alice');
    $user = new User();
    $user->setEmail('[email protected] ');
    $user->setName('Alice');
    $manager = self::getContainer()->get(UserManager::class);
    $manager->register($user);
    $this->assertAlreadyRegisteredMailSent ();
    }
    }

    View Slide

  24. use App\Entity\User;
    use App\Manager\UserManager;
    use App\Test\UserActionsTrait ;
    use Symfony\Bundle\FrameworkBundle \Test\KernelTestCase ;
    class RegistrationManagerTest extends KernelTestCase
    {
    use UserActionsTrait ; // Helper methods used below
    public function testRegisterExistingUser (): void
    {
    $this->createUser('alice');
    $user = new User();
    $user->setEmail('[email protected] ');
    $user->setName('Alice');
    $manager = self::getContainer()->get(UserManager::class);
    $manager->register($user);
    $this->assertAlreadyRegisteredMailSent ();
    }
    }

    View Slide

  25. use App\Entity\User;
    use App\Manager\UserManager;
    use App\Test\UserActionsTrait ;
    use Symfony\Bundle\FrameworkBundle \Test\KernelTestCase ;
    class RegistrationManagerTest extends KernelTestCase
    {
    use UserActionsTrait ; // Helper methods used below
    public function testRegisterExistingUser (): void
    {
    $this->createUser('alice');
    $user = new User();
    $user->setEmail('[email protected] ');
    $user->setName('Alice');
    $manager = self::getContainer()->get(UserManager::class);
    $manager->register($user);
    $this->assertAlreadyRegisteredMailSent ();
    }
    }

    View Slide

  26. use App\Entity\User;
    use App\Manager\UserManager;
    use App\Test\UserActionsTrait ;
    use Symfony\Bundle\FrameworkBundle \Test\KernelTestCase ;
    class RegistrationManagerTest extends KernelTestCase
    {
    use UserActionsTrait ; // Helper methods used below
    public function testRegisterExistingUser (): void
    {
    $this->createUser('alice');
    $user = new User();
    $user->setEmail('[email protected] ');
    $user->setName('Alice');
    $manager = self::getContainer()->get(UserManager::class);
    $manager->register($user);
    $this->assertAlreadyRegisteredMailSent ();
    }
    }

    View Slide

  27. use App\Entity\User;
    use App\Manager\UserManager;
    use App\Test\UserActionsTrait ;
    use Symfony\Bundle\FrameworkBundle \Test\KernelTestCase ;
    class RegistrationManagerTest extends KernelTestCase
    {
    use UserActionsTrait ; // Helper methods used below
    public function testRegisterExistingUser (): void
    {
    $this->createUser('alice');
    $user = new User();
    $user->setEmail('[email protected] ');
    $user->setName('Alice');
    $manager = self::getContainer()->get(UserManager::class);
    $manager->register($user);
    $this->assertAlreadyRegisteredMailSent ();
    }
    }

    View Slide

  28. use App\Entity\User;
    use App\Manager\UserManager;
    use App\Test\UserActionsTrait ;
    use Symfony\Bundle\FrameworkBundle \Test\KernelTestCase ;
    class RegistrationManagerTest extends KernelTestCase
    {
    use UserActionsTrait ; // Helper methods used below
    public function testRegisterExistingUser (): void
    {
    $this->createUser('alice');
    $user = new User();
    $user->setEmail('[email protected] ');
    $user->setName('Alice');
    $manager = self::getContainer()->get(UserManager::class);
    $manager->register($user);
    $this->assertAlreadyRegisteredMailSent ();
    }
    }

    View Slide

  29. Les tests d’intégration
    Avantages
    - Permet de tester n’importe quel service
    - Conditions réelles du service
    Inconvénients
    - Couplage du service non visible
    29

    View Slide

  30. Les tests
    applicatifs

    View Slide

  31. Les tests applicatifs testent le bon fonctionnement
    des différentes couches de l’application
    combinées ensemble.

    View Slide

  32. Application
    Service
    Les tests applicatifs
    User
    Repository
    Service
    Mailer
    Service
    User
    Manager
    Request Response
    User

    View Slide

  33. use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
    class RegisterControllerTest extends WebTestCase
    {
    public function testValid(): void
    {
    $client = self::createClient();
    $crawler = $client→request('GET', '/register');
    $form = $crawler→filter('main form')→form([
    'register[name]' => 'Test User',
    'register[email]' => '[email protected]',
    ]);
    $client→submit($form);
    $this→assertResponseStatusCodeSame(302);
    $this→assertResponseRedirects('/register/confirmation');
    }
    }

    View Slide

  34. use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
    class RegisterControllerTest extends WebTestCase
    {
    public function testValid(): void
    {
    $client = self::createClient();
    $crawler = $client→request('GET', '/register');
    $form = $crawler→filter('main form')→form([
    'register[name]' => 'Test User',
    'register[email]' => '[email protected]',
    ]);
    $client→submit($form);
    $this→assertResponseStatusCodeSame(302);
    $this→assertResponseRedirects('/register/confirmation');
    }
    }

    View Slide

  35. use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
    class RegisterControllerTest extends WebTestCase
    {
    public function testValid(): void
    {
    $client = self::createClient();
    $crawler = $client→request('GET', '/register');
    $form = $crawler→filter('main form')→form([
    'register[name]' => 'Test User',
    'register[email]' => '[email protected]',
    ]);
    $client→submit($form);
    $this→assertResponseStatusCodeSame(302);
    $this→assertResponseRedirects('/register/confirmation');
    }
    }

    View Slide

  36. use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
    class RegisterControllerTest extends WebTestCase
    {
    public function testValid(): void
    {
    $client = self::createClient();
    $crawler = $client→request('GET', '/register');
    $form = $crawler→filter('main form')→form([
    'register[name]' => 'Test User',
    'register[email]' => '[email protected]',
    ]);
    $client→submit($form);
    $this→assertResponseStatusCodeSame(302);
    $this→assertResponseRedirects('/register/confirmation');
    }
    }

    View Slide

  37. WebTestCase - BrowserKitAssertionsTrait

    View Slide

  38. PHPUnit 9.5.28 by Sebastian Bergmann and contributors.
    Testing App\Tests\Manager\User\RegistrationManagerTest
    F 1 / 1 (100%)
    Time: 00:00.413, Memory: 32.00 MB
    There was 1 failure:
    1) App\Tests\Manager\User\RegistrationManagerTest::testRegister
    Failed asserting that Symfony\Component\HttpFoundation\RedirectResponse
    Object ... (
    # ...
    'targetUrl' => '/register/actual'
    ) is redirected and has header "Location" with value "/register/expected".

    View Slide

  39. WebTestCase - DomCrawlerAssertionsTrait

    View Slide

  40. PHPUnit 9.5.28 by Sebastian Bergmann and contributors.
    Testing App\Tests\Manager\User\RegistrationManagerTest
    F 1 / 1 (100%)
    Time: 00:00.413, Memory: 32.00 MB
    There was 1 failure:
    1) App\Tests\Manager\User\RegistrationManagerTest::testRegister
    Failed asserting that Symfony\Component\DomCrawler\Crawler Object ... (
    ) matches selector "h1" and the text "register.title" of the node matching
    selector "body" contains "Register".

    View Slide

  41. Les tests applicatifs
    Avantages
    - Tests de l’extérieur de l’application
    - Utilitaires de WebTestCase
    Inconvénients
    - Tests gourmands en ressources
    41

    View Slide

  42. Les outils de Symfony

    View Slide

  43. Installer PHPUnit
    Utilisez le pack de test de Symfony :
    composer require --dev symfony/test-pack
    Cela installe PHPUnit ainsi que les outils que nous allons voir.

    View Slide

  44. Tester l’envoi
    d’emails

    View Slide

  45. Tester l’envoi d’emails
    public function testRegister(): void
    {
    // Fill and submit the form
    $crawler = $this→client→request('GET', '/register');
    $form = $crawler→filter('form')→form([
    'register[name]' => 'Alice',
    'register[email]' => '[email protected]',
    ]);
    $this→client→submit($form);
    $this→assertResponseIsSuccessful();
    }

    View Slide

  46. Tester l’envoi d’emails
    Hello Alice,
    Thank you for registering! To confirm your
    registration, please click on the link below:
    Happy conferences,
    The speaker
    Click here to activate your account
    From: The App
    Subject: Finish your registration

    View Slide

  47. // Verify that the registration mail has been sent
    $this→assertQueuedEmailCount(1);
    // Get the email
    $email = $this→getMailerMessage();
    // Assertions on the mail
    $this→assertEmailHeaderSame(
    $email,
    'Subject',
    'Finish your registration'
    );
    $this→assertEmailHtmlBodyContains(
    $email,
    'Click here to activate your account'
    );

    View Slide

  48. // Verify that the registration mail has been sent
    $this→assertQueuedEmailCount(1);
    // Get the email
    $email = $this→getMailerMessage();
    // Assertions on the mail
    $this→assertEmailHeaderSame(
    $email,
    'Subject',
    'Finish your registration'
    );
    $this→assertEmailHtmlBodyContains(
    $email,
    'Click here to activate your account'
    );

    View Slide

  49. // Verify that the registration mail has been sent
    $this→assertQueuedEmailCount(1);
    // Get the email
    $email = $this→getMailerMessage();
    // Assertions on the mail
    $this→assertEmailHeaderSame(
    $email,
    'Subject',
    'Finish your registration'
    );
    $this→assertEmailHtmlBodyContains(
    $email,
    'Click here to activate your account'
    );

    View Slide

  50. // Verify that the registration mail has been sent
    $this→assertQueuedEmailCount(1);
    // Get the email
    $email = $this→getMailerMessage();
    // Assertions on the mail
    $this→assertEmailHeaderSame(
    $email,
    'Subject',
    'Finish your registration'
    );
    $this→assertEmailHtmlBodyContains(
    $email,
    'Click here to activate your account'
    );

    View Slide

  51. // Verify that the registration mail has been sent
    $this→assertQueuedEmailCount(1);
    // Get the email
    $email = $this→getMailerMessage();
    // Assertions on the mail
    $this→assertEmailHeaderSame(
    $email,
    'Subject',
    'Finish your registration'
    );
    $this→assertEmailHtmlBodyContains(
    $email,
    'Click here to activate your account'
    );

    View Slide

  52. Tester l’envoi d’emails - MailerAssertionsTrait

    View Slide

  53. Tester l’envoi d’emails
    Hello Alice,
    Thank you for registering! To confirm your
    registration, please click on the link below:
    Happy conferences,
    The speaker
    Click here to activate your account
    From: The App
    Subject: Finish your registration

    View Slide

  54. public function testRegister(): void
    {
    // ...
    // Extract the link
    $link = (new Crawler($email->getHtmlBody()))
    ->filter('a:contains("Click here to activate your account")' )
    ;
    $href = $link->attr('href');
    // Browse the link
    $response = $this->client->request('GET', $href);
    $this->assertResponseIsSuccessful ();
    }

    View Slide

  55. public function testRegister(): void
    {
    // ...
    // Extract the link
    $link = (new Crawler($email->getHtmlBody()))
    ->filter('a:contains("Click here to activate your account")' )
    ;
    $href = $link->attr('href');
    // Browse the link
    $response = $this->client->request('GET', $href);
    $this->assertResponseIsSuccessful ();
    }

    View Slide

  56. public function testRegister(): void
    {
    // ...
    // Extract the link
    $link = (new Crawler($email->getHtmlBody()))
    ->filter('a:contains("Click here to activate your account")' )
    ;
    $href = $link->attr('href');
    // Browse the link
    $response = $this->client->request('GET', $href);
    $this->assertResponseIsSuccessful ();
    }

    View Slide

  57. public function testRegister(): void
    {
    // ...
    // Extract the link
    $link = (new Crawler($email->getHtmlBody()))
    ->filter('a:contains("Click here to activate your account")' )
    ;
    $href = $link->attr('href');
    // Browse the link
    $response = $this->client->request('GET', $href);
    $this->assertResponseIsSuccessful ();
    }

    View Slide

  58. public function testRegister(): void
    {
    // ...
    // Extract the link
    $link = (new Crawler($email->getHtmlBody()))
    ->filter('a:contains("Click here to activate your account")' )
    ;
    $href = $link->attr('href');
    // Browse the link
    $response = $this->client->request('GET', $href);
    $this->assertResponseIsSuccessful ();
    }

    View Slide

  59. Tests Messenger

    View Slide

  60. Tests Messenger
    Supposons que notre UserManager a la méthode suivante :
    public function register(User $user): void
    {
    $this→repository→save($user);
    $this→sendRegistrationMail($user);
    $this→messageBus→dispatch(
    new RegistrationMessage($user→getId()),
    );
    }

    View Slide

  61. Tests Messenger
    Supposons que notre UserManager a la méthode suivante :
    public function register(User $user): void
    {
    $this→repository→save($user);
    $this→sendRegistrationMail($user);
    $this→messageBus→dispatch(
    new RegistrationMessage($user→getId()),
    );
    }

    View Slide

  62. Tests Messenger
    # app/config/messenger.yaml
    framework:
    messenger:
    transports:
    async: '%env(MESSENGER_TRANSPORT_DSN)%'
    [email protected]:
    framework:
    messenger:
    transports:
    async: 'in-memory://'

    View Slide

  63. Tests Messenger
    On peut vérifier l’envoi d’un événement via le transport “in-memory”:
    $transport = self::getContainer()->get('messenger.transport.async' );
    $this->assertCount(1, $transport->getSent());
    $message = $transport->getSent()[0]->getMessage();
    $this->assertInstanceOf (RegistrationMessage ::class, $message);

    View Slide

  64. Tests Messenger
    On peut vérifier l’envoi d’un événement via le transport “in-memory”:
    $transport = self::getContainer()->get('messenger.transport.async' );
    $this->assertCount(1, $transport->getSent());
    $message = $transport->getSent()[0]->getMessage();
    $this->assertInstanceOf (RegistrationMessage ::class, $message);

    View Slide

  65. Tests Messenger
    On peut vérifier l’envoi d’un événement via le transport “in-memory”:
    $transport = self::getContainer()->get('messenger.transport.async' );
    $this->assertCount(1, $transport->getSent());
    $message = $transport->getSent()[0]->getMessage();
    $this->assertInstanceOf (RegistrationMessage ::class, $message);

    View Slide

  66. Tests Messenger
    On peut vérifier l’envoi d’un événement via le transport “in-memory”:
    $transport = self::getContainer()->get('messenger.transport.async' );
    $this->assertCount(1, $transport->getSent());
    $message = $transport->getSent()[0]->getMessage();
    $this->assertInstanceOf (RegistrationMessage ::class, $message);

    View Slide

  67. Clock mocking

    View Slide

  68. Clock mocking
    Supposons qu’on veuille faire le test suivant:
    public function testIsActivationKeyValid (): void
    {
    $user = new User();
    $key = $user->getActivationKey ();
    $this->assertTrue($user->isActivationKeyValid ($key));
    sleep(2 * 24 * 60 * 60); // 2 days
    $this->assertFalse($user->isActivationKeyValid ($key));
    }

    View Slide

  69. Clock mocking
    Supposons qu’on veuille faire le test suivant:
    public function testIsActivationKeyValid (): void
    {
    $user = new User();
    $key = $user->getActivationKey ();
    $this->assertTrue($user->isActivationKeyValid ($key));
    sleep(2 * 24 * 60 * 60); // 2 days
    $this->assertFalse($user->isActivationKeyValid ($key));
    }

    View Slide

  70. Clock mocking
    Supposons qu’on veuille faire le test suivant:
    public function testIsActivationKeyValid (): void
    {
    $user = new User();
    $key = $user->getActivationKey ();
    $this->assertTrue($user->isActivationKeyValid ($key));
    sleep(2 * 24 * 60 * 60); // 2 days
    $this->assertFalse($user->isActivationKeyValid ($key));
    }

    View Slide

  71. Clock mocking
    Supposons qu’on veuille faire le test suivant:
    public function testIsActivationKeyValid (): void
    {
    $user = new User();
    $key = $user->getActivationKey ();
    $this->assertTrue($user->isActivationKeyValid ($key));
    sleep(2 * 24 * 60 * 60); // 2 days
    $this->assertFalse($user->isActivationKeyValid ($key));
    }

    View Slide

  72. Clock mocking
    Supposons qu’on veuille faire le test suivant:
    public function testIsActivationKeyValid (): void
    {
    $user = new User();
    $key = $user->getActivationKey ();
    $this->assertTrue($user->isActivationKeyValid ($key));
    sleep(2 * 24 * 60 * 60); // 2 days
    $this->assertFalse($user->isActivationKeyValid ($key));
    }

    View Slide

  73. Clock mocking
    Grâce au test-pack de Symfony, nous n’avons qu’à mettre une annotation:
    /**
    * @group time-sensitive
    */
    public function testIsActivationKeyValid (): void
    {
    $user = new User();
    $key = $user->getActivationKey ();
    $this->assertTrue($user->isActivationKeyValid ($key));
    sleep(2 * 24 * 60 * 60); // 2 days
    $this->assertFalse($user->isActivationKeyValid ($key));
    }

    View Slide

  74. Cette annotation va déclarer des bouchons pour time(), sleep(), usleep()
    et date() dans les namespaces de la classe testée :
    - App\Entity\
    - App\Tests\Entity\
    Ainsi les appels de fonction dans User & UserTest utiliseront les déclarations de ce
    namespace.
    Clock mocking

    View Slide

  75. Clock mocking
    Mais…
    Il faut utiliser les fonctions : time(), sleep(), usleep() et date()
    Par exemple, pour DateTime et DateTimeImmutable:
    \DateTimeImmutable ::createFromFormat('U', time());
    \DateTimeImmutable ::createFromFormat('U', time())
    ->add(new \DateInterval('P2D'))
    ;

    View Slide

  76. Test unitaire de
    formulaire

    View Slide

  77. Bonjour SetPasswordType
    Un FormType de Symfony qui s’occupe du changement de mot de passe :
    $form = $this->createForm(SetPasswordType ::class, $user);
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
    $userManager->saveUser($user);
    }

    View Slide

  78. Bonjour SetPasswordType
    Un FormType de Symfony qui s’occupe du changement de mot de passe:
    ->addEventListener (FormEvents::POST_SUBMIT, function (PostSubmitEvent $e) {
    if (!$e->getForm()->isValid()) {
    Return;
    }
    $user = $e->getData();
    $password = $e->getForm()->get('password')->getData();
    $hashed = $this->userPasswordHasher ->hashPassword($user, $password);
    $user->setPassword($hashed);
    })

    View Slide

  79. use Symfony\Component\Form\Test\TypeTestCase;
    class SetPasswordTypeTest extends TypeTestCase
    {
    public function testValid(): void
    {
    $user = new User();
    $user→setPassword('foo');
    $form = $this→factory→create(SetPasswordType::class, $user);
    $form→submit(['password' => [
    'first' => 'bar',
    'second' => 'bar',
    ]]);
    $this→assertEquals('fooHashed', $user→getPassword());
    }

    View Slide

  80. use Symfony\Component\Form\Test\TypeTestCase;
    class SetPasswordTypeTest extends TypeTestCase
    {
    public function testValid(): void
    {
    $user = new User();
    $user→setPassword('fooHashed');
    $form = $this→factory→create(SetPasswordType::class, $user);
    $form→submit(['password' => [
    'first' => 'bar',
    'second' => 'bar',
    ]]);
    $this→assertEquals('barHashed', $user→getPassword());
    }

    View Slide

  81. use Symfony\Component\Form\Test\TypeTestCase;
    class SetPasswordTypeTest extends TypeTestCase
    {
    public function testValid(): void
    {
    $user = new User();
    $user→setPassword('fooHashed');
    $form = $this→factory→create(SetPasswordType::class, $user);
    $form→submit(['password' => [
    'first' => 'bar',
    'second' => 'bar',
    ]]);
    $this→assertEquals('barHashed', $user→getPassword());
    }

    View Slide

  82. use Symfony\Component\Form\Test\TypeTestCase;
    class SetPasswordTypeTest extends TypeTestCase
    {
    public function testValid(): void
    {
    $user = new User();
    $user→setPassword('fooHashed');
    $form = $this→factory→create(SetPasswordType::class, $user);
    $form→submit(['password' => [
    'first' => 'bar',
    'second' => 'bar',
    ]]);
    $this→assertEquals('barHashed', $user→getPassword());
    }

    View Slide

  83. use Symfony\Component\Form\Test\TypeTestCase;
    class SetPasswordTypeTest extends TypeTestCase
    {
    public function testValid(): void
    {
    $user = new User();
    $user→setPassword('fooHashed');
    $form = $this→factory→create(SetPasswordType::class, $user);
    $form→submit(['password' => [
    'first' => 'bar',
    'second' => 'bar',
    ]]);
    $this→assertEquals('barHashed', $user→getPassword());
    }

    View Slide

  84. class SetPasswordTypeTest extends TypeTestCase
    {
    private UserPasswordHasherInterface|MockObject $passwordHasher;
    protected function getExtensions(): array
    {
    $this→passwordHasher =
    $this→createMock(UserPasswordHasherInterface::class)
    ;
    $type = new SetPasswordType($this→passwordHasher);
    return [
    new PreloadedExtension([
    $type
    ], []),
    ];
    }

    View Slide

  85. class SetPasswordTypeTest extends TypeTestCase
    {
    private UserPasswordHasherInterface|MockObject $passwordHasher;
    protected function getExtensions(): array
    {
    $this→passwordHasher =
    $this→createMock(UserPasswordHasherInterface::class)
    ;
    $type = new SetPasswordType($this→passwordHasher);
    return [
    new PreloadedExtension([
    $type
    ], []),
    ];
    }

    View Slide

  86. class SetPasswordTypeTest extends TypeTestCase
    {
    private UserPasswordHasherInterface|MockObject $passwordHasher;
    protected function getExtensions(): array
    {
    $this→passwordHasher =
    $this→createMock(UserPasswordHasherInterface::class)
    ;
    $type = new SetPasswordType($this→passwordHasher);
    return [
    new PreloadedExtension([
    $type
    ], []),
    ];
    }

    View Slide

  87. class SetPasswordTypeTest extends TypeTestCase
    {
    private UserPasswordHasherInterface|MockObject $passwordHasher;
    protected function getExtensions(): array
    {
    $this→passwordHasher =
    $this→createMock(UserPasswordHasherInterface::class)
    ;
    $type = new SetPasswordType($this→passwordHasher);
    return [
    new PreloadedExtension([
    $type
    ], []),
    ];
    }

    View Slide

  88. On ajoute finalement les attentes sur le bouchon dans le test :
    public function testValid(): void
    {
    $user = new User();
    $this→passwordHasher
    →expects($this→once())
    →method('hashPassword')
    →with($user, 'bar')
    →willReturn('barHashed')
    ;
    # ...
    Test unitaire de formulaire

    View Slide

  89. $user = new User();
    $user→setPassword('fooHashed');
    $this→passwordHasher
    →expects($this→once())
    →method('hashPassword')
    →with($user, 'bar')
    →willReturn('barHashed')
    ;
    $form = $this→factory→create(SetPasswordType::class, $user);
    $form→submit(['password' => [
    'first' => 'bar',
    'second' => 'bar',
    ]]);
    $this→assertTrue($form→isSynchronized());
    $this→assertEquals('barHashed', $user→getPassword());

    View Slide

  90. $user = new User();
    $user→setPassword('fooHashed');
    $this→passwordHasher
    →expects($this→once())
    →method('hashPassword')
    →with($user, 'bar')
    →willReturn('barHashed')
    ;
    $form = $this→factory→create(SetPasswordType::class, $user);
    $form→submit(['password' => [
    'first' => 'bar',
    'second' => 'bar',
    ]]);
    $this→assertTrue($form→isSynchronized());
    $this→assertEquals('barHashed', $user→getPassword());

    View Slide

  91. $user = new User();
    $user→setPassword('fooHashed');
    $this→passwordHasher
    →expects($this→once())
    →method('hashPassword')
    →with($user, 'bar')
    →willReturn('barHashed')
    ;
    $form = $this→factory→create(SetPasswordType::class, $user);
    $form→submit(['password' => [
    'first' => 'bar',
    'second' => 'bar',
    ]]);
    $this→assertTrue($form→isSynchronized());
    $this→assertEquals('barHashed', $user→getPassword());

    View Slide

  92. $user = new User();
    $user→setPassword('fooHashed');
    $this→passwordHasher
    →expects($this→once())
    →method('hashPassword')
    →with($user, 'bar')
    →willReturn('barHashed')
    ;
    $form = $this→factory→create(SetPasswordType::class, $user);
    $form→submit(['password' => [
    'first' => 'bar',
    'second' => 'bar',
    ]]);
    $this→assertTrue($form→isSynchronized());
    $this→assertEquals('barHashed', $user→getPassword());

    View Slide

  93. $user = new User();
    $user→setPassword('fooHashed');
    $this→passwordHasher
    →expects($this→once())
    →method('hashPassword')
    →with($user, 'bar')
    →willReturn('barHashed')
    ;
    $form = $this→factory→create(SetPasswordType::class, $user);
    $form→submit(['password' => [
    'first' => 'bar',
    'second' => 'bar',
    ]]);
    $this→assertTrue($form→isSynchronized());
    $this→assertEquals('barHashed', $user→getPassword());

    View Slide

  94. Tester les
    commandes

    View Slide

  95. Bonjour UserActivateCommand
    On imagine une commande bin/console user:activate [email protected]
    - [ERROR] No user found with email
    "[email protected]".
    - [INFO] The user with email "[email protected]" is
    already activated.
    - [OK] The user with email "[email protected]" has
    been activated.

    View Slide

  96. class ActivateCommandTest extends KernelTestCase
    {
    public function testAlreadyActivated(): void
    {
    // Prepare the data
    $this→user→activate();
    $this→manager→saveUser($this→user);
    $app = new Application(self::bootKernel());
    $command = $app→find('user:activate');
    $tester = new CommandTester($command);
    $tester→execute(['email' => '[email protected]']);
    $tester→assertCommandIsSuccessful();
    $this→assertStringContainsString(
    'already activated',
    $tester→getDisplay(),
    );
    }

    View Slide

  97. class ActivateCommandTest extends KernelTestCase
    {
    public function testAlreadyActivated(): void
    {
    // Prepare the data
    $this→user→activate();
    $this→manager→saveUser($this→user);
    $app = new Application(self::bootKernel());
    $command = $app→find('user:activate');
    $tester = new CommandTester($command);
    $tester→execute(['email' => '[email protected]']);
    $tester→assertCommandIsSuccessful();
    $this→assertStringContainsString(
    'already activated',
    $tester→getDisplay(),
    );
    }

    View Slide

  98. class ActivateCommandTest extends KernelTestCase
    {
    public function testAlreadyActivated(): void
    {
    // Prepare the data
    $this→user→activate();
    $this→manager→saveUser($this→user);
    $app = new Application(self::bootKernel());
    $command = $app→find('user:activate');
    $tester = new CommandTester($command);
    $tester→execute(['email' => '[email protected]']);
    $tester→assertCommandIsSuccessful();
    $this→assertStringContainsString(
    'already activated',
    $tester→getDisplay(),
    );
    }

    View Slide

  99. class ActivateCommandTest extends KernelTestCase
    {
    public function testAlreadyActivated(): void
    {
    // Prepare the data
    $this→user→activate();
    $this→manager→saveUser($this→user);
    $app = new Application(self::bootKernel());
    $command = $app→find('user:activate');
    $tester = new CommandTester($command);
    $tester→execute(['email' => '[email protected]']);
    $tester→assertCommandIsSuccessful();
    $this→assertStringContainsString(
    'already activated',
    $tester→getDisplay(),
    );
    }

    View Slide

  100. class ActivateCommandTest extends KernelTestCase
    {
    public function testAlreadyActivated(): void
    {
    // Prepare the data
    $this→user→activate();
    $this→manager→saveUser($this→user);
    $app = new Application(self::bootKernel());
    $command = $app→find('user:activate');
    $tester = new CommandTester($command);
    $tester→execute(['email' => '[email protected]']);
    $tester→assertCommandIsSuccessful();
    $this→assertStringContainsString(
    'already activated',
    $tester→getDisplay(),
    );
    }

    View Slide

  101. class ActivateCommandTest extends KernelTestCase
    {
    public function testAlreadyActivated(): void
    {
    // Prepare the data
    $this→user→activate();
    $this→manager→saveUser($this→user);
    $app = new Application(self::bootKernel());
    $command = $app→find('user:activate');
    $tester = new CommandTester($command);
    $tester→execute(['email' => '[email protected]']);
    $tester→assertCommandIsSuccessful();
    $this→assertStringContainsString(
    'already activated',
    $tester→getDisplay(),
    );
    }

    View Slide

  102. CommandTester

    View Slide

  103. Testing Stimulus

    View Slide

  104. Symfony UX Stimulus testing est considéré actuellement comme expérimental.
    Symfony UX Stimulus testing

    View Slide

  105. import { Application } from '@hotwired/stimulus';
    import { clearDOM, mountDOM } from '@symfony/stimulus-testing';
    import HelloController from '../controllers/hello_controller.js';
    const startStimulus = () => {
    const application = Application.start();
    application.register('hello', HelloController);
    };
    describe('HelloController', () => {
    let container;
    beforeEach(() => {
    startStimulus();
    container = mountDOM('');
    });
    afterEach(() => {
    clearDOM();
    });
    // Test here
    });

    View Slide

  106. import { Application } from '@hotwired/stimulus';
    import { clearDOM, mountDOM } from '@symfony/stimulus-testing';
    import HelloController from '../controllers/hello_controller.js';
    const startStimulus = () => {
    const application = Application.start();
    application.register('hello', HelloController);
    };
    describe('HelloController', () => {
    let container;
    beforeEach(() => {
    startStimulus();
    container = mountDOM('');
    });
    afterEach(() => {
    clearDOM();
    });
    // Test here
    });

    View Slide

  107. describe('HelloController', () => {
    // ...
    it('connect', async () => {
    expect(container.textContent).toContain("Hello world");
    });
    });

    View Slide

  108. Le client HTTP

    View Slide

  109. Un CRM vers lequel nous synchronisons les utilisateurs par un appel HTTP.
    class SalesPower
    {
    public function __construct(
    private readonly HttpClientInterface $client,
    ) { }
    public function createUser(User $user): void
    {
    $this→client→request('PUT', '/users', ...);
    }
    }
    Bonjour SalesPower

    View Slide

  110. use Symfony\Component\HttpClient\MockHttpClient;
    $mock = new MockHttpClient(new MockResponse('OK'));
    $crm = new SalesPower($mock);
    $crm→createUser($alice);
    Méthode simple

    View Slide

  111. $count = 0;
    $mock = new MockHttpClient(
    function (string $method, string $path) use (&$count) {
    $this→assertSame(0, $count++);
    $this→assertSame('PUT', $method);
    $this→assertSame('/users', $path);
    return new MockResponse('OK');
    }
    );
    $crm = new SalesPower($mock);
    $crm→createUser($alice);
    $this→assertSame(1, $count, 'Effective call to the HTTP client');
    Méthode avancée

    View Slide

  112. $count = 0;
    $mock = new MockHttpClient(
    function (string $method, string $path) use (&$count) {
    $this→assertSame(0, $count++);
    $this→assertSame('PUT', $method);
    $this→assertSame('/users', $path);
    return new MockResponse('OK');
    }
    );
    $crm = new SalesPower($mock);
    $crm→createUser($alice);
    $this→assertSame(1, $count, 'Effective call to the HTTP client');
    Méthode avancée

    View Slide

  113. $count = 0;
    $mock = new MockHttpClient(
    function (string $method, string $path) use (&$count) {
    $this→assertSame(0, $count++);
    $this→assertSame('PUT', $method);
    $this→assertSame('/users', $path);
    return new MockResponse('OK');
    }
    );
    $crm = new SalesPower($mock);
    $crm→createUser($alice);
    $this→assertSame(1, $count, 'Effective call to the HTTP client');
    Méthode avancée

    View Slide

  114. Aller plus loin

    View Slide

  115. Bouchonner les
    services

    View Slide

  116. $repository = $this→getMockBuilder(UserRepository::class)
    →disableOriginalConstructor()
    →getMock()
    ;
    $repository
    →expects($this→once())
    →method('findOneBy')
    →with(['email' => '[email protected]'])
    →willReturn($alice)
    ;
    self::getContainer()→set(UserRepository::class, $repository);
    $manager = self::getContainer()→get(UserManager::class);
    $manager→recoverPassword('[email protected]');
    $this→assertQueuedEmailCount(1);

    View Slide

  117. $repository = $this→getMockBuilder(UserRepository::class)
    →disableOriginalConstructor()
    →getMock()
    ;
    $repository
    →expects($this→once())
    →method('findOneBy')
    →with(['email' => '[email protected]'])
    →willReturn($alice)
    ;
    self::getContainer()→set(UserRepository::class, $repository);
    $manager = self::getContainer()→get(UserManager::class);
    $manager→recoverPassword('[email protected]');
    $this→assertQueuedEmailCount(1);

    View Slide

  118. $repository = $this→getMockBuilder(UserRepository::class)
    →disableOriginalConstructor()
    →getMock()
    ;
    $repository
    →expects($this→once())
    →method('findOneBy')
    →with(['email' => '[email protected]'])
    →willReturn($alice)
    ;
    self::getContainer()→set(UserRepository::class, $repository);
    $manager = self::getContainer()→get(UserManager::class);
    $manager→recoverPassword('[email protected]');
    $this→assertQueuedEmailCount(1);

    View Slide

  119. $repository = $this→getMockBuilder(UserRepository::class)
    →disableOriginalConstructor()
    →getMock()
    ;
    $repository
    →expects($this→once())
    →method('findOneBy')
    →with(['email' => '[email protected]'])
    →willReturn($alice)
    ;
    self::getContainer()→set(UserRepository::class, $repository);
    $manager = self::getContainer()→get(UserManager::class);
    $manager→recoverPassword('[email protected]');
    $this→assertQueuedEmailCount(1);

    View Slide

  120. Profiler
    collectors

    View Slide

  121. Profiler collectors
    La magie des tests est rendue possible grâce aux collectors:
    $this→client→enableProfiler();
    $this→client→request('GET', '/');
    $collectors = $this→client→getProfile()→getCollectors();

    View Slide

  122. - Request
    - Time
    - Memory
    - Validator
    - Ajax
    - Form
    - Exception
    - Logger
    - Events
    Les collectors disponibles
    - Router
    - Cache
    - Security
    - Twig
    - Http Client
    - Database
    - Messenger
    - Mailer
    - Config

    View Slide

  123. On vérifie qu’aucune requête HTTP n’a été générée:
    $collectors = $client→getProfile()→getCollectors();
    /** @var HttpClientDataCollector $httpClientCollector */
    $httpClientCollector = $collectors['http_client'];
    $this→assertEquals(0, $httpClientCollector→getRequestCount());
    Exemple avec “http_client”

    View Slide

  124. Données de test
    aka “fixtures”

    View Slide

  125. Fixtures
    - Utile pour le développement

    View Slide

  126. Fixtures
    - Utile pour le développement
    - Risqué pour les tests automatisés (modification des données)

    View Slide

  127. Fixtures
    - Utile pour le développement
    - Risqué pour les tests automatisés (modification des données)
    - Préférez les tests auto-suffisants en données

    View Slide

  128. Test auto-suffisant supprimant des données
    public function setUp(): void
    {
    $this→client = static::createClient();
    $userManager = self::getContainer()→get(UserManager::class);
    try {
    $user = $userManager→findOneByEmail('[email protected]');
    $userManager→deleteUser($user);
    } catch (\InvalidArgumentException) {
    // Ignore
    }
    }

    View Slide

  129. Dépendances
    externes

    View Slide

  130. Abstraire les services externes
    Exemple industrialisé : Symfony Mailer

    View Slide

  131. Abstraire les services externes
    Exemple industrialisé : Symfony Mailer
    Exemple minimaliste: une API tierce de CRM :
    - PUT /user quand un utilisateur est créé
    - DELETE /user/ quand un utilisateur est supprimé

    View Slide

  132. namespace App\Crm;
    interface CrmInterface
    {
    /** @throws ApiException */
    public function createUser(User $user): void;
    /** @throws ApiException */
    public function deleteUser(string $id): void;
    }
    Créez le contrat d’interface

    View Slide

  133. namespace App\Crm;
    class SalesPower implements CrmInterface
    {
    public function __construct(
    private readonly HttpClientInterface $client,
    ) {
    }
    public function createUser(User $user): void
    {
    # PUT /user
    }
    public function deleteUser(string $id): void
    {
    # DELETE /user/
    }
    }

    View Slide

  134. namespace App\Crm;
    class SalesPower implements CrmInterface
    {
    public function __construct(
    private readonly HttpClientInterface $client,
    ) {
    }
    public function createUser(User $user): void
    {
    # PUT /user
    }
    public function deleteUser(string $id): void
    {
    # DELETE /user/
    }
    }

    View Slide

  135. namespace App\Crm;
    class SalesPower implements CrmInterface
    {
    public function __construct(
    private readonly HttpClientInterface $client,
    ) {
    }
    public function createUser(User $user): void
    {
    # PUT /user
    }
    public function deleteUser(string $id): void
    {
    # DELETE /user/
    }
    }

    View Slide

  136. namespace App\Crm;
    class SalesPower implements CrmInterface
    {
    public function __construct(
    private readonly HttpClientInterface $client,
    ) {
    }
    public function createUser(User $user): void
    {
    # PUT /user
    }
    public function deleteUser(string $id): void
    {
    # DELETE /user/
    }
    }

    View Slide

  137. Test unitaire de cette classe:
    - Cette implémentation utilise le client HTTP de Symfony
    - Le client HTTP de Symfony peut être bouchonné
    SalesPowerTest

    View Slide

  138. namespace App\Crm;
    class InMemoryCrm implements CrmInterface
    {
    public array $createdUsers = [];
    public array $deletedUserIds = [];
    public function createUser(User $user): void
    {
    $this→createdUsers[] = $user;
    }
    public function deleteUser(string $id): void
    {
    $this→deletedUserIds[] = $id;
    }
    }

    View Slide

  139. namespace App\Crm;
    class InMemoryCrm implements CrmInterface
    {
    public array $createdUsers = [];
    public array $deletedUserIds = [];
    public function createUser(User $user): void
    {
    $this→createdUsers[] = $user;
    }
    public function deleteUser(string $id): void
    {
    $this→deletedUserIds[] = $id;
    }
    }

    View Slide

  140. # config/services.yaml
    services:
    App\Crm\CrmInterface:
    alias: App\Crm\SalesPower
    [email protected]:
    Services:
    App\Crm\CrmInterface:
    alias: App\Crm\InMemoryCrm

    View Slide

  141. # config/services.yaml
    services:
    App\Crm\CrmInterface:
    alias: App\Crm\SalesPower
    [email protected]:
    Services:
    App\Crm\CrmInterface:
    alias: App\Crm\InMemoryCrm

    View Slide

  142. class RegisterTest extends WebTestCase
    {
    public function testRegister(): void
    {
    /** @var InMemoryCrm $crm */
    $crm = self::getContainer()→get(CrmInterface::class);
    # ...
    $this→assertCount(1, $crm→createdUsers);
    }
    }

    View Slide

  143. class RegisterTest extends WebTestCase
    {
    public function testRegister(): void
    {
    /** @var InMemoryCrm $crm */
    $crm = self::getContainer()→get(CrmInterface::class);
    # ...
    $this→assertCount(1, $crm→createdUsers);
    }
    }

    View Slide

  144. Ecosystème

    View Slide

  145. Playwright
    Exécute les tests dans un navigateur.

    View Slide

  146. Blackfire
    Permet de réaliser des tests de performance
    sur votre application Symfony du développement à la production.

    View Slide

  147. 147
    Conclusion
    M x N > A

    View Slide

  148. 148
    “Pas de tests,
    pas de garanties”

    View Slide

  149. 149
    Merci !

    View Slide

  150. 150
    Questions ?

    View Slide