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

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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size 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 full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

  12. Unit Integration
    MyController
    Address
    AddressRepository
    routes.php

    View full-size slide

  13. Unit Integration
    MyController
    Address
    AddressRepository
    routes.php

    View full-size slide

  14. Unit Integration
    MyController
    Address
    AddressRepository
    routes.php

    View full-size slide

  15. Unit Integration
    MyController
    Address
    AddressRepository
    routes.php

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

  19. UI
    MyController
    Address
    AddressRepository
    routes.php

    View full-size slide

  20. UI
    MyController
    Address
    AddressRepository
    routes.php

    View full-size slide

  21. UI
    MyController
    Address
    AddressRepository
    routes.php

    View full-size slide

  22. AddressRepository
    Access database
    Map records to classes

    View full-size slide

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

    View full-size slide

  24. Doctrine Entities

    View full-size slide

  25. SubscriptionRenewal
    Mocked
    PaymentProvider

    View full-size slide

  26. SubscriptionRenewal
    Mocked
    PaymentProvider

    View full-size slide

  27. SubscriptionRenewal
    Real
    PaymentProvider

    View full-size slide

  28. SubscriptionRenewal
    Real
    PaymentProvider

    View full-size slide

  29. cardToken
    amount
    description
    ChargeRequest

    View full-size slide

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

    View full-size slide

  31. SubscriptionRenewal PaymentProvider

    View full-size slide

  32. SubscriptionRenewal PaymentProvider

    View full-size slide

  33. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  36. @afilina
    Questions?

    View full-size slide