behat best practices with symfony

91
Behat Best Practices with Symfony Ciaran McNulty @ciaranmcnulty | #symfony_live

Upload: ciaranmcnulty

Post on 21-Jan-2018

428 views

Category:

Software


4 download

TRANSCRIPT

Page 1: Behat Best Practices with Symfony

Behat Best Practices with SymfonyCiaran McNulty

@ciaranmcnulty | #symfony_live

Page 2: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 3: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 4: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 5: Behat Best Practices with Symfony

Behaviour Driven Development

@ciaranmcnulty | #symfony_live

Page 6: Behat Best Practices with Symfony

BDD is the art of using examples in conversations to illustrate behaviour

— Liz Keogh

@ciaranmcnulty | #symfony_live

Page 7: Behat Best Practices with Symfony

BDD is the art of using examples in

conversations to illustrate behaviour

@ciaranmcnulty | #symfony_live

Page 8: Behat Best Practices with Symfony

BDD is the art of using examples in

conversations to illustrate behaviour

@ciaranmcnulty | #symfony_live

Page 9: Behat Best Practices with Symfony

BDD is the art of using examples in

conversations to illustrate behaviour

@ciaranmcnulty | #symfony_live

Page 10: Behat Best Practices with Symfony

Example2 - Something that serves to illustrate or explain a rule— Wiktionary

@ciaranmcnulty | #symfony_live

Page 11: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 12: Behat Best Practices with Symfony

Rule:We charge our customers sales tax at a rate of 20%

@ciaranmcnulty | #symfony_live

Page 13: Behat Best Practices with Symfony

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

Page 14: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 15: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 16: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 17: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 18: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 19: Behat Best Practices with Symfony

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

Page 20: Behat Best Practices with Symfony

Rule:We charge our customers sales tax at a rate of 20%

Example:No! If an item is priced at £10, we charge £10and allocate £1.67 of that as sales tax

@ciaranmcnulty | #symfony_live

Page 21: Behat Best Practices with Symfony

Capturing ExamplesInput -> Rules -> Output

@ciaranmcnulty | #symfony_live

Page 22: Behat Best Practices with Symfony

Capturing ExamplesInput: When an action is takenOutput: Then an outcome should occur

@ciaranmcnulty | #symfony_live

Page 23: Behat Best Practices with Symfony

Capturing ExamplesWhen I buy a pair of Levi 501sThen I am charged £32.99

@ciaranmcnulty | #symfony_live

Page 24: Behat Best Practices with Symfony

Capturing ExamplesContext: Given some situationInput: When an action is takenOutput: Then an outcome should occur

@ciaranmcnulty | #symfony_live

Page 25: Behat Best Practices with Symfony

Capturing ExamplesGiven Levi 501s are listed at £32.99When I buy a pair of Levi 501sThen I am charged £32.99

@ciaranmcnulty | #symfony_live

Page 26: Behat Best Practices with Symfony

Context Questioning“Is there any other context which, when this event happens, will produce a different outcome?” - Liz Keogh

@ciaranmcnulty | #symfony_live

Page 27: Behat Best Practices with Symfony

Context QuestioningGiven Levi 501s are listed at £32.99When I buy a pair of Levi 501sThen I am charged £32.99

“Is there any situation where I could buy these jeans and pay a different amount?”

@ciaranmcnulty | #symfony_live

Page 28: Behat Best Practices with Symfony

Context QuestioningGiven Levi 501s are listed at £32.99When I buy a pair of Levi 501sThen 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

Page 29: Behat Best Practices with Symfony

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

Page 30: Behat Best Practices with Symfony

Outcome QuestioningGiven Levi 501s are listed at £32.99When I buy a pair of Levi 501sThen 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

Page 31: Behat Best Practices with Symfony

Outcome QuestioningGiven Levi 501s are listed at £32.99When I buy a pair of Levi 501sThen 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

Page 32: Behat Best Practices with Symfony

Validating examples

@ciaranmcnulty | #symfony_live

Page 33: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 34: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 35: Behat Best Practices with Symfony

Behat

@ciaranmcnulty | #symfony_live

Page 36: Behat Best Practices with Symfony

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

Page 37: Behat Best Practices with Symfony

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

Page 38: Behat Best Practices with Symfony

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

Page 39: Behat Best Practices with Symfony

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

Page 40: Behat Best Practices with Symfony

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

Page 41: Behat Best Practices with Symfony

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

Page 42: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 43: Behat Best Practices with Symfony

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

Page 44: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 45: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 46: Behat Best Practices with Symfony

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

Page 47: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 48: Behat Best Practices with Symfony

Given "BDD for Beginners" was proposed with a class size of 2 to 3 peopleWhen only Alice enrols on this courseThen 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

Page 49: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 50: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 51: Behat Best Practices with Symfony

Given "BDD for Beginners" was proposed with a class size of 2 to 3 peopleWhen only Alice enrols on this courseThen 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

Page 52: Behat Best Practices with Symfony

Given "BDD for Beginners" was proposed with a class size of 2 to 3 peopleWhen only Alice enrols on this courseThen 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

Page 53: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 54: Behat Best Practices with Symfony

Given "BDD for Beginners" was proposed with a class size of 2 to 3 peopleGiven Alice has already enrolled on this courseWhen Bob enrols on this courseThen 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

Page 55: Behat Best Practices with Symfony

Given "BDD for Beginners" was proposed with a class size of 2 to 3 peopleGiven Alice has already enrolled on this courseWhen Bob enrols on this courseThen 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

