Behat Best
Practices
Ciaran McNulty
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
Behaviour Driven
Development
@ciaranmcnulty | #scotphp17
BDD is the art of using examples in
conversations to illustrate behaviour
— Liz Keogh
@ciaranmcnulty | #scotphp17
BDD is the art of using
examples in
conversations to
illustrate behaviour
@ciaranmcnulty | #scotphp17
BDD is the art of using
examples in
conversations to
illustrate behaviour
@ciaranmcnulty | #scotphp17
BDD is the art of using
examples in
conversations to
illustrate behaviour
@ciaranmcnulty | #scotphp17
Example
2 - Something that serves to illustrate or explain a rule
— Wiktionary
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
Rule:
We charge our customers sales tax at a rate of 20%
@ciaranmcnulty | #scotphp17
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
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
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
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
Capturing Examples
Input -> Rules -> Output
@ciaranmcnulty | #scotphp17
Capturing Examples
Input: When an action is taken
Output: Then an outcome should occur
@ciaranmcnulty | #scotphp17
Capturing Examples
When I buy a pair of Levi 501s
Then I am charged £32.99
@ciaranmcnulty | #scotphp17
Capturing Examples
Context: Given some situation
Input: When an action is taken
Output: Then an outcome should occur
@ciaranmcnulty | #scotphp17
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
Context Questioning
“Is there any other context which, when this event happens, will
produce a different outcome?” - Liz Keogh
@ciaranmcnulty | #scotphp17
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
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
Outcome Questioning
“Given this context, when this event happens, is there another outcome
that’s important? Something we missed, perhaps?” - Liz Keogh
@ciaranmcnulty | #scotphp17
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
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
BDD is not about
testing
@ciaranmcnulty | #scotphp17
BDD is also not about
requirement capture
@ciaranmcnulty | #scotphp17
Validating examples
(ok it is a bit about testing,
sometimes)
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
Behat
@ciaranmcnulty | #scotphp17
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
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
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
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
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
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
@ciaranmcnulty | #scotphp17
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
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
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
@ciaranmcnulty | #scotphp17
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
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
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
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
@ciaranmcnulty | #scotphp17
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
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
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
@ciaranmcnulty | #scotphp17
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
class Course
{
//...
public function isViable()
{
return $this->classSize->isViable($this->learners);
}
}
class ClassSize
{
//...
public function isViable(int $size)
{
return $size >= $this->min;
}
}
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
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
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
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
@ciaranmcnulty | #scotphp17
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
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
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
Suites
# behat.yml
default:
suites:
domain:
contexts: [ DomainContext ]
services:
contexts: [ ServiceContext ]
@ciaranmcnulty | #scotphp17
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
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
Symfony extension
# behat.yml
default:
suites:
domain:
contexts:
- DomainContext
services:
contexts:
- ServiceContext:
courseEnrolments: "@cjm.training.enrolment.course_enrolments"
extensions:
BehatSymfony2Extension: ~
@ciaranmcnulty | #scotphp17
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
Autowiring
Type hint services and get them automatically injected in:
» Constructors
» Step definitions
» Transformations
@ciaranmcnulty | #scotphp17
class CourseEnrolments
{
public function propose(string $title, int $minimum, int $maximum)
{
$this->courses->add(
Course::propose(
$title,
ClassSize::between($minimum, $maximum)
)
);
}
}
@ciaranmcnulty | #scotphp17
Infrastructure
» Using real infrastructure is slow
» Using fake infrastructure can lower confidence
» Use fake infrastructure but sync via contract tests
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
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
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
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
Domain vs Service layer
» Start by driving domain layer
» Refactor to services when confidence grows
» Drop back to domain layer when remodelling
@ciaranmcnulty | #scotphp17
@ciaranmcnulty | #scotphp17
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
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
Mink
» Browser driver abstraction
» Supports Selenium, Goutte, Browserkit
@ciaranmcnulty | #scotphp17
# 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
Mink with Symfony
# behat.yml
extensions:
BehatSymfony2Extension: ~
BehatMinkExtension:
sessions:
symfony:
symfony2: ~
@ciaranmcnulty | #scotphp17
Mink with PSR-7
# behat.yml
extensions:
CjmBehatPsr7Extension:
app: %paths.base%/path/to/file.php
BehatMinkExtension:
sessions:
psr:
psr-7: ~
@ciaranmcnulty | #scotphp17
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
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
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
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
# behat.yml
extensions:
BehatMinkExtension:
sessions:
symfony:
symfony2: ~
selenium2:
browser: chrome
capabilities:
chrome:
switches:
- "--headless"
- "--disable-gpu"
@ciaranmcnulty | #scotphp17
Things I'm trying out
» dmore/behat-chrome-extension
» aligning end-to-end tests to service APIs
» property-based testing
@ciaranmcnulty | #scotphp17
Summary
» Drive domain objects directly to explore model
» Refactor to services when model is stable
» Add minimal UI coverage
@ciaranmcnulty | #scotphp17
Thanks
» @ciaranmcnulty
» @Inviqa
» @PhpSpec
» @BDDLondon
» @SymfonyUK
github.com/ciaranmcnulty/behat-symfony-demo
joind.in/event/scotlandphp-2017/behat-best-practices
@ciaranmcnulty | #scotphp17

