Slide 1

Slide 1 text

Tests in a Symfony Application Alexandre Salomé

Slide 2

Slide 2 text

Our use case Name: Email: Alice [email protected] Register Register

Slide 3

Slide 3 text

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 Subject: Finish your registration

Slide 4

Slide 4 text

Application Example Request Response

Slide 5

Slide 5 text

Application Service Example User Repository Request Response User

Slide 6

Slide 6 text

Application Service Example User Repository Service Mailer Request Response User

Slide 7

Slide 7 text

Application Service Example User Repository Service Mailer Service User Manager Request Response User

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Unit tests

Slide 10

Slide 10 text

These tests ensure that individual units of source code (e.g. a single class) behave as intended.

Slide 11

Slide 11 text

Application Service Unit tests User Repository Service Mailer Service User Manager Request Response User

Slide 12

Slide 12 text

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()); } }

Slide 13

Slide 13 text

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()); } }

Slide 14

Slide 14 text

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()); } }

Slide 15

Slide 15 text

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()); } }

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Unit tests Pro - Fast to execute - Simple and fundamental Con - Mock of the dependencies

Slide 18

Slide 18 text

Integration tests

Slide 19

Slide 19 text

These tests test a combination of classes and commonly interact with Symfony's service container.

Slide 20

Slide 20 text

Application Service Integration tests User Repository Service Mailer Service User Manager Request Response User

Slide 21

Slide 21 text

Application Service Integration tests User Repository Service Mailer Service User Manager Request Response User

Slide 22

Slide 22 text

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 (); } }

Slide 23

Slide 23 text

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 (); } }

Slide 24

Slide 24 text

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 (); } }

Slide 25

Slide 25 text

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 (); } }

Slide 26

Slide 26 text

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 (); } }

Slide 27

Slide 27 text

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 (); } }

Slide 28

Slide 28 text

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 (); } }

Slide 29

Slide 29 text

Integration tests Pro - They allows testing any service - The service is in real conditions Con - The coupling of the service is not visible

Slide 30

Slide 30 text

Application tests

Slide 31

Slide 31 text

Application tests test the behavior of a complete application.

Slide 32

Slide 32 text

Application Service Application tests User Repository Service Mailer Service User Manager Request Response User

Slide 33

Slide 33 text

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'); } }

Slide 34

Slide 34 text

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'); } }

Slide 35

Slide 35 text

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'); } }

Slide 36

Slide 36 text

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'); } }

Slide 37

Slide 37 text

WebTestCase - BrowserKitAssertionsTrait

Slide 38

Slide 38 text

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".

Slide 39

Slide 39 text

WebTestCase - DomCrawlerAssertionsTrait

Slide 40

Slide 40 text

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".

Slide 41

Slide 41 text

Application tests Pro - They test from outside of the application - Useful methods in WebTestCase Con - Significant computation cost

Slide 42

Slide 42 text

Symfony tools

Slide 43

Slide 43 text

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.

Slide 44

Slide 44 text

Testing email sending

Slide 45

Slide 45 text

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(); }

Slide 46

Slide 46 text

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 Subject: Finish your registration

Slide 47

Slide 47 text

// 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' );

Slide 48

Slide 48 text

// 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' );

Slide 49

Slide 49 text

// 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' );

Slide 50

Slide 50 text

// 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' );

Slide 51

Slide 51 text

// 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' );

Slide 52

Slide 52 text

Testing email sending - MailerAssertionsTrait

Slide 53

Slide 53 text

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 Subject: Finish your registration

Slide 54

Slide 54 text

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 (); }

Slide 55

Slide 55 text

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 (); }

Slide 56

Slide 56 text

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 (); }

Slide 57

Slide 57 text

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 (); }

Slide 58

Slide 58 text

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 (); }

Slide 59

Slide 59 text

Messenger tests

Slide 60

Slide 60 text

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()), ); }

Slide 61

Slide 61 text

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()), ); }

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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);

Slide 64

Slide 64 text

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);

Slide 65

Slide 65 text

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);

Slide 66

Slide 66 text

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);

Slide 67

Slide 67 text

Clock mocking

Slide 68

Slide 68 text

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)); }

