The document discusses best practices for using Behat, a behavior-driven development (BDD) framework, for testing Symfony applications. It covers how to write feature files with Gherkin syntax, map examples to PHP step definitions, and drive the domain and service layers with Behat scenarios. Running scenarios directly against domain objects or injecting services into a Behat context allows testing features end-to-end while aligning code with business needs. Using fakes for infrastructure keeps tests fast but contract tests sync code with real systems.
12. Rule:
We charge our customers sales tax at a rate of 20%
Crania Ltd | @ciaranmcnulty | #symfonycon
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
Crania Ltd | @ciaranmcnulty | #symfonycon
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
Crania Ltd | @ciaranmcnulty | #symfonycon
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
Crania Ltd | @ciaranmcnulty | #symfonycon
22. Capturing Examples
Action Outcome
What causes the behaviour? What is the result of the
behaviour?
I buy a pair of Levi 501s I am charged £32.99
Crania Ltd | @ciaranmcnulty | #symfonycon
23. Why does that action
cause that outcome?
Crania Ltd | @ciaranmcnulty | #symfonycon
24. Capturing Examples
Context Action Outcome
What happened in
the past that affects
the behaviour?
What causes the
behaviour?
What is the result of
the behaviour?
Levi 501s are listed
at £32.99
I buy a pair of Levi
501s
I am charged £32.99
Crania Ltd | @ciaranmcnulty | #symfonycon
26. Context Questioning
“Is there any other context which, when this event happens, will
produce a different outcome?” - Liz Keogh
Crania Ltd | @ciaranmcnulty | #symfonycon
27. Context Questioning
Context Action Outcome
Levi 501s are listed
at £32.99
I buy a pair of Levi
501s
I am charged £32.99
“Is there any situation where I could buy these jeans and pay a different
amount?”
Crania Ltd | @ciaranmcnulty | #symfonycon
28. Context Questioning
Context Action Outcome
Levi 501s are listed at
£32.99
I buy a pair of Levi 501s I am charged £32.99
Levis are on sale " ?
Purchaser is a staff
member
" ?
The jeans are damaged " ?
Crania Ltd | @ciaranmcnulty | #symfonycon
29. Outcome Questioning
“Given this context, when this event happens, is there another outcome
that’s important? Something we missed, perhaps?” - Liz Keogh
Crania Ltd | @ciaranmcnulty | #symfonycon
30. Outcome Questioning
Context Action Outcome
Levi 501s are listed
at £32.99
I buy a pair of Levi
501s
I am charged £32.99
"Aside from me being charged for the jeans, does something else
happen that we need to care about?"
Crania Ltd | @ciaranmcnulty | #symfonycon
31. Outcome Questioning
Context Action Outcome
Levi 501s are listed
at £32.99
I buy a pair of Levi
501s
I am charged
£32.99 ...and I get
sent some
jeans ...and the
warehouse stock
level is reduced
Crania Ltd | @ciaranmcnulty | #symfonycon
43. Outstanding questions
» Can we start work on this item without this answer?
» Do we need to resolve it first?
» Can the story be split along this seam?
Crania Ltd | @ciaranmcnulty | #symfonycon
58. 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
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
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
Crania Ltd | @ciaranmcnulty | #symfonycon
59. 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
Crania Ltd | @ciaranmcnulty | #symfonycon
60. 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
Crania Ltd | @ciaranmcnulty | #symfonycon
61. 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
Crania Ltd | @ciaranmcnulty | #symfonycon
62. 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
Crania Ltd | @ciaranmcnulty | #symfonycon
63. 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
Crania Ltd | @ciaranmcnulty | #symfonycon
65. Gherkin:
Given a thing happens to Ciaran
PHP:
/**
* @Given a thing happens to :person
*/
public function doAThing(string $person)
{
// you have to write this
}
Crania Ltd | @ciaranmcnulty | #symfonycon
68. 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
Crania Ltd | @ciaranmcnulty | #symfonycon
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 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)
);
}
}
Crania Ltd | @ciaranmcnulty | #symfonycon
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 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);
}
}
Crania Ltd | @ciaranmcnulty | #symfonycon
74. 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);
}
}
Crania Ltd | @ciaranmcnulty | #symfonycon
78. 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
Crania Ltd | @ciaranmcnulty | #symfonycon
80. 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);
}
}
Crania Ltd | @ciaranmcnulty | #symfonycon
81. 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);
}
}
Crania Ltd | @ciaranmcnulty | #symfonycon
83. class CourseEnrolments
{
private $courses;
public function __construct(Courses $courses)
{
$this->courses = $courses;
}
public function propose(string $title, int $minimum, int $maximum)
{
$this->courses->add(
Course::propose(
$title,
ClassSize::between($minimum, $maximum)
)
);
}
}
Crania Ltd | @ciaranmcnulty | #symfonycon
84. Infrastructure
» Using real infrastructure is slow
» Using fake infrastructure can lower confidence
» Use fake infrastructure but sync via contract tests
Crania Ltd | @ciaranmcnulty | #symfonycon
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 ServiceContext implements Context
{
/**
* @When (only) :learner enrols on this course
*/
public function learnerEnrolsOnThisCourse(string $learner)
{
$this->courseEnrolments->enrol($learner, $this->courseId);
}
}
Crania Ltd | @ciaranmcnulty | #symfonycon
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 ServiceContext implements Context
{
/**
* @Then this course will not be viable
*/
public function thisCourseWillNotBeViable()
{
assert($this->courseEnrolments->isCourseViable($this->courseId) == false);
}
}
Crania Ltd | @ciaranmcnulty | #symfonycon
87. Domain vs Service layer
» Start by driving domain layer
» Refactor to services when confidence grows
» Drop back to domain layer when remodelling
Crania Ltd | @ciaranmcnulty | #symfonycon
89. 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
Crania Ltd | @ciaranmcnulty | #symfonycon
90. 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"
Crania Ltd | @ciaranmcnulty | #symfonycon
95. 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);
}
}
Crania Ltd | @ciaranmcnulty | #symfonycon
96. 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');
}
}
Crania Ltd | @ciaranmcnulty | #symfonycon
97. 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');
}
}
Crania Ltd | @ciaranmcnulty | #symfonycon
98. 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
Crania Ltd | @ciaranmcnulty | #symfonycon
100. chrome --disable-gpu --headless
--remote-debugging-address=0.0.0.0
--remote-debugging-port=9222
or
docker run -d -p 9222:9222
--cap-add=SYS_ADMIN
justinribeiro/chrome-headless
Crania Ltd | @ciaranmcnulty | #symfonycon
101. Summary
» Drive domain objects directly to explore model
» Refactor to services when model is stable
» Add minimal UI coverage
Crania Ltd | @ciaranmcnulty | #symfonycon