Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Behat Best Practices with Symfony

158 views

Published on

From SymfonyCon Lisbon 2018

Published in: Software
  • Be the first to comment

  • Be the first to like this

Behat Best Practices with Symfony

  1. 1. Behat Best Practices with Symfony Ciaran McNulty Crania Ltd | @ciaranmcnulty | #symfonycon
  2. 2. Crania Ltd | @ciaranmcnulty | #symfonycon
  3. 3. Crania Ltd | @ciaranmcnulty | #symfonycon
  4. 4. Crania Ltd | @ciaranmcnulty | #symfonycon
  5. 5. Behaviour Driven Development Crania Ltd | @ciaranmcnulty | #symfonycon
  6. 6. BDD is the art of using examples in conversations to illustrate behaviour — Liz Keogh Crania Ltd | @ciaranmcnulty | #symfonycon
  7. 7. BDD is the art of using examples in conversations to illustrate behaviour Crania Ltd | @ciaranmcnulty | #symfonycon
  8. 8. BDD is the art of using examples in conversations to illustrate behaviour Crania Ltd | @ciaranmcnulty | #symfonycon
  9. 9. BDD is the art of using examples in conversations to illustrate behaviour Crania Ltd | @ciaranmcnulty | #symfonycon
  10. 10. Example 2 - Something that serves to illustrate or explain a rule — Wiktionary Crania Ltd | @ciaranmcnulty | #symfonycon
  11. 11. Crania Ltd | @ciaranmcnulty | #symfonycon
  12. 12. Rule: We charge our customers sales tax at a rate of 20% Crania Ltd | @ciaranmcnulty | #symfonycon
  13. 13. Rule: We charge our customers sales tax at a rate of 20% Example: So, if an item is priced at $10, we charge $10 + $2 tax for a total of $12 Crania Ltd | @ciaranmcnulty | #symfonycon
  14. 14. Crania Ltd | @ciaranmcnulty | #symfonycon
  15. 15. Crania Ltd | @ciaranmcnulty | #symfonycon
  16. 16. Crania Ltd | @ciaranmcnulty | #symfonycon
  17. 17. Crania Ltd | @ciaranmcnulty | #symfonycon
  18. 18. Crania Ltd | @ciaranmcnulty | #symfonycon
  19. 19. Rule: We charge our customers sales tax at a rate of 20% Example: So, if an item is priced at $10, we charge $10 + $2 tax for a total of $12 Crania Ltd | @ciaranmcnulty | #symfonycon
  20. 20. Rule: We charge our customers sales tax at a rate of 20% Example: No! If an item is priced at £10, we charge £10 and allocate £1.67 of that as sales tax Crania Ltd | @ciaranmcnulty | #symfonycon
  21. 21. Capturing Examples Action Outcome What causes the behaviour? What is the result of the behaviour? Crania Ltd | @ciaranmcnulty | #symfonycon
  22. 22. Capturing Examples Action Outcome What causes the behaviour? What is the result of the behaviour? I buy a pair of Levi 501s I am charged £32.99 Crania Ltd | @ciaranmcnulty | #symfonycon
  23. 23. Why does that action cause that outcome? Crania Ltd | @ciaranmcnulty | #symfonycon
  24. 24. Capturing Examples Context Action Outcome What happened in the past that affects the behaviour? What causes the behaviour? What is the result of the behaviour? Levi 501s are listed at £32.99 I buy a pair of Levi 501s I am charged £32.99 Crania Ltd | @ciaranmcnulty | #symfonycon
  25. 25. Crania Ltd | @ciaranmcnulty | #symfonycon
  26. 26. Context Questioning “Is there any other context which, when this event happens, will produce a different outcome?” - Liz Keogh Crania Ltd | @ciaranmcnulty | #symfonycon
  27. 27. Context Questioning Context Action Outcome Levi 501s are listed at £32.99 I buy a pair of Levi 501s I am charged £32.99 “Is there any situation where I could buy these jeans and pay a different amount?” Crania Ltd | @ciaranmcnulty | #symfonycon
  28. 28. Context Questioning Context Action Outcome Levi 501s are listed at £32.99 I buy a pair of Levi 501s I am charged £32.99 Levis are on sale " ? Purchaser is a staff member " ? The jeans are damaged " ? Crania Ltd | @ciaranmcnulty | #symfonycon
  29. 29. Outcome Questioning “Given this context, when this event happens, is there another outcome that’s important? Something we missed, perhaps?” - Liz Keogh Crania Ltd | @ciaranmcnulty | #symfonycon
  30. 30. Outcome Questioning Context Action Outcome Levi 501s are listed at £32.99 I buy a pair of Levi 501s I am charged £32.99 "Aside from me being charged for the jeans, does something else happen that we need to care about?" Crania Ltd | @ciaranmcnulty | #symfonycon
  31. 31. Outcome Questioning Context Action Outcome Levi 501s are listed at £32.99 I buy a pair of Levi 501s I am charged £32.99 ...and I get sent some jeans ...and the warehouse stock level is reduced Crania Ltd | @ciaranmcnulty | #symfonycon
  32. 32. Example Mapping Matt Wynne Crania Ltd | @ciaranmcnulty | #symfonycon
  33. 33. Crania Ltd | @ciaranmcnulty | #symfonycon
  34. 34. Crania Ltd | @ciaranmcnulty | #symfonycon
  35. 35. Crania Ltd | @ciaranmcnulty | #symfonycon
  36. 36. Crania Ltd | @ciaranmcnulty | #symfonycon
  37. 37. Crania Ltd | @ciaranmcnulty | #symfonycon
  38. 38. Crania Ltd | @ciaranmcnulty | #symfonycon
  39. 39. Crania Ltd | @ciaranmcnulty | #symfonycon
  40. 40. Crania Ltd | @ciaranmcnulty | #symfonycon
  41. 41. Crania Ltd | @ciaranmcnulty | #symfonycon
  42. 42. Crania Ltd | @ciaranmcnulty | #symfonycon
  43. 43. Outstanding questions » Can we start work on this item without this answer? » Do we need to resolve it first? » Can the story be split along this seam? Crania Ltd | @ciaranmcnulty | #symfonycon
  44. 44. Crania Ltd | @ciaranmcnulty | #symfonycon
  45. 45. Crania Ltd | @ciaranmcnulty | #symfonycon
  46. 46. Crania Ltd | @ciaranmcnulty | #symfonycon
  47. 47. Example mapping » One-on-one » Large group » Round-robin Crania Ltd | @ciaranmcnulty | #symfonycon
  48. 48. Feature Mapping John Ferguson Smart Crania Ltd | @ciaranmcnulty | #symfonycon
  49. 49. Crania Ltd | @ciaranmcnulty | #symfonycon
  50. 50. Event Storming Alberto Brandolini Crania Ltd | @ciaranmcnulty | #symfonycon
  51. 51. Crania Ltd | @ciaranmcnulty | #symfonycon
  52. 52. BDD is not about testing Crania Ltd | @ciaranmcnulty | #symfonycon
  53. 53. BDD is also not about one-way requirement capture Crania Ltd | @ciaranmcnulty | #symfonycon
  54. 54. Validating examples (ok it is a bit about testing, sometimes) Crania Ltd | @ciaranmcnulty | #symfonycon
  55. 55. Crania Ltd | @ciaranmcnulty | #symfonycon
  56. 56. Crania Ltd | @ciaranmcnulty | #symfonycon
  57. 57. Behat Crania Ltd | @ciaranmcnulty | #symfonycon
  58. 58. Feature: Scheduling a training course As a trainer In order to be able to cancel courses or schedule new ones I should be able to specify a maximum and minimum class size Rules: - Course is proposed with size limits - When enough enrolments happen, course is considered viable - When maximum class size is reached, further enrolments are not allowed Background: Given "BDD for Beginners" was proposed with a class size of 2 to 3 people Scenario: Course does not get enough enrolments to be viable Given "BDD for Beginners" was proposed with a class size of 2 to 3 people When only Alice enrols on this course Then this course will not be viable Scenario: Course gets enough enrolments to be viable Given Alice has already enrolled on this course When Bob enrols on this course Then this course will be viable Scenario: Enrolments are stopped when class size is reached Given Alice, Bob and Charlie have already enrolled on this course When Derek tries to enrol on this course Then he should not be able to enrol Crania Ltd | @ciaranmcnulty | #symfonycon
  59. 59. Feature: Scheduling a training course As a trainer In order to be able to cancel courses or schedule new ones I should be able to specify a maximum and minimum class size Rules: - Course is proposed with size limits - When enough enrolments happen, course is considered viable - When maximum class size is reached, further enrolments are not allowed Crania Ltd | @ciaranmcnulty | #symfonycon
  60. 60. Background: Given "BDD for Beginners" was proposed with a class size of 2 to 3 people Scenario: Course does not get enough enrolments to be viable When only Alice enrols on this course Then this course will not be viable Crania Ltd | @ciaranmcnulty | #symfonycon
  61. 61. Background: Given "BDD for Beginners" was proposed with a class size of 2 to 3 people Scenario: Course does not get enough enrolments to be viable When only Alice enrols on this course Then this course will not be viable Crania Ltd | @ciaranmcnulty | #symfonycon
  62. 62. Background: Given "BDD for Beginners" was proposed with a class size of 2 to 3 people Scenario: Course gets enough enrolments to be viable Given Alice has already enrolled on this course When Bob enrols on this course Then this course will be viable Crania Ltd | @ciaranmcnulty | #symfonycon
  63. 63. Background: Given "BDD for Beginners" was proposed with a class size of 2 to 3 people Scenario: Enrolments are stopped when class size is reached Given Alice, Bob and Charlie have already enrolled on this course When Derek tries to enrol on this course Then he should not be able to enrol Crania Ltd | @ciaranmcnulty | #symfonycon
  64. 64. Crania Ltd | @ciaranmcnulty | #symfonycon
  65. 65. Gherkin: Given a thing happens to Ciaran PHP: /** * @Given a thing happens to :person */ public function doAThing(string $person) { // you have to write this } Crania Ltd | @ciaranmcnulty | #symfonycon
  66. 66. Crania Ltd | @ciaranmcnulty | #symfonycon
  67. 67. Crania Ltd | @ciaranmcnulty | #symfonycon
  68. 68. Driving the Domain layer » Drive PHP objects directly from scenario » Proves domain supports business actions » Aligns domain model with business language » Executes quickly with few dependencies Crania Ltd | @ciaranmcnulty | #symfonycon
  69. 69. Crania Ltd | @ciaranmcnulty | #symfonycon
  70. 70. Given "BDD for Beginners" was proposed with a class size of 2 to 3 people When only Alice enrols on this course Then this course will not be viable class FeatureContext implements Context { /** * @Given :courseTitle was proposed with a class size of :min to :max people */ public function courseWasProposedWithAClassSizeOfToPeople(string $courseTitle, int $min, int $max) { $this->course = Course::propose( $courseTitle, ClassSize::between($min, $max) ); } } Crania Ltd | @ciaranmcnulty | #symfonycon
  71. 71. Crania Ltd | @ciaranmcnulty | #symfonycon
  72. 72. Crania Ltd | @ciaranmcnulty | #symfonycon
  73. 73. Given "BDD for Beginners" was proposed with a class size of 2 to 3 people When only Alice enrols on this course Then this course will not be viable class FeatureContext implements Context { /** @Transform */ public function transformLearner(string $name) : Learner { return Learner::called($name); } /** * @When only :learner enrols on this course */ public function learnerEnrolsOnCourse(Learner $learner) { $this->course->enrol($learner); } } Crania Ltd | @ciaranmcnulty | #symfonycon
  74. 74. Given "BDD for Beginners" was proposed with a class size of 2 to 3 people When only Alice enrols on this course Then this course will not be viable class FeatureContext implements Context { /** * @Then this course will not be viable */ public function thisCourseWillNotBeViable() { assert($this->course->isViable() == false); } } Crania Ltd | @ciaranmcnulty | #symfonycon
  75. 75. Crania Ltd | @ciaranmcnulty | #symfonycon
  76. 76. Crania Ltd | @ciaranmcnulty | #symfonycon
  77. 77. Crania Ltd | @ciaranmcnulty | #symfonycon
  78. 78. Driving the Service layer » Configure services in test environment » Inject services into Behat context (using an extension) » Interact with domain model via the services » Aligns service layer with business use cases Crania Ltd | @ciaranmcnulty | #symfonycon
  79. 79. Suites # behat.yml default: suites: domain: contexts: [ DomainContext ] services: contexts: [ ServiceContext ] Crania Ltd | @ciaranmcnulty | #symfonycon
  80. 80. Given "BDD for Beginners" was proposed with a class size of 2 to 3 people When only Alice enrols on this course Then this course will not be viable class ServiceContext implements Context { public function __construct(CourseEnrolments $courseEnrolments) { $this->courseEnrolments = $courseEnrolments; } /** * @Given :course was proposed with a class size of :min to :max people */ public function wasProposedWithAClassSizeOfToPeople(string $course, int $min, int $max) { $this->course = $course; $this->courseEnrolments->propose($course, $min, $max); } } Crania Ltd | @ciaranmcnulty | #symfonycon
  81. 81. Given "BDD for Beginners" was proposed with a class size of 2 to 3 people When only Alice enrols on this course Then this course will not be viable class ServiceContext implements Context { public function __construct(CourseEnrolments $courseEnrolments) { $this->courseEnrolments = $courseEnrolments; } /** * @Given :course was proposed with a class size of :min to :max people */ public function wasProposedWithAClassSizeOfToPeople(string $course, int $min, int $max) { $this->courseId = $course; $this->courseEnrolments->propose($course, $min, $max); } } Crania Ltd | @ciaranmcnulty | #symfonycon
  82. 82. Symfony extension # behat.yml default: suites: domain: contexts: - DomainContext services: contexts: - ServiceContext: courseEnrolments: "@cjm.training.enrolment.course_enrolments" extensions: BehatSymfony2Extension: ~ Crania Ltd | @ciaranmcnulty | #symfonycon
  83. 83. class CourseEnrolments { private $courses; public function __construct(Courses $courses) { $this->courses = $courses; } public function propose(string $title, int $minimum, int $maximum) { $this->courses->add( Course::propose( $title, ClassSize::between($minimum, $maximum) ) ); } } Crania Ltd | @ciaranmcnulty | #symfonycon
  84. 84. Infrastructure » Using real infrastructure is slow » Using fake infrastructure can lower confidence » Use fake infrastructure but sync via contract tests Crania Ltd | @ciaranmcnulty | #symfonycon
  85. 85. Given "BDD for Beginners" was proposed with a class size of 2 to 3 people When only Alice enrols on this course Then this course will not be viable class ServiceContext implements Context { /** * @When (only) :learner enrols on this course */ public function learnerEnrolsOnThisCourse(string $learner) { $this->courseEnrolments->enrol($learner, $this->courseId); } } Crania Ltd | @ciaranmcnulty | #symfonycon
  86. 86. Given "BDD for Beginners" was proposed with a class size of 2 to 3 people When only Alice enrols on this course Then this course will not be viable class ServiceContext implements Context { /** * @Then this course will not be viable */ public function thisCourseWillNotBeViable() { assert($this->courseEnrolments->isCourseViable($this->courseId) == false); } } Crania Ltd | @ciaranmcnulty | #symfonycon
  87. 87. Domain vs Service layer » Start by driving domain layer » Refactor to services when confidence grows » Drop back to domain layer when remodelling Crania Ltd | @ciaranmcnulty | #symfonycon
  88. 88. Crania Ltd | @ciaranmcnulty | #symfonycon
  89. 89. Driving the UI layer » Simulate a browser with behat/minkextension » Interact with domain model via the UI » Ensures UI supports business actions » Slow, brittle, flakey... » Does not constrain API Crania Ltd | @ciaranmcnulty | #symfonycon
  90. 90. Don't do this Scenario: Buying a pair of jeans Given I am on "/products/levi-501" When I click on "#add-form input[type=submit]" Then "#basket ul" should contain "jeans" Crania Ltd | @ciaranmcnulty | #symfonycon
  91. 91. Mink » Browser driver abstraction » Supports Selenium, Goutte, Browserkit Crania Ltd | @ciaranmcnulty | #symfonycon
  92. 92. # behat.yml deafult: suites: services: contexts: - ServiceContext: courseEnrolments: "@cjm.training.enrolment.course_enrolments" endtoend: filters: tags: "@endtoend" contexts: - EndToEndContext: courseEnrolments: "@cjm.training.enrolment.course_enrolments" Crania Ltd | @ciaranmcnulty | #symfonycon
  93. 93. Mink with Symfony # behat.yml extensions: BehatSymfony2Extension: ~ BehatMinkExtension: sessions: symfony: symfony2: ~ Crania Ltd | @ciaranmcnulty | #symfonycon
  94. 94. Mink with PSR-7 # behat.yml extensions: CjmBehatPsr7Extension: app: %paths.base%/path/to/file.php BehatMinkExtension: sessions: psr: psr-7: ~ Crania Ltd | @ciaranmcnulty | #symfonycon
  95. 95. Given "BDD for Beginners" was proposed with a class size of 2 to 3 people When only Alice enrols on this course Then this course will not be viable class EndToEndContext implements RawMinkContext { /** * @Given :course was proposed with a class size of :min to :max people */ public function wasProposedWithAClassSizeOfToPeople(string $course, int $min, int $max) { $this->course = $course; $this->courseEnrolments->propose($course, $min, $max); } } Crania Ltd | @ciaranmcnulty | #symfonycon
  96. 96. Given "BDD for Beginners" was proposed with a class size of 2 to 3 people When only Alice enrols on this course Then this course will not be viable class EndToEndContext implements RawMinkContext { /** * @When only :learner enrols on this course */ public function learnerEnrolsOnCourse(string $learner) { $this->visitPath('/courses/' . $this->course); $page = $this->getSession()->getPage(); $page->fillField('Your name', $learner); $page->pressButton('Enrol'); } } Crania Ltd | @ciaranmcnulty | #symfonycon
  97. 97. Given "BDD for Beginners" was proposed with a class size of 2 to 3 people When only Alice enrols on this course Then this course will not be viable class EndToEndContext implements RawMinkContext { /** * @Then this course will not be viable */ public function thisCourseWillNotBeViable() { $this->visitPath('/courses/'.$this->course); $this->assertSession()->elementExists('css', '#not-viable-warning'); } } Crania Ltd | @ciaranmcnulty | #symfonycon
  98. 98. Automating a real browser » Avoid or minimise » Orders of magnitude slower » Required for end-to-end with JS » Can often be replaced with JS cucumber stack Crania Ltd | @ciaranmcnulty | #symfonycon
  99. 99. # behat.yml extensions: BehatSymfony2Extension: ~ DMoreChromeExtensionBehatServiceContainerChromeExtension: ~ BehatMinkExtension: sessions: symfony: symfony2: ~ chrome-driver: chrome: api_url: "http://localhost:9222" Crania Ltd | @ciaranmcnulty | #symfonycon
  100. 100. chrome --disable-gpu --headless --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 or docker run -d -p 9222:9222 --cap-add=SYS_ADMIN justinribeiro/chrome-headless Crania Ltd | @ciaranmcnulty | #symfonycon
  101. 101. Summary » Drive domain objects directly to explore model » Refactor to services when model is stable » Add minimal UI coverage Crania Ltd | @ciaranmcnulty | #symfonycon
  102. 102. Future » Autowiring » Panther support » Cucumberification Crania Ltd | @ciaranmcnulty | #symfonycon
  103. 103. Thanks » @ciaranmcnulty » @PhpSpec » @BDDLondon » @SymfonyUK github.com/ciaranmcnulty/behat-symfony-demo Crania Ltd | @ciaranmcnulty | #symfonycon

×