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

Tests in a Symfony Application

Tests in a Symfony Application

Alexandre Salomé

June 16, 2023
Tweet

More Decks by Alexandre Salomé

Other Decks in Technology

Transcript

  1. Our use case 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. Different test types 3 test types are defined by Symfony:

    1. Unit tests 2. Integration tests 3. Application tests Application tests Integration tests Unit tests
  3. Unit tests 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. Unit tests 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. Unit tests 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. Unit tests 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. Unit tests use PHPUnit\Framework\TestCase; class UserTest extends TestCase { public

    function testActivate(): void public function testActivateTwice(): void public function testActivateWhenLocked(): void }
  8. Unit tests Pro - Fast to execute - Simple and

    fundamental Con - Mock of the dependencies
  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. Integration tests Pro - They allows testing any service -

    The service is in real conditions Con - The coupling of the service is not visible
  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. Application tests Pro - They test from outside of the

    application - Useful methods in WebTestCase Con - Significant computation cost
  24. Installing PHPUnit Use the Symfony Test Pack: composer require --dev

    symfony/test-pack This will install PHPUnit and the tools we will cover in this presentation.
  25. Testing email sending 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. Testing email sending 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. Testing email sending 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. Messenger tests Suppose that UserManager has the following method: public

    function register(User $user): void { $this→repository→save($user); $this→sendRegistrationMail($user); $this→messageBus→dispatch( new RegistrationMessage($user→getId()), ); }
  39. Messenger tests Suppose that UserManager has the following method: public

    function register(User $user): void { $this→repository→save($user); $this→sendRegistrationMail($user); $this→messageBus→dispatch( new RegistrationMessage($user→getId()), ); }
  40. Messenger tests We can verify the sending of an event

    via the in-memory transport: $transport = self::getContainer()->get('messenger.transport.async' ); $this->assertCount(1, $transport->getSent()); $message = $transport->getSent()[0]->getMessage(); $this->assertInstanceOf (RegistrationMessage ::class, $message);
  41. Messenger tests We can verify the sending of an event

    via the in-memory transport: $transport = self::getContainer()->get('messenger.transport.async' ); $this->assertCount(1, $transport->getSent()); $message = $transport->getSent()[0]->getMessage(); $this->assertInstanceOf (RegistrationMessage ::class, $message);
  42. Messenger tests We can verify the sending of an event

    via the in-memory transport: $transport = self::getContainer()->get('messenger.transport.async' ); $this->assertCount(1, $transport->getSent()); $message = $transport->getSent()[0]->getMessage(); $this->assertInstanceOf (RegistrationMessage ::class, $message);
  43. Messenger tests We can verify the sending of an event

    via the in-memory transport: $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 Suppose we want to have the following test:

    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 Suppose we want to have the following test:

    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 Suppose we want to have the following test:

    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 Suppose we want to have the following test:

    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 Suppose we want to have the following test:

    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 Thanks to the Symfony test-pack, we only need

    one 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. This annotation will declare mocks for time(), sleep(), usleep() and

    date() in the namespaces of the tested class: - App\Entity\ - App\Tests\Entity\ Calls to the time functions in User & UserTest will use the namespace declarations. Clock mocking
  51. Clock mocking But… You must use : time(), sleep(), usleep()

    et date() As an example, for DateTime et DateTimeImmutable: \DateTimeImmutable ::createFromFormat('U', time()); \DateTimeImmutable ::createFromFormat('U', time()) ->add(new \DateInterval('P2D')) ;
  52. Hello SetPasswordType A Symfony FormType that takes care of changing

    the password: $form = $this->createForm(SetPasswordType ::class, $user); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $userManager->saveUser($user); }
  53. Hello SetPasswordType A Symfony FormType that takes care of changing

    the password: ->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. We finally declare the expectations for the mock: public function

    testValid(): void { $user = new User(); $this→passwordHasher →expects($this→once()) →method('hashPassword') →with($user, 'bar') →willReturn('barHashed') ; # ... Form unit tests
  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. Hello UserActivateCommand Imagine a command 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. A CRM in which we synchronize the users through an

    HTTP call. class SalesPower { public function __construct( private readonly HttpClientInterface $client, ) { } public function createUser(User $user): void { $this→client→request('PUT', '/users', ...); } } Hello 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'); Advanced test method
  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'); Advanced test method
  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'); Advanced test method
  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 The test magic is possible because of the

    collectors: $this→client→enableProfiler(); $this→client→request('GET', '/'); $collectors = $this→client→getProfile()→getCollectors();
  88. - Request - Time - Memory - Validator - Ajax

    - Form - Exception - Logger - Events Available collectors - Router - Cache - Security - Twig - Http Client - Database - Messenger - Mailer - Config
  89. We verify that no request was generated: $collectors = $client→getProfile()→getCollectors();

    /** @var HttpClientDataCollector $httpClientCollector */ $httpClientCollector = $collectors['http_client']; $this→assertEquals(0, $httpClientCollector→getRequestCount()); Example with “http_client”
  90. Fixtures - Useful for development - Risky for automated tests

    (data mutation) - Prefer data self-sufficient tests
  91. Self-sufficient test deleting data 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 } }
  92. Abstract external services Industrialized example: Symfony Mailer Minimalist example: a

    third CRM API: - PUT /user when a user is created - DELETE /user/<ID> when a user is deleted
  93. namespace App\Crm; interface CrmInterface { /** @throws ApiException */ public

    function createUser(User $user): void; /** @throws ApiException */ public function deleteUser(string $id): void; } Create the interface contract
  94. 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> } }
  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. Unit test the class: - This implementation relies on the

    Symfony HTTP client - The Symfony HTTP client can be mocked SalesPowerTest
  99. 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; } }
  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. class RegisterTest extends WebTestCase { public function testRegister(): void {

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

    /** @var InMemoryCrm $crm */ $crm = self::getContainer()→get(CrmInterface::class); # ... $this→assertCount(1, $crm→createdUsers); } }