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

Writing Testable Symfony Apps

Writing Testable Symfony Apps

Do you struggle as soon as you want to test a controller or a repository? Do you feel like it's sometimes impossible to write unit tests for a Symfony application? In this talk, I will show you design patterns that will not only make testing your Symfony applications trivial, but will also make your code a pleasure to work with.

B3b2139e4f2c0eca4efe2379fcebc1c5?s=128

Anna Filina
PRO

June 16, 2022
Tweet

More Decks by Anna Filina

Other Decks in Programming

Transcript

  1. Writing Testable Symfony Apps SYMFONY WORLD ONLINE | JUN, 2022

    @afilina
  2. “Our tests are slow.“ “We can’t really write unit tests.”

    “Tests are hard to maintain.”
  3. Anna Filina • Coding since 1997 • PHP since 2003

    • Legacy archaeology • Test automation • Public speaking • Mentorship • YouTube videos
  4. class MyController extends AbstractController { public function index(): Response {

    return $this->render('my/index.html.twig', [ 'variable' => 'value', ]); } }
  5. class MyControllerTest extends TestCase { public function testIndex(): void {

    $controller = new MyController(); $controller->index(); } }
  6. Error : Call to a member function has() on null

    ./vendor/symfony/framework-bundle/Controller/AbstractController.php:254 ./vendor/symfony/framework-bundle/Controller/AbstractController.php:266 ./src/Controller/MyController.php:14 ./tests/Controller/MyControllerTest.php:25
  7. class MyController extends AbstractController { public function index(): Response {

    return $this->render('my/index.html.twig', [ 'variable' => 'value', ]); } }
  8. use Twig\Environment; class MyTestableController { public function __construct(private Environment $twig)

    { } public function index(): Response { $html = $this->twig->render('my/index.html.twig', [ 'var' => 'value', ]); return new Response($html); } }
  9. services: App\Controller\: resource: '../src/Controller/' tags: ['controller.service_arguments']

  10. public function testIndex(): void { $controller = new MyTestableController( $twig

    = $this->createMock(Environment::class) ); $twig ->method('render') ->with('my/index.html.twig', ['var' => 'value']) ->willReturn('some content'); self::assertEquals( 'some content', $controller->index()->getContent() ); }
  11. Time: 00:00.089, Memory: 10.00 MB OK (1 test, 1 assertion)

  12. Unit Integration MyController Address AddressRepository routes.php

  13. Unit Integration MyController Address AddressRepository routes.php

  14. Unit Integration MyController Address AddressRepository routes.php

  15. Unit Integration MyController Address AddressRepository routes.php

  16. MyController Address AddressRepository GET /saved-addresses routes.php

  17. # Behat feature file Scenario: Buyer can purchase using a

    credit card Given I selected a product When I submit a valid credit card Then I should see a payment receipt afilina.com/learn
  18. • Routes • Service container • Security • Cache, Twig

    and other packages
  19. UI MyController Address AddressRepository routes.php

  20. UI MyController Address AddressRepository routes.php

  21. UI MyController Address AddressRepository routes.php

  22. AddressRepository Access database Map records to classes

  23. if ($address->isBilling() && $address->getCountry() !== 'Canada') { //... }

  24. None
  25. None
  26. Doctrine Entities

  27. None
  28. None
  29. None
  30. SubscriptionRenewal Mocked PaymentProvider

  31. SubscriptionRenewal Mocked PaymentProvider

  32. SubscriptionRenewal Real PaymentProvider

  33. SubscriptionRenewal Real PaymentProvider

  34. cardToken amount description ChargeRequest

  35. final class ChargeRequest { public function __construct( readonly public string

    $token, readonly public int $amount, readonly public string $description ) {} }
  36. SubscriptionRenewal PaymentProvider

  37. SubscriptionRenewal PaymentProvider

  38. final class ChargeRequest { public function __construct( readonly public string

    $token, readonly public int $amount, readonly public string $description ) { if ($amount < 0) { throw new \DomainException('Amount cannot be lower than 0'); } } }
  39. Isn’t that overkill for an edge case?

  40. • Avoid extending framework classes • Use multiple test types

    in tandem (pyramid) • Extract business logic from infrastructure • Use small immutable classes
  41. @afilina Questions?