Slide 69

Slide 69 text

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)); }

Slide 70

Slide 70 text

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)); }

Slide 71

Slide 71 text

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)); }

Slide 72

Slide 72 text

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)); }

Slide 73

Slide 73 text

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)); }

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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')) ;

Slide 76

Slide 76 text

Form unit tests

Slide 77

Slide 77 text

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); }

Slide 78

Slide 78 text

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); })

Slide 79

Slide 79 text

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()); }

Slide 80

Slide 80 text

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()); }

Slide 81

Slide 81 text

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()); }

Slide 82

Slide 82 text

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()); }

Slide 83

Slide 83 text

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()); }

Slide 84

Slide 84 text

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 ], []), ]; }

Slide 85

Slide 85 text

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 ], []), ]; }

Slide 86

Slide 86 text

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 ], []), ]; }

Slide 87

Slide 87 text

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 ], []), ]; }

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

$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());

Slide 90

Slide 90 text

$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());

Slide 91

Slide 91 text

$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());

Slide 92

Slide 92 text

$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());

Slide 93

Slide 93 text

$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());

Slide 94

Slide 94 text

Testing commands

Slide 95

Slide 95 text

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.

Slide 96

Slide 96 text

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(), ); }

Slide 97

Slide 97 text

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(), ); }

Slide 98

Slide 98 text

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(), ); }

Slide 99

Slide 99 text

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(), ); }

Slide 100

Slide 100 text

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(), ); }

Slide 101

Slide 101 text

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(), ); }

Slide 102

Slide 102 text

CommandTester

Slide 103

Slide 103 text

Testing Stimulus

Slide 104

Slide 104 text

Symfony UX Stimulus testing is currently considered as experimental. Symfony UX Stimulus testing

Slide 105

Slide 105 text

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 });

Slide 106

Slide 106 text

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 });

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

HTTP client

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

use Symfony\Component\HttpClient\MockHttpClient; $mock = new MockHttpClient(new MockResponse('OK')); $crm = new SalesPower($mock); $crm→createUser($alice); Simple test method

Slide 111

Slide 111 text

$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

Slide 112

Slide 112 text

$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

Slide 113

Slide 113 text

$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

Slide 114

Slide 114 text

Going further

Slide 115

Slide 115 text

Mock services

Slide 116

Slide 116 text

$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);

Slide 117

Slide 117 text

$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);

Slide 118

Slide 118 text

$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);

Slide 119

Slide 119 text

$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);

Slide 120

Slide 120 text

Profiler collectors

Slide 121

Slide 121 text

Profiler collectors The test magic is possible because of the collectors: $this→client→enableProfiler(); $this→client→request('GET', '/'); $collectors = $this→client→getProfile()→getCollectors();

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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”

Slide 124

Slide 124 text

Test data aka “fixtures”

Slide 125

Slide 125 text

Fixtures - Useful for development

Slide 126

Slide 126 text

Fixtures - Useful for development - Risky for automated tests (data mutation)

Slide 127

Slide 127 text

Fixtures - Useful for development - Risky for automated tests (data mutation) - Prefer data self-sufficient tests

Slide 128

Slide 128 text

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 } }

Slide 129

Slide 129 text

External dependencies

Slide 130

Slide 130 text

Abstract external services Industrialized example: Symfony Mailer

Slide 131

Slide 131 text

Abstract external services Industrialized example: Symfony Mailer Minimalist example: a third CRM API: - PUT /user when a user is created - DELETE /user/ when a user is deleted

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

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/ } }

Slide 134

Slide 134 text

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/ } }

Slide 135

Slide 135 text

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/ } }

Slide 136

Slide 136 text

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/ } }

Slide 137

Slide 137 text

Unit test the class: - This implementation relies on the Symfony HTTP client - The Symfony HTTP client can be mocked SalesPowerTest

Slide 138

Slide 138 text

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; } }

Slide 139

Slide 139 text

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; } }

Slide 140

Slide 140 text

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

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

Conclusion M x N > A

Slide 145

Slide 145 text

“No tests, No guarantee”

Slide 146

Slide 146 text

Thank you!

Slide 147

Slide 147 text

Questions?