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

1,841 views

Published on

Behat is widely used as part of a Behaviour Driven Development lifecycle, but it's also widely misused. In this talk Ciaran will explain BDD, and show the best practices for using Behat including: writing good scenarios, driving service development from scenarios, and fast UI testing, using Behat and the Symfony2Extension.

Published in: Software
  • Be the first to comment

Behat Best Practices with Symfony

  1. 1. Behat Best Practices with Symfony Ciaran McNulty @ciaranmcnulty | #symfony_live
  2. 2. @ciaranmcnulty | #symfony_live
  3. 3. @ciaranmcnulty | #symfony_live
  4. 4. @ciaranmcnulty | #symfony_live
  5. 5. Behaviour Driven Development @ciaranmcnulty | #symfony_live
  6. 6. BDD is the art of using examples in conversations to illustrate behaviour — Liz Keogh @ciaranmcnulty | #symfony_live
  7. 7. BDD is the art of using examples in conversations to illustrate behaviour @ciaranmcnulty | #symfony_live
  8. 8. BDD is the art of using examples in conversations to illustrate behaviour @ciaranmcnulty | #symfony_live
  9. 9. BDD is the art of using examples in conversations to illustrate behaviour @ciaranmcnulty | #symfony_live
  10. 10. Example 2 - Something that serves to illustrate or explain a rule — Wiktionary @ciaranmcnulty | #symfony_live
  11. 11. @ciaranmcnulty | #symfony_live
  12. 12. Rule: We charge our customers sales tax at a rate of 20% @ciaranmcnulty | #symfony_live
  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 @ciaranmcnulty | #symfony_live
  14. 14. @ciaranmcnulty | #symfony_live
  15. 15. @ciaranmcnulty | #symfony_live
  16. 16. @ciaranmcnulty | #symfony_live
  17. 17. @ciaranmcnulty | #symfony_live
  18. 18. @ciaranmcnulty | #symfony_live
  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 @ciaranmcnulty | #symfony_live
  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 @ciaranmcnulty | #symfony_live
  21. 21. Capturing Examples Input -> Rules -> Output @ciaranmcnulty | #symfony_live
  22. 22. Capturing Examples Input: When an action is taken Output: Then an outcome should occur @ciaranmcnulty | #symfony_live
  23. 23. Capturing Examples When I buy a pair of Levi 501s Then I am charged £32.99 @ciaranmcnulty | #symfony_live
  24. 24. Capturing Examples Context: Given some situation Input: When an action is taken Output: Then an outcome should occur @ciaranmcnulty | #symfony_live
  25. 25. Capturing Examples Given Levi 501s are listed at £32.99 When I buy a pair of Levi 501s Then I am charged £32.99 @ciaranmcnulty | #symfony_live
  26. 26. Context Questioning “Is there any other context which, when this event happens, will produce a different outcome?” - Liz Keogh @ciaranmcnulty | #symfony_live
  27. 27. Context Questioning Given Levi 501s are listed at £32.99 When I buy a pair of Levi 501s Then I am charged £32.99 “Is there any situation where I could buy these jeans and pay a different amount?” @ciaranmcnulty | #symfony_live
  28. 28. Context Questioning Given Levi 501s are listed at £32.99 When I buy a pair of Levi 501s Then I am charged £32.99 “Is there any situation where I could buy these jeans and pay a different amount?” » When they are on sale » When I get a staff discount » When they are ex-display @ciaranmcnulty | #symfony_live
  29. 29. Outcome Questioning “Given this context, when this event happens, is there another outcome that’s important? Something we missed, perhaps?” - Liz Keogh @ciaranmcnulty | #symfony_live
  30. 30. Outcome Questioning Given Levi 501s are listed at £32.99 When I buy a pair of Levi 501s Then I am charged £32.99 "Aside from me being charged for the jeans, does something else happen that we need to care about?" @ciaranmcnulty | #symfony_live
  31. 31. Outcome Questioning Given Levi 501s are listed at £32.99 When I buy a pair of Levi 501s Then I am charged £32.99 "Aside from me being charged for the jeans, does something else happen that we need to care about?" » I get given a pair of jeans! » Stock control has to be told we sold them @ciaranmcnulty | #symfony_live
  32. 32. Validating examples @ciaranmcnulty | #symfony_live
  33. 33. @ciaranmcnulty | #symfony_live
  34. 34. @ciaranmcnulty | #symfony_live
  35. 35. Behat @ciaranmcnulty | #symfony_live
  36. 36. 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 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 @ciaranmcnulty | #symfony_live
  37. 37. 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 @ciaranmcnulty | #symfony_live
  38. 38. 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 @ciaranmcnulty | #symfony_live
  39. 39. 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 @ciaranmcnulty | #symfony_live
  40. 40. 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 @ciaranmcnulty | #symfony_live
  41. 41. 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 @ciaranmcnulty | #symfony_live
  42. 42. @ciaranmcnulty | #symfony_live
  43. 43. Gherkin: Given a thing happens to Ciaran PHP: /** * @Given a thing happens to :person */ public function doAThing(string $person) { // you have to write this } @ciaranmcnulty | #symfony_live
  44. 44. @ciaranmcnulty | #symfony_live
  45. 45. @ciaranmcnulty | #symfony_live
  46. 46. 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 @ciaranmcnulty | #symfony_live
  47. 47. @ciaranmcnulty | #symfony_live
  48. 48. 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) ); } } @ciaranmcnulty | #symfony_live
  49. 49. @ciaranmcnulty | #symfony_live
  50. 50. @ciaranmcnulty | #symfony_live
  51. 51. 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); } } @ciaranmcnulty | #symfony_live
  52. 52. 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); } } @ciaranmcnulty | #symfony_live
  53. 53. @ciaranmcnulty | #symfony_live
  54. 54. Given "BDD for Beginners" was proposed with a class size of 2 to 3 people Given Alice has already enrolled on this course When Bob enrols on this course Then this course will be viable class FeatureContext implements Context { /** * @When only :learner enrols on this course * @Given :learner has already enrolled on this course */ public function learnerEnrolsOnCourse(Learner $learner) { $this->course->enrol($learner); } } @ciaranmcnulty | #symfony_live
  55. 55. Given "BDD for Beginners" was proposed with a class size of 2 to 3 people Given Alice has already enrolled on this course When Bob enrols on this course Then this course will be viable class FeatureContext implements Context { /** * @When (only) :learner enrols on this course * @Given :learner has already enrolled on this course */ public function learnerEnrolsOnCourse(Learner $learner) { $this->course->enrol($learner); } } @ciaranmcnulty | #symfony_live
  56. 56. Given "BDD for Beginners" was proposed with a class size of 2 to 3 people Given Alice has already enrolled on this course When Bob enrols on this course Then this course will be viable class FeatureContext implements Context { /** * @Then this course will be viable */ public function thisCourseWillBeViable() { assert($this->course->isViable() == true); } } @ciaranmcnulty | #symfony_live
  57. 57. @ciaranmcnulty | #symfony_live
  58. 58. class Course { //... public function isViable(): bool { return $this->classSize->isViable($this->learners); } } class ClassSize { //... public function isViable(int $size) : bool { return $size >= $this->min; } } @ciaranmcnulty | #symfony_live
  59. 59. class Course { //... public function isViable() { return $this->classSize->isViable($this->learners); } } class ClassSize { //... public function isViable(int $size) { return $size >= $this->min; } } @ciaranmcnulty | #symfony_live
  60. 60. @ciaranmcnulty | #symfony_live
  61. 61. 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 class FeatureContext implements Context { /** * @Given :learner1, :learner2 and :learner3 have already enrolled on this course */ public function learnersHaveAlreadyEnrolledOnThisCourse( Learner $learner1, Learner $learner2, Learner $learner3 ) { $this->course->enrol($learner1); $this->course->enrol($learner2); $this->course->enrol($learner3); } } @ciaranmcnulty | #symfony_live
  62. 62. 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 class FeatureContext implements Context { /** * @When :learner tries to enrol on this course */ public function learnerTriesToEnrolOnCourse(Learner $learner) { try { $this->course->enrol($learner); } catch (Exception $e) { $this->enrolmentProblem = $e; } } } @ciaranmcnulty | #symfony_live
  63. 63. 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 class FeatureContext implements Context { /** * @Then (s)he should not be able to enrol */ public function learnerShouldNotBeAbleToEnrol() { assert($this->enrolmentProblem instanceof CjmTrainingEnrolmentProblem); } } @ciaranmcnulty | #symfony_live
  64. 64. @ciaranmcnulty | #symfony_live
  65. 65. final class Course { public function enrol(Learner $learner) { if (!$this->classSize->hasMoreCapacity($this->learners)) { throw new EnrolmentProblem('Class is already at capacity'); } $this->learners++; } } @ciaranmcnulty | #symfony_live
  66. 66. @ciaranmcnulty | #symfony_live
  67. 67. @ciaranmcnulty | #symfony_live
  68. 68. Driving the Service layer » Configure services in test SF environment » Inject services into Behat with behat/symfony2extension » Interact with domain model via the services » Aligns service layer with business use cases @ciaranmcnulty | #symfony_live
  69. 69. # behat.yml default: suites: domain: contexts: [ DomainContext ] services: contexts: [ ServiceContext ] extensions: BehatSymfony2Extension: ~ @ciaranmcnulty | #symfony_live
  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 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); } } @ciaranmcnulty | #symfony_live
  71. 71. 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); } } @ciaranmcnulty | #symfony_live
  72. 72. # behat.yml default: suites: domain: contexts: - DomainContext services: contexts: - ServiceContext: courseEnrolments: "@cjm.training.enrolment.course_enrolments" extensions: BehatSymfony2Extension: ~ @ciaranmcnulty | #symfony_live
  73. 73. class CourseEnrolments { public function propose(string $title, int $minimum, int $maximum) { $this->courses->add( Course::propose( $title, ClassSize::between($minimum, $maximum) ) ); } } @ciaranmcnulty | #symfony_live
  74. 74. Repositories » Using real infrastructure is slow » Using fake infrastructure can lower confidence » Use fake infrastructure but sync via contract tests @ciaranmcnulty | #symfony_live
  75. 75. final class Courses implements CjmTrainingEnrolmentModelCourses { public function add(Course $course) : void { $this->courses[] = $course; } public function findByTitle(string $title): Course { return $this->courses[0]; } } @ciaranmcnulty | #symfony_live
  76. 76. 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->course); } } @ciaranmcnulty | #symfony_live
  77. 77. 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->course) == false); } } @ciaranmcnulty | #symfony_live
  78. 78. Domain vs Service layer » Start by driving domain layer » Refactor to services when confidence grows » Drop back to domain layer when remodelling @ciaranmcnulty | #symfony_live
  79. 79. @ciaranmcnulty | #symfony_live
  80. 80. 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 @ciaranmcnulty | #symfony_live
  81. 81. 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" @ciaranmcnulty | #symfony_live
  82. 82. Mink » Browser driver abstraction » Supports Selenium, Goutte, Browserkit @ciaranmcnulty | #symfony_live
  83. 83. # behat.yml endtoend: filters: tags: "@endtoend" suites: domain: false services: false endtoend: contexts: - EndToEndContext: courseEnrolments: "@cjm.training.enrolment.course_enrolments" @ciaranmcnulty | #symfony_live
  84. 84. # behat.yml extensions: BehatSymfony2Extension: env: test_e2e BehatMinkExtension: sessions: symfony: symfony2: ~ @ciaranmcnulty | #symfony_live
  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 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); } } @ciaranmcnulty | #symfony_live
  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 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'); } } @ciaranmcnulty | #symfony_live
  87. 87. 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'); } } @ciaranmcnulty | #symfony_live
  88. 88. Automating a real browser » Avoid or minimise » Orders of magnitude slower » Required for end-to-end with JS » Replace with JS cucumber stack @ciaranmcnulty | #symfony_live
  89. 89. # behat.yml extensions: BehatMinkExtension: sessions: symfony: symfony2: ~ selenium2: browser: chrome capabilities: chrome: switches: - "--headless" - "--disable-gpu" @ciaranmcnulty | #symfony_live
  90. 90. Summary » Drive domain objects directly to explore model » Refactor to services when model is stable » Add minimal UI coverage @ciaranmcnulty | #symfony_live
  91. 91. Thanks » @ciaranmcnulty » @Inviqa » @PhpSpec » @BDDLondon github.com/ciaranmcnulty/behat-symfony-demo @ciaranmcnulty | #symfony_live

×