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

483 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 what BDD and Behat involve, and show the best practices including writing good scenarios, driving service development from scenarios, and techniques for fast UI testing.

Published in: Software

Behat Best Practices

  1. 1. Behat Best Practices Ciaran McNulty @ciaranmcnulty | #scotphp17
  2. 2. @ciaranmcnulty | #scotphp17
  3. 3. @ciaranmcnulty | #scotphp17
  4. 4. @ciaranmcnulty | #scotphp17
  5. 5. Behaviour Driven Development @ciaranmcnulty | #scotphp17
  6. 6. BDD is the art of using examples in conversations to illustrate behaviour — Liz Keogh @ciaranmcnulty | #scotphp17
  7. 7. BDD is the art of using examples in conversations to illustrate behaviour @ciaranmcnulty | #scotphp17
  8. 8. BDD is the art of using examples in conversations to illustrate behaviour @ciaranmcnulty | #scotphp17
  9. 9. BDD is the art of using examples in conversations to illustrate behaviour @ciaranmcnulty | #scotphp17
  10. 10. Example 2 - Something that serves to illustrate or explain a rule — Wiktionary @ciaranmcnulty | #scotphp17
  11. 11. @ciaranmcnulty | #scotphp17
  12. 12. Rule: We charge our customers sales tax at a rate of 20% @ciaranmcnulty | #scotphp17
  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 | #scotphp17
  14. 14. @ciaranmcnulty | #scotphp17
  15. 15. @ciaranmcnulty | #scotphp17
  16. 16. @ciaranmcnulty | #scotphp17
  17. 17. @ciaranmcnulty | #scotphp17
  18. 18. @ciaranmcnulty | #scotphp17
  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 | #scotphp17
  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 | #scotphp17
  21. 21. Capturing Examples Input -> Rules -> Output @ciaranmcnulty | #scotphp17
  22. 22. Capturing Examples Input: When an action is taken Output: Then an outcome should occur @ciaranmcnulty | #scotphp17
  23. 23. Capturing Examples When I buy a pair of Levi 501s Then I am charged £32.99 @ciaranmcnulty | #scotphp17
  24. 24. Capturing Examples Context: Given some situation Input: When an action is taken Output: Then an outcome should occur @ciaranmcnulty | #scotphp17
  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 | #scotphp17
  26. 26. Context Questioning “Is there any other context which, when this event happens, will produce a different outcome?” - Liz Keogh @ciaranmcnulty | #scotphp17
  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 | #scotphp17
  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 | #scotphp17
  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 | #scotphp17
  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 | #scotphp17
  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 | #scotphp17
  32. 32. BDD is not about testing @ciaranmcnulty | #scotphp17
  33. 33. BDD is also not about requirement capture @ciaranmcnulty | #scotphp17
  34. 34. Validating examples (ok it is a bit about testing, sometimes) @ciaranmcnulty | #scotphp17
  35. 35. @ciaranmcnulty | #scotphp17
  36. 36. @ciaranmcnulty | #scotphp17
  37. 37. Behat @ciaranmcnulty | #scotphp17
  38. 38. 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 | #scotphp17
  39. 39. 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 | #scotphp17
  40. 40. 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 | #scotphp17
  41. 41. 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 | #scotphp17
  42. 42. 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 | #scotphp17
  43. 43. 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 | #scotphp17
  44. 44. @ciaranmcnulty | #scotphp17
  45. 45. 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 | #scotphp17
  46. 46. @ciaranmcnulty | #scotphp17
  47. 47. @ciaranmcnulty | #scotphp17
  48. 48. 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 | #scotphp17
  49. 49. @ciaranmcnulty | #scotphp17
  50. 50. 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 | #scotphp17
  51. 51. @ciaranmcnulty | #scotphp17
  52. 52. @ciaranmcnulty | #scotphp17
  53. 53. 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 | #scotphp17
  54. 54. 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 | #scotphp17
  55. 55. @ciaranmcnulty | #scotphp17
  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 { /** * @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 | #scotphp17
  57. 57. 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 | #scotphp17
  58. 58. 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 | #scotphp17
  59. 59. @ciaranmcnulty | #scotphp17
  60. 60. 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 | #scotphp17
  61. 61. class Course { //... public function isViable() { return $this->classSize->isViable($this->learners); } } class ClassSize { //... public function isViable(int $size) { return $size >= $this->min; } } @ciaranmcnulty | #scotphp17
  62. 62. @ciaranmcnulty | #scotphp17
  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 { /** * @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 | #scotphp17
  64. 64. 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 | #scotphp17
  65. 65. 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 | #scotphp17
  66. 66. @ciaranmcnulty | #scotphp17
  67. 67. 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 | #scotphp17
  68. 68. @ciaranmcnulty | #scotphp17
  69. 69. @ciaranmcnulty | #scotphp17
  70. 70. 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 @ciaranmcnulty | #scotphp17
  71. 71. Suites # behat.yml default: suites: domain: contexts: [ DomainContext ] services: contexts: [ ServiceContext ] @ciaranmcnulty | #scotphp17
  72. 72. 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 | #scotphp17
  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 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); } } @ciaranmcnulty | #scotphp17
  74. 74. Symfony extension # behat.yml default: suites: domain: contexts: - DomainContext services: contexts: - ServiceContext: courseEnrolments: "@cjm.training.enrolment.course_enrolments" extensions: BehatSymfony2Extension: ~ @ciaranmcnulty | #scotphp17
  75. 75. PSR-11 support » 'Native' support for instantiatable containers » Extension point to plug in containers using other methods » e.g. Roave/behat-psr11extension for Zend Expressive @ciaranmcnulty | #scotphp17
  76. 76. Autowiring Type hint services and get them automatically injected in: » Constructors » Step definitions » Transformations @ciaranmcnulty | #scotphp17
  77. 77. class CourseEnrolments { public function propose(string $title, int $minimum, int $maximum) { $this->courses->add( Course::propose( $title, ClassSize::between($minimum, $maximum) ) ); } } @ciaranmcnulty | #scotphp17
  78. 78. Infrastructure » Using real infrastructure is slow » Using fake infrastructure can lower confidence » Use fake infrastructure but sync via contract tests @ciaranmcnulty | #scotphp17
  79. 79. @ciaranmcnulty | #scotphp17
  80. 80. @ciaranmcnulty | #scotphp17
  81. 81. 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 | #scotphp17
  82. 82. 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); } } @ciaranmcnulty | #scotphp17
  83. 83. 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); } } @ciaranmcnulty | #scotphp17
  84. 84. Domain vs Service layer » Start by driving domain layer » Refactor to services when confidence grows » Drop back to domain layer when remodelling @ciaranmcnulty | #scotphp17
  85. 85. @ciaranmcnulty | #scotphp17
  86. 86. 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 | #scotphp17
  87. 87. 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 | #scotphp17
  88. 88. Mink » Browser driver abstraction » Supports Selenium, Goutte, Browserkit @ciaranmcnulty | #scotphp17
  89. 89. # behat.yml deafult: suites: domain: contexts: - DomainContext services: contexts: - ServiceContext: courseEnrolments: "@cjm.training.enrolment.course_enrolments" endtoend: filters: tags: "@endtoend" contexts: - EndToEndContext: courseEnrolments: "@cjm.training.enrolment.course_enrolments" @ciaranmcnulty | #scotphp17
  90. 90. Mink with Symfony # behat.yml extensions: BehatSymfony2Extension: ~ BehatMinkExtension: sessions: symfony: symfony2: ~ @ciaranmcnulty | #scotphp17
  91. 91. Mink with PSR-7 # behat.yml extensions: CjmBehatPsr7Extension: app: %paths.base%/path/to/file.php BehatMinkExtension: sessions: psr: psr-7: ~ @ciaranmcnulty | #scotphp17
  92. 92. 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 | #scotphp17
  93. 93. 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 | #scotphp17
  94. 94. 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 | #scotphp17
  95. 95. 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 @ciaranmcnulty | #scotphp17
  96. 96. # behat.yml extensions: BehatMinkExtension: sessions: symfony: symfony2: ~ selenium2: browser: chrome capabilities: chrome: switches: - "--headless" - "--disable-gpu" @ciaranmcnulty | #scotphp17
  97. 97. Things I'm trying out » dmore/behat-chrome-extension » aligning end-to-end tests to service APIs » property-based testing @ciaranmcnulty | #scotphp17
  98. 98. Summary » Drive domain objects directly to explore model » Refactor to services when model is stable » Add minimal UI coverage @ciaranmcnulty | #scotphp17
  99. 99. Thanks » @ciaranmcnulty » @Inviqa » @PhpSpec » @BDDLondon » @SymfonyUK github.com/ciaranmcnulty/behat-symfony-demo joind.in/event/scotlandphp-2017/behat-best-practices @ciaranmcnulty | #scotphp17

×