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.

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

    View Slide

  2. “Our tests are slow.“
    “We can’t really write unit tests.”
    “Tests are hard to maintain.”

    View Slide

  3. Anna Filina
    • Coding since 1997
    • PHP since 2003
    • Legacy archaeology
    • Test automation
    • Public speaking
    • Mentorship
    • YouTube videos

    View Slide

  4. class MyController extends AbstractController
    {
    public function index(): Response
    {
    return $this->render('my/index.html.twig', [
    'variable' => 'value',
    ]);
    }
    }

    View Slide

  5. class MyControllerTest extends TestCase
    {
    public function testIndex(): void
    {
    $controller = new MyController();
    $controller->index();
    }
    }

    View Slide

  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

    View Slide

  7. class MyController extends AbstractController
    {
    public function index(): Response
    {
    return $this->render('my/index.html.twig', [
    'variable' => 'value',
    ]);
    }
    }

    View Slide

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

    View Slide

  9. services:
    App\Controller\:
    resource: '../src/Controller/'
    tags: ['controller.service_arguments']

    View Slide

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

    View Slide

  11. Time: 00:00.089, Memory: 10.00 MB
    OK (1 test, 1 assertion)

    View Slide

  12. Unit Integration
    MyController
    Address
    AddressRepository
    routes.php

    View Slide

  13. Unit Integration
    MyController
    Address
    AddressRepository
    routes.php

    View Slide

  14. Unit Integration
    MyController
    Address
    AddressRepository
    routes.php

    View Slide

  15. Unit Integration
    MyController
    Address
    AddressRepository
    routes.php

    View Slide

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

    View Slide

  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

    View Slide

  18. • Routes
    • Service container
    • Security
    • Cache, Twig and other packages

    View Slide

  19. UI
    MyController
    Address
    AddressRepository
    routes.php

    View Slide

  20. UI
    MyController
    Address
    AddressRepository
    routes.php

    View Slide

  21. UI
    MyController
    Address
    AddressRepository
    routes.php

    View Slide

  22. AddressRepository
    Access database
    Map records to classes

    View Slide

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

    View Slide

  24. View Slide

  25. View Slide

  26. Doctrine Entities

    View Slide

  27. View Slide

  28. View Slide

  29. View Slide

  30. SubscriptionRenewal
    Mocked
    PaymentProvider

    View Slide

  31. SubscriptionRenewal
    Mocked
    PaymentProvider

    View Slide

  32. SubscriptionRenewal
    Real
    PaymentProvider

    View Slide

  33. SubscriptionRenewal
    Real
    PaymentProvider

    View Slide

  34. cardToken
    amount
    description
    ChargeRequest

    View Slide

  35. final class ChargeRequest
    {
    public function __construct(
    readonly public string $token,
    readonly public int $amount,
    readonly public string $description
    ) {}
    }

    View Slide

  36. SubscriptionRenewal PaymentProvider

    View Slide

  37. SubscriptionRenewal PaymentProvider

    View Slide

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

    View Slide

  39. Isn’t that overkill for an edge case?

    View Slide

  40. • Avoid extending framework classes
    • Use multiple test types in tandem (pyramid)
    • Extract business logic from infrastructure
    • Use small immutable classes

    View Slide

  41. @afilina
    Questions?

    View Slide