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

BDD Your Symfony Application - SymfonyLive London 2019

Kamil Kokot
September 13, 2019

BDD Your Symfony Application - SymfonyLive London 2019

Behaviour Driven Development helps bridge the communication gap between business and IT. Having testable specifications is a desirable side effect.

This talk will explain the basics of BDD methodology, best practices for writing Cucumber scenarios and how to integrate Symfony with Behat by using a new emerging solution - FriendsOfBehat's SymfonyExtension.

I will share the practical insights distilled from 4 years of developing and maintaining the biggest open-source Behat suite which is a part of Sylius.

Kamil Kokot

September 13, 2019
Tweet

More Decks by Kamil Kokot

Other Decks in Programming

Transcript

  1. Behaviour-driven development • Agile development process • Encourages communica8on •

    Creates shared understanding • Provides living, executable documenta8on
  2. Living documentaFon Feature: Some feature In order to get more

    value from the product As a customer I want to have an easier way to do something Scenario: An example usage of this feature Given some context When an event happens Then an outcome should occur Scenario: An another example usage of this feature ...
  3. Don’t do this Feature: Booking flight tickets Scenario: Booking flight

    ticket for one person Given there are the following flights: | route | pilot | seats_available | price | | LTN-WAW | Mark | 5 | £50 | When I visit "/flight/LTN-WAW" And I fill in "Adult" field with "1" And I click "Book" button Then I should be on "/flight/LTN-WAW/success" And I should see "Your flight has been booked." in "#result"
  4. Focus on the behaviour Feature: Booking a flight ticket In

    order to provide hassle-free flying experience As a plane pilot I want passengers to book tickets for my flight Scenario: Booking a flight for one person Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should be still available
  5. BDD aids communicaFon Scenario: Booking a flight for one person

    Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should be still available Can we get a different outcome when the context changes?
  6. BDD aids communicaFon Scenario: Booking a flight for one person

    Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should be still available Can we get a different outcome when the context changes? •When there was only one seat available •When there were no available seats
  7. BDD aids communicaFon Scenario: Booking a flight for one person

    Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should be still available Can we get the same outcome when the event changes?
  8. BDD aids communicaFon Scenario: Booking a flight for one person

    Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should be still available Can we get the same outcome when the event changes? •When it is booked for an adult and an infant •When it is booked for a child
  9. BDD aids communicaFon Scenario: Booking a flight for one person

    Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should be still available Does anything else happen that is not menFoned?
  10. BDD aids communicaFon Scenario: Booking a flight for one person

    Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should be still available Does anything else happen that is not menFoned? •An invoice is generated for John •A no8fica8on is sent to the pilot
  11. Figuring out the rules Feature: Booking a flight ticket In

    order to provide hassle-free flying experience As a plane pilot I want passengers to book tickets for my flight Rules: - Adults are 15+ years old - Children are 2-14 years old - Infants are up to 2 years old - Infants and children can only travel with an adult - An infant can share the seat with an adult - Surplus infants need their own seat - We don't allow for overbooking
  12. TranslaFng rules into examples Scenario: Trying to book a flight

    on a full flight Given there is a flight with no seats available When Rebecca tries to book this flight for one adult Then she should not get a ticket We don’t allow for overbooking
  13. TranslaFng rules into examples Scenario: Booking a flight ticket for

    an adult and an infant Given there is a flight with one seat available When Amy books this flight for one adult and one infant Then she should get a ticket for one seat And this flight should be no longer available An infant can share the seat with an adult
  14. TranslaFng rules into examples Scenario: Booking a flight ticket for

    more infants than adults Given there is a flight with 10 seats available When Rob books this flight for two adults and three infants Then he should get a ticket for three seats Surplus infants need their own seat
  15. Given a context When an event Then an outcome Domain

    Context •Given a context •When an event •Then an outcome API Context •Given a context •When an event •Then an outcome
  16. final class DomainContext implements Context { /** @Given there is

    a flight with :availableSeats seat(s) available */ public function thereIsFlightWithSeatsAvailable(int $availableSeats): void { // ... } /** @When :client books this flight for :adults adult */ public function oneBooksThisFlightForAdult(Client $client, int $adults): void { // ... } /** @Then (s)he should get a ticket for :reservedSeats seat(s) */ public function oneShouldGetTicketForSeats(int $reservedSeats): void { // ... } }
  17. /** * @Given there is a flight with :availableSeats seat(s)

    available * @Given /^there is a flight with (\d+) seats? available$/ */ public function thereIsFlightWithSeatsAvailable(int $availableSeats): void { // ... }
  18. suites: domain: contexts: - App\Tests\DomainContext filters: tags: “@domain” api: contexts:

    - App\Tests\ApiContext filters: tags: “@api” @domain @api Scenario: Booking a flight for one person Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should be still available
  19. # behat.yaml suites: api: contexts: - app.behat.api_context # config/services_test.yaml services:

    app.behat.api_context: class: App\Tests\Behat\ApiContext arguments: - ‘@router’ public: true Friends of Behat: Symfony Extension Contexts registered as Symfony services
  20. # behat.yaml suites: api: contexts: - App\Tests\Behat\ApiContext class ApiContext implements

    Context { public function __construct( RouterInterface $router ) { /* */ } } Friends of Behat: Symfony Extension Autowired contexts # config/services_test.yaml services: _defaults: autowire: true autoconfigure: true App\Tests\Behat\: resource: '../tests/Behat/*'
  21. /** * @Given there is a flight with :availableSeats seat(s)

    available */ public function thereIsFlightWithSeatsAvailable(int $availableSeats): void { $this->flight = Flight::schedule($availableSeats); } Scenario: Booking a flight for one person Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should still be available
  22. /** @Transform */ public function transformClient(string $name): Client { return

    Client::named($name); } /** * @When :client books this flight for :adults adult */ public function oneBooksThisFlightForAdult(Client $client, int $adults): void { $this->ticket = $this->flight->book( $client, Booking::forAdults($adults) ); } Scenario: Booking a flight for one person Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should still be available
  23. /** * @Then (s)he should get a ticket for :reservedSeats

    seat(s) */ public function oneShouldGetTicketForSeats(int $reservedSeats): void { assert($this->ticket->reservedSeats() === $reservedSeats); } /** * @Then this flight should still be available */ public function thisFlightShouldBeAvailable(): void { assert($this->flight->isAvailable() === true); } Scenario: Booking a flight for one person Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should still be available
  24. public function __construct( FlightBookingService $flightBookingService, KernelBrowser $kernelBrowser ) { $this->flightBookingService

    = $flightBookingService; $this->kernelBrowser = $kernelBrowser; } /** @Given there is a flight with :availableSeats seat(s) available */ public function thereIsFlightWithSeatsAvailable(int $availableSeats): void { $this->flightId = $this->flightBookingService->scheduleFlight($availableSeats); } Scenario: Booking a flight for one person Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should still be available
  25. /** @When :client books this flight for :adults adult */

    public function oneBooksThisFlightForAdult(string $client, int $adults): void { $this->kernelBrowser->request( 'POST', sprintf('/flights/%s/book', $this->flightId), ['client' => $client, 'adults' => $adults] ); $response = $this->kernelBrowser->getResponse(); assert($response->getStatusCode() === Response::HTTP_OK); $data = json_decode($response->getContent(), true); $this->reservedSeats = $data['reserved_seats']; } Scenario: Booking a flight for one person Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should still be available
  26. /** @Then (s)he should get a ticket for :reservedSeats seat(s)

    */ public function oneShouldGetTicketForSeats(int $reservedSeats): void { assert($this->reservedSeats === $reservedSeats); } Scenario: Booking a flight for one person Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should still be available
  27. /** @Then this flight should be still available */ public

    function thisFlightShouldBeAvailable(): void { $this->kernelBrowser->request('GET', sprintf('/flights/%s', $flightId)); $response = $this->kernelBrowser->getResponse(); assert($response->getStatusCode() === Response::HTTP_OK); $data = json_decode($response->getContent(), true); assert($data['available'] === true); } Scenario: Booking a flight for one person Given there is a flight with 5 seats available When John books this flight for one adult Then he should get a ticket for one seat And this flight should still be available
  28. Performance - UI automaFon • Non-JS: 12716 steps, 1175 scenarios


    8 min 8 sec, 2.4 scenario / sec • JS: 2171 steps, 206 scenarios
 17 min 48 sec, 0.19 scenario / sec 12x faster
  29. Performance Fps Log in programa8cally public function logIn(UserInterface $user) {

    $token = new UsernamePasswordToken( $user, $user->getPassword(), 'firewall_context', $user->getRoles() ); $serializedToken = serialize($token); $this->session->set('_security_firewall_context', $serializedToken); $this->session->save(); $this->cookieSetter->setCookie( $this->session->getName(), $this->session->getId() ); } 30% faster!
  30. Performance Fps Setup the test environment on CI like the

    produc8on • Turn off kernel’s debug seYng • Add Doctrine cache for queries, metadata and results • Enable OPcache • Warmup Symfony cache • Look for memory leaks (kernel is reboo8ng) 40% faster!
  31. How to split contexts? suites: ui_managing_product_options: contexts: - sylius.behat.context.hook.doctrine_orm -

    sylius.behat.context.transform.lexical - sylius.behat.context.transform.locale - sylius.behat.context.transform.product_option - sylius.behat.context.transform.shared_storage - sylius.behat.context.setup.locale - sylius.behat.context.setup.product_option - sylius.behat.context.setup.admin_security - sylius.behat.context.ui.admin.managing_product_options - sylius.behat.context.ui.admin.notification
  32. What about the shared state? final class SharedStorageContext implements Context

    { /** @var SharedStorageInterface */ private $sharedStorage; public function __construct(SharedStorageInterface $sharedStorage) { $this->sharedStorage = $sharedStorage; } /** @Transform /^(?:this|that|the) ([^"]+)$/ */ public function getResource($resource) { return $this->sharedStorage->get(StringInflector::nameToCode($resource)); } }
  33. Too many setup steps Background: Given the store uses "English

    (United States)" locale And the store supports USD currency And the store defines a zone for United States And the store operates on a channel "US" Background: Given the store operates on a single channel in "United States"
  34. Too detailed scenarios Background: Given there's has an enabled product

    "Shovel" priced at £10, in "Tools" category, available in the "Default" channel, with 10% tax excluded from price When I add "Shovel" to my cart Then my order's total should be £11,11 Background: Given there's a product priced at £10 with 10% tax excluded from price When I add "Shovel" to my cart Then my order's total should be £11,11