Behat Best Practices with
Symfony
Ciaran McNulty
@ciaranmcnulty | #symfony_live
@ciaranmcnulty | #symfony_live
@ciaranmcnulty | #symfony_live
@ciaranmcnulty | #symfony_live
Behaviour Driven
Development
@ciaranmcnulty | #symfony_live
BDD is the art of using examples in
conversations to illustrate behaviour
— Liz Keogh
@ciaranmcnulty | #symfony_live
BDD is the art of using
examples in
conversations to
illustrate behaviour
@ciaranmcnulty | #symfony_live
BDD is the art of using
examples in
conversations to
illustrate behaviour
@ciaranmcnulty | #symfony_live
BDD is the art of using
examples in
conversations to
illustrate behaviour
@ciaranmcnulty | #symfony_live
Example
2 - Something that serves to illustrate or explain a rule
— Wiktionary
@ciaranmcnulty | #symfony_live
@ciaranmcnulty | #symfony_live
Rule:
We charge our customers sales tax at a rate of 20%
@ciaranmcnulty | #symfony_live
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
@ciaranmcnulty | #symfony_live
@ciaranmcnulty | #symfony_live
@ciaranmcnulty | #symfony_live
@ciaranmcnulty | #symfony_live
@ciaranmcnulty | #symfony_live
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
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
Capturing Examples
Input -> Rules -> Output
@ciaranmcnulty | #symfony_live
Capturing Examples
Input: When an action is taken
Output: Then an outcome should occur
@ciaranmcnulty | #symfony_live
Capturing Examples
When I buy a pair of Levi 501s
Then I am charged £32.99
@ciaranmcnulty | #symfony_live
Capturing Examples
Context: Given some situation
Input: When an action is taken
Output: Then an outcome should occur
@ciaranmcnulty | #symfony_live
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
Context Questioning
“Is there any other context which, when this event happens, will
produce a different outcome?” - Liz Keogh
@ciaranmcnulty | #symfony_live
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
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
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
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
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
Validating examples
@ciaranmcnulty | #symfony_live
@ciaranmcnulty | #symfony_live
@ciaranmcnulty | #symfony_live
Behat
@ciaranmcnulty | #symfony_live
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
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
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
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
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
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
@ciaranmcnulty | #symfony_live
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
@ciaranmcnulty | #symfony_live
@ciaranmcnulty | #symfony_live
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
@ciaranmcnulty | #symfony_live
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
@ciaranmcnulty | #symfony_live
@ciaranmcnulty | #symfony_live
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
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
@ciaranmcnulty | #symfony_live
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
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
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
@ciaranmcnulty | #symfony_live
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
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
@ciaranmcnulty | #symfony_live
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
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
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
@ciaranmcnulty | #symfony_live
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
@ciaranmcnulty | #symfony_live
@ciaranmcnulty | #symfony_live
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
# behat.yml
default:
suites:
domain:
contexts: [ DomainContext ]
services:
contexts: [ ServiceContext ]
extensions:
BehatSymfony2Extension: ~
@ciaranmcnulty | #symfony_live
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
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
# behat.yml
default:
suites:
domain:
contexts:
- DomainContext
services:
contexts:
- ServiceContext:
courseEnrolments: "@cjm.training.enrolment.course_enrolments"
extensions:
BehatSymfony2Extension: ~
@ciaranmcnulty | #symfony_live
class CourseEnrolments
{
public function propose(string $title, int $minimum, int $maximum)
{
$this->courses->add(
Course::propose(
$title,
ClassSize::between($minimum, $maximum)
)
);
}
}
@ciaranmcnulty | #symfony_live
Repositories
» Using real infrastructure is slow
» Using fake infrastructure can lower confidence
» Use fake infrastructure but sync via contract tests
@ciaranmcnulty | #symfony_live
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
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
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
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
@ciaranmcnulty | #symfony_live
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
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
Mink
» Browser driver abstraction
» Supports Selenium, Goutte, Browserkit
@ciaranmcnulty | #symfony_live
# behat.yml
endtoend:
filters:
tags: "@endtoend"
suites:
domain: false
services: false
endtoend:
contexts:
- EndToEndContext:
courseEnrolments: "@cjm.training.enrolment.course_enrolments"
@ciaranmcnulty | #symfony_live
# behat.yml
extensions:
BehatSymfony2Extension:
env:
test_e2e
BehatMinkExtension:
sessions:
symfony:
symfony2: ~
@ciaranmcnulty | #symfony_live
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
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
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
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
# behat.yml
extensions:
BehatMinkExtension:
sessions:
symfony:
symfony2: ~
selenium2:
browser: chrome
capabilities:
chrome:
switches:
- "--headless"
- "--disable-gpu"
@ciaranmcnulty | #symfony_live
Summary
» Drive domain objects directly to explore model
» Refactor to services when model is stable
» Add minimal UI coverage
@ciaranmcnulty | #symfony_live
Thanks
» @ciaranmcnulty
» @Inviqa
» @PhpSpec
» @BDDLondon
github.com/ciaranmcnulty/behat-symfony-demo
@ciaranmcnulty | #symfony_live

