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 full-size slide

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

    View full-size 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 full-size slide

  4. Application
    Exemple
    Request Response

    View full-size slide

  5. Application
    Service
    Exemple
    User
    Repository
    Request Response
    User

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

  9. Les tests
    unitaires

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

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

    View full-size slide

  18. Les tests
    d’intégration

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  30. Les tests
    applicatifs

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  37. WebTestCase - BrowserKitAssertionsTrait

    View full-size 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 full-size slide

  39. WebTestCase - DomCrawlerAssertionsTrait

    View full-size 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 full-size 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 full-size slide

  42. Les outils de Symfony

    View full-size 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 full-size slide

  44. Tester l’envoi
    d’emails

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  52. Tester l’envoi d’emails - MailerAssertionsTrait

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  59. Tests Messenger

    View full-size 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 full-size 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 full-size slide

  62. Tests Messenger
    # app/config/messenger.yaml
    framework:
    messenger:
    transports:
    async: '%env(MESSENGER_TRANSPORT_DSN)%'
    when@test:
    framework:
    messenger:
    transports:
    async: 'in-memory://'

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  67. Clock mocking

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  76. Test unitaire de
    formulaire

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  94. Tester les
    commandes

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  102. CommandTester

    View full-size slide

  103. Testing Stimulus

    View full-size slide

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

    View full-size 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 full-size 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 full-size slide

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

    View full-size slide

  108. Le client HTTP

    View full-size 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 full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size slide

  114. Aller plus loin

    View full-size slide

  115. Bouchonner les
    services

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  120. Profiler
    collectors

    View full-size 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 full-size 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 full-size 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 full-size slide

  124. Données de test
    aka “fixtures”

    View full-size slide

  125. Fixtures
    - Utile pour le développement

    View full-size slide

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

    View full-size 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 full-size 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 full-size slide

  129. Dépendances
    externes

    View full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  140. # config/services.yaml
    services:
    App\Crm\CrmInterface:
    alias: App\Crm\SalesPower
    when@test:
    Services:
    App\Crm\CrmInterface:
    alias: App\Crm\InMemoryCrm

    View full-size slide

  141. # config/services.yaml
    services:
    App\Crm\CrmInterface:
    alias: App\Crm\SalesPower
    when@test:
    Services:
    App\Crm\CrmInterface:
    alias: App\Crm\InMemoryCrm

    View full-size 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 full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  146. 147
    Conclusion
    M x N > A

    View full-size slide

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

    View full-size slide

  148. 150
    Questions ?

    View full-size slide