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. 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 <[email protected]> Subject: Finish your registration
  2. 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
  3. 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()); } }
  4. 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()); } }
  5. 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()); } }
  6. 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()); } }
  7. Les tests unitaires use PHPUnit\Framework\TestCase; class UserTest extends TestCase {

    public function testActivate(): void public function testActivateTwice(): void public function testActivateWhenLocked(): void }
  8. Les tests unitaires Avantages - Rapides à exécuter - Simples

    et fondamental Inconvénients - Bouchons pour les dépendances 17
  9. 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 (); } }
  10. 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 (); } }
  11. 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 (); } }
  12. 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 (); } }
  13. 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 (); } }
  14. 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 (); } }
  15. 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 (); } }
  16. 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
  17. 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'); } }
  18. 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'); } }
  19. 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'); } }
  20. 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'); } }
  21. 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".
  22. 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".
  23. Les tests applicatifs Avantages - Tests de l’extérieur de l’application

    - Utilitaires de WebTestCase Inconvénients - Tests gourmands en ressources 41
  24. 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.
  25. 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(); }
  26. 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 <[email protected]> Subject: Finish your registration
  27. // 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' );
  28. // 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' );
  29. // 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' );
  30. // 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' );
  31. // 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' );
  32. 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 <[email protected]> Subject: Finish your registration
  33. 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 (); }
  34. 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 (); }
  35. 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 (); }
  36. 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 (); }
  37. 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 (); }
  38. 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()), ); }
  39. 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()), ); }
  40. 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);
  41. 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);
  42. 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);
  43. 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);
  44. 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)); }
  45. 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)); }
  46. 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)); }
  47. 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)); }
  48. 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)); }
  49. 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)); }
  50. 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
  51. 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')) ;
  52. 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); }
  53. 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); })
  54. 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()); }
  55. 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()); }
  56. 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()); }
  57. 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()); }
  58. 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()); }
  59. 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 ], []), ]; }
  60. 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 ], []), ]; }
  61. 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 ], []), ]; }
  62. 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 ], []), ]; }
  63. 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
  64. $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());
  65. $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());
  66. $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());
  67. $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());
  68. $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());
  69. 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.
  70. 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(), ); }
  71. 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(), ); }
  72. 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(), ); }
  73. 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(), ); }
  74. 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(), ); }
  75. 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(), ); }
  76. 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('<div data-controller="hello"></div>'); }); afterEach(() => { clearDOM(); }); // Test here });
  77. 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('<div data-controller="hello"></div>'); }); afterEach(() => { clearDOM(); }); // Test here });
  78. describe('HelloController', () => { // ... it('connect', async () =>

    { expect(container.textContent).toContain("Hello world"); }); });
  79. 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
  80. $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
  81. $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
  82. $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
  83. $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);
  84. $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);
  85. $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);
  86. $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);
  87. 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();
  88. - Request - Time - Memory - Validator - Ajax

    - Form - Exception - Logger - Events Les collectors disponibles - Router - Cache - Security - Twig - Http Client - Database - Messenger - Mailer - Config
  89. 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”
  90. Fixtures - Utile pour le développement - Risqué pour les

    tests automatisés (modification des données)
  91. 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
  92. 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 } }
  93. Abstraire les services externes Exemple industrialisé : Symfony Mailer Exemple

    minimaliste: une API tierce de CRM : - PUT /user quand un utilisateur est créé - DELETE /user/<ID> quand un utilisateur est supprimé
  94. 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
  95. 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/<id> } }
  96. 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/<id> } }
  97. 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/<id> } }
  98. 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/<id> } }
  99. Test unitaire de cette classe: - Cette implémentation utilise le

    client HTTP de Symfony - Le client HTTP de Symfony peut être bouchonné SalesPowerTest
  100. 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; } }
  101. 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; } }
  102. class RegisterTest extends WebTestCase { public function testRegister(): void {

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

    /** @var InMemoryCrm $crm */ $crm = self::getContainer()→get(CrmInterface::class); # ... $this→assertCount(1, $crm→createdUsers); } }
  104. Blackfire Permet de réaliser des tests de performance sur votre

    application Symfony du développement à la production.