Behat Best Practices

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
    BDD is theart of using examples in conversations to illustrate behaviour — Liz Keogh @ciaranmcnulty | #scotphp17
  • 7.
    BDD is theart of using examples in conversations to illustrate behaviour @ciaranmcnulty | #scotphp17
  • 8.
    BDD is theart of using examples in conversations to illustrate behaviour @ciaranmcnulty | #scotphp17
  • 9.
    BDD is theart of using examples in conversations to illustrate behaviour @ciaranmcnulty | #scotphp17
  • 10.
    Example 2 - Somethingthat serves to illustrate or explain a rule — Wiktionary @ciaranmcnulty | #scotphp17
  • 11.
  • 12.
    Rule: We charge ourcustomers sales tax at a rate of 20% @ciaranmcnulty | #scotphp17
  • 13.
    Rule: We charge ourcustomers 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.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
    Rule: We charge ourcustomers 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.
    Rule: We charge ourcustomers 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.
    Capturing Examples Input ->Rules -> Output @ciaranmcnulty | #scotphp17
  • 22.
    Capturing Examples Input: Whenan action is taken Output: Then an outcome should occur @ciaranmcnulty | #scotphp17
  • 23.
    Capturing Examples When Ibuy a pair of Levi 501s Then I am charged £32.99 @ciaranmcnulty | #scotphp17
  • 24.
    Capturing Examples Context: Givensome situation Input: When an action is taken Output: Then an outcome should occur @ciaranmcnulty | #scotphp17
  • 25.
    Capturing Examples Given Levi501s are listed at £32.99 When I buy a pair of Levi 501s Then I am charged £32.99 @ciaranmcnulty | #scotphp17
  • 26.
    Context Questioning “Is thereany other context which, when this event happens, will produce a different outcome?” - Liz Keogh @ciaranmcnulty | #scotphp17
  • 27.
    Context Questioning Given Levi501s 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.
    Context Questioning Given Levi501s 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.
    Outcome Questioning “Given thiscontext, when this event happens, is there another outcome that’s important? Something we missed, perhaps?” - Liz Keogh @ciaranmcnulty | #scotphp17
  • 30.
    Outcome Questioning Given Levi501s 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.
    Outcome Questioning Given Levi501s 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.
    BDD is notabout testing @ciaranmcnulty | #scotphp17
  • 33.
    BDD is alsonot about requirement capture @ciaranmcnulty | #scotphp17
  • 34.
    Validating examples (ok itis a bit about testing, sometimes) @ciaranmcnulty | #scotphp17
  • 35.
  • 36.
  • 37.
  • 38.
    Feature: Scheduling atraining 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.
    Feature: Scheduling atraining 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.
    Background: Given "BDD forBeginners" 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.
    Background: Given "BDD forBeginners" 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.
    Background: Given "BDD forBeginners" 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.
    Background: Given "BDD forBeginners" 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.
  • 45.
    Gherkin: Given a thinghappens to Ciaran PHP: /** * @Given a thing happens to :person */ public function doAThing(string $person) { // you have to write this } @ciaranmcnulty | #scotphp17
  • 46.
  • 47.
  • 48.
    Driving the Domainlayer » 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.
  • 50.
    Given "BDD forBeginners" 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.
  • 52.
  • 53.
    Given "BDD forBeginners" 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.
    Given "BDD forBeginners" 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.
  • 56.
    Given "BDD forBeginners" 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.
    Given "BDD forBeginners" 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.
    Given "BDD forBeginners" 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.
  • 60.
    class Course { //... public functionisViable(): bool { return $this->classSize->isViable($this->learners); } } class ClassSize { //... public function isViable(int $size) : bool { return $size >= $this->min; } } @ciaranmcnulty | #scotphp17
  • 61.
    class Course { //... public functionisViable() { return $this->classSize->isViable($this->learners); } } class ClassSize { //... public function isViable(int $size) { return $size >= $this->min; } } @ciaranmcnulty | #scotphp17
  • 62.
  • 63.
    Given Alice, Boband 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.
    Given Alice, Boband 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.
    Given Alice, Boband 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.
  • 67.
    final class Course { publicfunction enrol(Learner $learner) { if (!$this->classSize->hasMoreCapacity($this->learners)) { throw new EnrolmentProblem('Class is already at capacity'); } $this->learners++; } } @ciaranmcnulty | #scotphp17
  • 68.
  • 69.
  • 70.
    Driving the Servicelayer » 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.
    Suites # behat.yml default: suites: domain: contexts: [DomainContext ] services: contexts: [ ServiceContext ] @ciaranmcnulty | #scotphp17
  • 72.
    Given "BDD forBeginners" 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.
    Given "BDD forBeginners" 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.
    Symfony extension # behat.yml default: suites: domain: contexts: -DomainContext services: contexts: - ServiceContext: courseEnrolments: "@cjm.training.enrolment.course_enrolments" extensions: BehatSymfony2Extension: ~ @ciaranmcnulty | #scotphp17
  • 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.
    Autowiring Type hint servicesand get them automatically injected in: » Constructors » Step definitions » Transformations @ciaranmcnulty | #scotphp17
  • 77.
    class CourseEnrolments { public functionpropose(string $title, int $minimum, int $maximum) { $this->courses->add( Course::propose( $title, ClassSize::between($minimum, $maximum) ) ); } } @ciaranmcnulty | #scotphp17
  • 78.
    Infrastructure » Using realinfrastructure is slow » Using fake infrastructure can lower confidence » Use fake infrastructure but sync via contract tests @ciaranmcnulty | #scotphp17
  • 79.
  • 80.
  • 81.
    final class Coursesimplements CjmTrainingEnrolmentModelCourses { public function add(Course $course) : void { $this->courses[] = $course; } public function findByTitle(string $title): Course { return $this->courses[0]; } } @ciaranmcnulty | #scotphp17
  • 82.
    Given "BDD forBeginners" 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.
    Given "BDD forBeginners" 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.
    Domain vs Servicelayer » Start by driving domain layer » Refactor to services when confidence grows » Drop back to domain layer when remodelling @ciaranmcnulty | #scotphp17
  • 85.
  • 86.
    Driving the UIlayer » 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.
    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.
    Mink » Browser driverabstraction » Supports Selenium, Goutte, Browserkit @ciaranmcnulty | #scotphp17
  • 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.
    Mink with Symfony #behat.yml extensions: BehatSymfony2Extension: ~ BehatMinkExtension: sessions: symfony: symfony2: ~ @ciaranmcnulty | #scotphp17
  • 91.
    Mink with PSR-7 #behat.yml extensions: CjmBehatPsr7Extension: app: %paths.base%/path/to/file.php BehatMinkExtension: sessions: psr: psr-7: ~ @ciaranmcnulty | #scotphp17
  • 92.
    Given "BDD forBeginners" 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.
    Given "BDD forBeginners" 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.
    Given "BDD forBeginners" 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.
    Automating a realbrowser » 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.
    # behat.yml extensions: BehatMinkExtension: sessions: symfony: symfony2: ~ selenium2: browser:chrome capabilities: chrome: switches: - "--headless" - "--disable-gpu" @ciaranmcnulty | #scotphp17
  • 97.
    Things I'm tryingout » dmore/behat-chrome-extension » aligning end-to-end tests to service APIs » property-based testing @ciaranmcnulty | #scotphp17
  • 98.
    Summary » Drive domainobjects directly to explore model » Refactor to services when model is stable » Add minimal UI coverage @ciaranmcnulty | #scotphp17
  • 99.
    Thanks » @ciaranmcnulty » @Inviqa »@PhpSpec » @BDDLondon » @SymfonyUK github.com/ciaranmcnulty/behat-symfony-demo joind.in/event/scotlandphp-2017/behat-best-practices @ciaranmcnulty | #scotphp17