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.
12. Rule:
We charge our customers sales tax at a rate of 20%
@ciaranmcnulty | #symfony_live
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
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. 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
23. Capturing Examples
When I buy a pair of Levi 501s
Then I am charged £32.99
@ciaranmcnulty | #symfony_live
24. Capturing Examples
Context: Given some situation
Input: When an action is taken
Output: Then an outcome should occur
@ciaranmcnulty | #symfony_live
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. Context Questioning
“Is there any other context which, when this event happens, will
produce a different outcome?” - Liz Keogh
@ciaranmcnulty | #symfony_live
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. 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. 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. 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. 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
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. 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. 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. 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. 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. 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
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
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
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
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. 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
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. 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. 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
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. 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. 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
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
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
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. 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
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. Repositories
» Using real infrastructure is slow
» Using fake infrastructure can lower confidence
» Use fake infrastructure but sync via contract tests
@ciaranmcnulty | #symfony_live
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. 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. 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. 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
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. 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
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. 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. 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. 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
90. Summary
» Drive domain objects directly to explore model
» Refactor to services when model is stable
» Add minimal UI coverage
@ciaranmcnulty | #symfony_live