Behat Best Practices with Symfony

  • 1.
    Behat Best Practiceswith Symfony Ciaran McNulty @ciaranmcnulty | #symfony_live
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
    BDD is theart of using examples in conversations to illustrate behaviour — Liz Keogh @ciaranmcnulty | #symfony_live
  • 7.
    BDD is theart of using examples in conversations to illustrate behaviour @ciaranmcnulty | #symfony_live
  • 8.
    BDD is theart of using examples in conversations to illustrate behaviour @ciaranmcnulty | #symfony_live
  • 9.
    BDD is theart of using examples in conversations to illustrate behaviour @ciaranmcnulty | #symfony_live
  • 10.
    Example 2 - Somethingthat serves to illustrate or explain a rule — Wiktionary @ciaranmcnulty | #symfony_live
  • 11.
  • 12.
    Rule: We charge ourcustomers sales tax at a rate of 20% @ciaranmcnulty | #symfony_live
  • 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 | #symfony_live
  • 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 | #symfony_live
  • 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 | #symfony_live
  • 21.
    Capturing Examples Input ->Rules -> Output @ciaranmcnulty | #symfony_live
  • 22.
    Capturing Examples Input: Whenan action is taken Output: Then an outcome should occur @ciaranmcnulty | #symfony_live
  • 23.
    Capturing Examples When Ibuy a pair of Levi 501s Then I am charged £32.99 @ciaranmcnulty | #symfony_live
  • 24.
    Capturing Examples Context: Givensome situation Input: When an action is taken Output: Then an outcome should occur @ciaranmcnulty | #symfony_live
  • 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 | #symfony_live
  • 26.
    Context Questioning “Is thereany other context which, when this event happens, will produce a different outcome?” - Liz Keogh @ciaranmcnulty | #symfony_live
  • 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 | #symfony_live
  • 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 | #symfony_live
  • 29.
    Outcome Questioning “Given thiscontext, when this event happens, is there another outcome that’s important? Something we missed, perhaps?” - Liz Keogh @ciaranmcnulty | #symfony_live
  • 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 | #symfony_live
  • 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 | #symfony_live
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
    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 | #symfony_live
  • 37.
    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 | #symfony_live
  • 38.
    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 | #symfony_live
  • 39.
    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 | #symfony_live
  • 40.
    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 | #symfony_live
  • 41.
    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 | #symfony_live
  • 42.
  • 43.
    Gherkin: Given a thinghappens to Ciaran PHP: /** * @Given a thing happens to :person */ public function doAThing(string $person) { // you have to write this } @ciaranmcnulty | #symfony_live
  • 44.
  • 45.
  • 46.
    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 | #symfony_live
  • 47.
  • 48.
    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 | #symfony_live
  • 49.
  • 50.
  • 51.
    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 | #symfony_live
  • 52.
    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 | #symfony_live
  • 53.
  • 54.
    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 | #symfony_live
  • 55.
    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 | #symfony_live
  • 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 { /** * @Then this course will be viable */ public function thisCourseWillBeViable() { assert($this->course->isViable() == true); } } @ciaranmcnulty | #symfony_live
  • 57.
  • 58.
    class Course { //... public functionisViable(): bool { return $this->classSize->isViable($this->learners); } } class ClassSize { //... public function isViable(int $size) : bool { return $size >= $this->min; } } @ciaranmcnulty | #symfony_live
  • 59.
    class Course { //... public functionisViable() { return $this->classSize->isViable($this->learners); } } class ClassSize { //... public function isViable(int $size) { return $size >= $this->min; } } @ciaranmcnulty | #symfony_live
  • 60.
  • 61.
    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 | #symfony_live
  • 62.
    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 | #symfony_live
  • 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 { /** * @Then (s)he should not be able to enrol */ public function learnerShouldNotBeAbleToEnrol() { assert($this->enrolmentProblem instanceof CjmTrainingEnrolmentProblem); } } @ciaranmcnulty | #symfony_live
  • 64.
  • 65.
    final class Course { publicfunction enrol(Learner $learner) { if (!$this->classSize->hasMoreCapacity($this->learners)) { throw new EnrolmentProblem('Class is already at capacity'); } $this->learners++; } } @ciaranmcnulty | #symfony_live
  • 66.
  • 67.
  • 68.
    Driving the Servicelayer » 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.
    # behat.yml default: suites: domain: contexts: [DomainContext ] services: contexts: [ ServiceContext ] extensions: BehatSymfony2Extension: ~ @ciaranmcnulty | #symfony_live
  • 70.
    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 | #symfony_live
  • 71.
    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 | #symfony_live
  • 72.
    # behat.yml default: suites: domain: contexts: - DomainContext services: contexts: -ServiceContext: courseEnrolments: "@cjm.training.enrolment.course_enrolments" extensions: BehatSymfony2Extension: ~ @ciaranmcnulty | #symfony_live
  • 73.
    class CourseEnrolments { public functionpropose(string $title, int $minimum, int $maximum) { $this->courses->add( Course::propose( $title, ClassSize::between($minimum, $maximum) ) ); } } @ciaranmcnulty | #symfony_live
  • 74.
    Repositories » Using realinfrastructure is slow » Using fake infrastructure can lower confidence » Use fake infrastructure but sync via contract tests @ciaranmcnulty | #symfony_live
  • 75.
    final class Coursesimplements 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 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->course); } } @ciaranmcnulty | #symfony_live
  • 77.
    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->course) == false); } } @ciaranmcnulty | #symfony_live
  • 78.
    Domain vs Servicelayer » Start by driving domain layer » Refactor to services when confidence grows » Drop back to domain layer when remodelling @ciaranmcnulty | #symfony_live
  • 79.
  • 80.
    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 | #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
  • 82.
    Mink » Browser driverabstraction » Supports Selenium, Goutte, Browserkit @ciaranmcnulty | #symfony_live
  • 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.
  • 85.
    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 | #symfony_live
  • 86.
    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 | #symfony_live
  • 87.
    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 | #symfony_live
  • 88.
    Automating a realbrowser » Avoid or minimise » Orders of magnitude slower » Required for end-to-end with JS » Replace with JS cucumber stack @ciaranmcnulty | #symfony_live
  • 89.
    # behat.yml extensions: BehatMinkExtension: sessions: symfony: symfony2: ~ selenium2: browser:chrome capabilities: chrome: switches: - "--headless" - "--disable-gpu" @ciaranmcnulty | #symfony_live
  • 90.
    Summary » Drive domainobjects directly to explore model » Refactor to services when model is stable » Add minimal UI coverage @ciaranmcnulty | #symfony_live
  • 91.
    Thanks » @ciaranmcnulty » @Inviqa »@PhpSpec » @BDDLondon github.com/ciaranmcnulty/behat-symfony-demo @ciaranmcnulty | #symfony_live