Page 56: Behat Best Practices with Symfony

Given "BDD for Beginners" was proposed with a class size of 2 to 3 peopleGiven Alice has already enrolled on this courseWhen Bob enrols on this courseThen 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

Page 57: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 58: Behat Best Practices with Symfony

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

Page 59: Behat Best Practices with Symfony

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

Page 60: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 61: Behat Best Practices with Symfony

Given Alice, Bob and Charlie have already enrolled on this courseWhen Derek tries to enrol on this courseThen 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

Page 62: Behat Best Practices with Symfony

Given Alice, Bob and Charlie have already enrolled on this courseWhen Derek tries to enrol on this courseThen 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

Page 63: Behat Best Practices with Symfony

Given Alice, Bob and Charlie have already enrolled on this courseWhen Derek tries to enrol on this courseThen 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 Cjm\Training\EnrolmentProblem); }}

@ciaranmcnulty | #symfony_live

Page 64: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 65: Behat Best Practices with Symfony

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

Page 66: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 67: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 68: Behat Best Practices with Symfony

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

Page 69: Behat Best Practices with Symfony

# behat.ymldefault: suites: domain: contexts: [ DomainContext ]

services: contexts: [ ServiceContext ]

extensions: Behat\Symfony2Extension: ~

@ciaranmcnulty | #symfony_live

Page 70: Behat Best Practices with Symfony

Given "BDD for Beginners" was proposed with a class size of 2 to 3 peopleWhen only Alice enrols on this courseThen 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

Page 71: Behat Best Practices with Symfony

Given "BDD for Beginners" was proposed with a class size of 2 to 3 peopleWhen only Alice enrols on this courseThen 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

Page 72: Behat Best Practices with Symfony

# behat.ymldefault: suites: domain: contexts: - DomainContext

services: contexts: - ServiceContext: courseEnrolments: "@cjm.training.enrolment.course_enrolments"

extensions: Behat\Symfony2Extension: ~

@ciaranmcnulty | #symfony_live

Page 73: Behat Best Practices with Symfony

class CourseEnrolments{ public function propose(string $title, int $minimum, int $maximum) { $this->courses->add( Course::propose( $title, ClassSize::between($minimum, $maximum) ) ); }}

@ciaranmcnulty | #symfony_live

Page 74: Behat Best Practices with Symfony

Repositories» Using real infrastructure is slow

» Using fake infrastructure can lower confidence

» Use fake infrastructure but sync via contract tests

@ciaranmcnulty | #symfony_live

Page 75: Behat Best Practices with Symfony

final class Courses implements \Cjm\Training\Enrolment\Model\Courses{ public function add(Course $course) : void { $this->courses[] = $course; }

public function findByTitle(string $title): Course { return $this->courses[0]; }}

@ciaranmcnulty | #symfony_live

Page 76: Behat Best Practices with Symfony

Given "BDD for Beginners" was proposed with a class size of 2 to 3 peopleWhen only Alice enrols on this courseThen 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

Page 77: Behat Best Practices with Symfony

Given "BDD for Beginners" was proposed with a class size of 2 to 3 peopleWhen only Alice enrols on this courseThen 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

Page 78: Behat Best Practices with Symfony

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

Page 79: Behat Best Practices with Symfony

@ciaranmcnulty | #symfony_live

Page 80: Behat Best Practices with Symfony

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

Page 81: Behat Best Practices with Symfony

Don't do thisScenario: 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

Page 82: Behat Best Practices with Symfony

Mink» Browser driver abstraction

» Supports Selenium, Goutte, Browserkit

@ciaranmcnulty | #symfony_live

Page 83: Behat Best Practices with Symfony

# behat.ymlendtoend: filters: tags: "@endtoend" suites: domain: false services: false endtoend: contexts: - EndToEndContext: courseEnrolments: "@cjm.training.enrolment.course_enrolments"

@ciaranmcnulty | #symfony_live

Page 84: Behat Best Practices with Symfony

# behat.yml extensions: Behat\Symfony2Extension: env: test_e2e Behat\MinkExtension: sessions: symfony: symfony2: ~

@ciaranmcnulty | #symfony_live

Page 85: Behat Best Practices with Symfony

Given "BDD for Beginners" was proposed with a class size of 2 to 3 peopleWhen only Alice enrols on this courseThen 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

Page 86: Behat Best Practices with Symfony

Given "BDD for Beginners" was proposed with a class size of 2 to 3 peopleWhen only Alice enrols on this courseThen 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

Page 87: Behat Best Practices with Symfony

Given "BDD for Beginners" was proposed with a class size of 2 to 3 peopleWhen only Alice enrols on this courseThen 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

Page 88: Behat Best Practices with Symfony

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

Page 89: Behat Best Practices with Symfony

# behat.yml extensions: Behat\MinkExtension: sessions: symfony: symfony2: ~ selenium2: browser: chrome capabilities: chrome: switches: - "--headless" - "--disable-gpu"

@ciaranmcnulty | #symfony_live

Page 90: Behat Best Practices with Symfony

Summary» Drive domain objects directly to explore model

» Refactor to services when model is stable

» Add minimal UI coverage

@ciaranmcnulty | #symfony_live

Page 91: Behat Best Practices with Symfony

Thanks» @ciaranmcnulty

» @Inviqa

» @PhpSpec

» @BDDLondon

github.com/ciaranmcnulty/behat-symfony-demo

@ciaranmcnulty | #symfony_live