Download - Symfony2 Specification by examples
Specification by Examplesfor Symfony2 applications
About meCofounder Cofounder
on Github on Twitter
- PHP & Cloud @ Genova - 24 Oct - Internet Of Things! @ Turin [CFP] - 15 Nov
- CloudComputing @ Turin [CFP ASAP]
Corley S.r.l. - @CorleyCloudUpCloo LTD - @UpCloo
wdalmutwalterdalmut
www.cloudparty.itinternetof.itwww.cloudconf.it
Repo with all examplesRepo on github
Software deliverycustomer wants Aproject manager understand Bdevelopers write down Cdevelopers effectively write down Dcustomer realize to get E
Successive refinements of EE'E''E'''E''''E'''''
The customer will never obtain an A
The problem is to achieve asingle communication domain
How many points does it have?Example from: Bridging the communication gap (Gojko Adzic)
10
5
14
9
Effectively, there is no rightanswer.
It depends on what you consider as a point
Behaviour-driven developmentis a software development process based on test-driven development(TDD). Behavior-driven development combines the general techniques
and principles of TDD with ideas from domain-driven design and object-oriented analysis and design to provide software development and
management teams with shared tools and a shared process tocollaborate on software development.
from wikipedia
BDDAlthough BDD is principally an idea about how software developmentshould be managed by both business interests and technical insight
from wikipedia
Dan North: What is a story?Behaviour-driven development is an “outside-in” methodology. It startsat the outside by identifying business outcomes, and then drills downinto the feature set that will achieve those outcomes. Each feature is
captured as a “story”, which defines the scope of the feature along withits acceptance criteria. This article introduces the BDD approach to
defining and identifying stories and their acceptance criteria.Read the article here
BDD is more than tools, but weare here in order to use BDD
methodology in our Symfony2projects
ToolsBehat - Behaviour-driven framework
Mink - Web Acceptance testing framework
Pay attention on Behat versionThe latest is 3.0 (at today) but there are so many examples and
docs for 2.5
We will work with 3.0
Here is a storyFeature: a description of this feature
Scenario: A business situation Given a precondition And another precondition When an actor execute an action And another action appends Then some testable result is in place And another condition should be verified
Scenario: Another business scenario starts
Here is a story in ItalianFunzionalità: una generica descrizione di questa funzionalità
Scenario: una situazione di business Data una precondizione E ancora una nuova precondizione Quando un attore esegue una azione E succede anche un altra azione Allora esite un risultato testabile Ed un nuova condizione da verificare
Scenario: Un altro scenario di business da verificare
Gherkin is a Business Readableand Domain Specific Languagethat let us describe software’s
behaviour without detailinghow that behaviour is
implemented.From the cucumber project
Gherkin supports so manylanguages
We are building a common language withdifferent actors involved into to design
process
There are at least two ways to use behat inyour Symfony2 applications
Per-bundle featuresSpecific features per bundle
Application featuresAll features in a single folder
Personally prefer a single place for allfeatures
Mainly because it is so difficult to share theapplication bundle strategies to a
customerKeep the focus on the communication not
how we implement the solution
behat.ymldefault: suites: default: path: %paths.base%/features contexts: [Behat\MinkExtension\Context\MinkContext] extensions: Behat\Symfony2Extension: ~ Behat\MinkExtension: base_url: http://localhost:8000 sessions: default: symfony2: ~
Behat uses a "FeatureContext"as a clue for join the Gherkin
language with the application
A featureFeature: An hello world feature example
Scenario: Just say hello to me! When I am on "/hello/walter" Then I should see "Hello Walter"
Every step is a custom method in theFeatureContext
/** @Given /̂I am on "([̂"]*)"$/ */public function goOnPage($url){...}
Mink carries different predefined stepsI am on "a page"I press "Get me in!"I should see "Ok, you are in"and more... (CSS selectors etc.)
But remember that we are building acommon communication domain
Write down a new step that use a mink's steps instead force a newdefinition of something
By ExamplesScenario Outline: Just say hello to everyone! When I am on <page> Then I should see <hello>
Examples: | page | hello | | "/hello/walter" | "Hello Walter" | | "/hello/marco" | "Hello Marco" | | "/hello/giovanni" | "Hello Giovanni" | | "/hello/martina" | "Hello Martina" |
ExpectationsScenario Outline: Say a better hello to special guests When I am on <page> Then I should see <hello>
Examples: | page | hello | | "/hello/fabien" | "Captain on the bridge" | | "/hello/walter" | "Hello Walter" | | "/hello/marco" | "Hello Marco" | | "/hello/giovanni" | "Hello Giovanni" | | "/hello/martina" | "Hello Martina" |
If we run itFeature: An hello world feature example
Scenario Outline: Say a better hello to special guests When I am on <page> Then I should see <hello>
Examples: | page | hello | | "/hello/fabien" | "Captain on the bridge" | The text "Captain on the bridge" was not found anywhere in the text of the current page. (Behat\Mink\Exception\ResponseTextException) | "/hello/walter" | "Hello Walter" | | "/hello/marco" | "Hello Marco" | | "/hello/giovanni" | "Hello Giovanni" | | "/hello/martina" | "Hello Martina" |
‐‐‐ Failed scenarios:
features/hello.feature:24
Fix the codeclass DefaultController extends Controller{ public function indexAction($name) { if ($name == "fabien") { $name = "Captain on the bridge"; }
return $this‐>render('CorleyBaseBundle:Default:index.html.twig', array('name' => $name)); }}
My steps interacts with the entity managerdefault: suites: default: path: %paths.base%/features contexts: ‐ Behat\MinkExtension\Context\MinkContext ‐ HelloFeatureContext: entityManager: '@doctrine.orm.entity_manager' extensions: Behat\Symfony2Extension: ~ Behat\MinkExtension: base_url: http://localhost:8000 sessions: default: symfony2: ~
Create contextsbin/behat ‐‐init
Context constructor/** * Behat context class. */class HelloFeatureContext implements SnippetAcceptingContext{ private $entityManager;
/** * Initializes context. * * Every scenario gets its own context object. * You can also pass arbitrary arguments to the context constructor through behat.yml. */ public function __construct(EntityManager $entityManager) { $this‐>entityManager = $entityManager; }
My context clears all before runs/** * @BeforeScenario */public function clearDatabase(){ $purger = new ORMPurger($this‐>entityManager); $purger‐>purge();}
Load books for this scenarioFeature: My books features
Scenario: List my books Given there are books | title | author | | Specification by Examples | Gojko Adzic | | Bridging the communication gap | Gojko Adzic | | The RSpec Book | David Chelimsky | When I am on "/books" Then I should see "Specification by Examples" And I should see "Bridging the communication gap" And I should see "The RSpec Book"
Run it!(and get the missing step definition)
‐‐‐ HelloFeatureContext has missing steps. Define them with these snippets:
/** * @Given there are books */ public function thereAreBooks(TableNode $table) { throw new PendingException(); }
My custom step/** * @Given there are books */public function thereAreBooks(TableNode $table){ foreach ($table‐>getHash() as $row) { $book = new Book();
$book‐>setTitle($row["title"]); $book‐>setAuthor($row["author"]);
$this‐>entityManager‐>persist($book); } $this‐>entityManager‐>flush();}
Scenarios can share the same backgroundFeature: My books features
Background: Given there are books | title | author | | Specification by Examples | Gojko Adzic | | Bridging the communication gap | Gojko Adzic | | The RSpec Book | David Chelimsky |
Scenario: List my books When I am on "/books" Then I should see "Specification by Examples" And I should see "Bridging the communication gap" And I should see "The RSpec Book"
A storyScenario: An interested user becomes an active user Given an interested user with email "[email protected]" When he goes to homepage And he fills all personal fields Then he confirms the registration And he should be registered as an unconfirmed user And he should receive the registration email And he should be in the reserved area
Signup Feature Contextclass SignupContext implements SnippetAcceptingContext, MinkAwareContext { private $mink; private $minkParameters; private $entityManager;
...
public function setMink(Mink $mink) { $this‐>mink = $mink; }
public function setMinkParameters(array $parameters) { $this‐>minkParameters = $parameters; } ...
GIVEN i am an interested user with email/** * @Given I am an interested user with email :arg1 */public function anInterestedUserWithEmail($email){ // shared in the feature context $this‐>email = $email;
// Check that is an interested user and not an existing one Assert::assertNull( $this‐>entityManager ‐>getRepository("CorleyBaseBundle:User")‐>findOneByEmail($email) );}
WHEN i fill all personal fields/** * @When I fill all personal fields */public function heFillsAllPersonalFields(){ $this‐>mink‐>getSession()‐>getPage() ‐>find("css", "#corley_bundle_basebundle_user_email") ‐>setValue($this‐>email);
$this‐>mink‐>getSession()‐>getPage() ‐>find("css", "#corley_bundle_basebundle_user_name") ‐>setValue(rand(0,100000));}
THEN I confirm my registration/** * @Then I confirm my registration */public function iConfirmTheRegistration(){ $client = $this‐>mink ‐>getSession() ‐>getDriver() ‐>getClient(); $client‐>followRedirects(false);
$this‐>mink‐>getSession()‐>getPage() ‐>find("css", "#corley_bundle_basebundle_user_submit")‐>click();}
THEN I should be registered as anunconfirmed user
/** * @Then I should be registered as an unconfirmed user */public function heShouldBeRegisteredAsAnUnconfirmedUser(){ $this‐>entityManager‐>clear();
$entity = $this‐>entityManager ‐>getRepository("CorleyBaseBundle:User") ‐>findOneByEmail($this‐>email);
Assert::assertNotNull($entity); Assert::assertFalse($entity‐>isConfirmed());}
THEN I should receive the reg. email/** * @Then I should receive the registration email */public function heShouldReceiveTheRegistrationEmail(){ $driver = $this‐>mink‐>getSession()‐>getDriver(); if (!$driver instanceof KernelDriver) { throw new \RuntimeException("Only kernel drivers"); }
$profile = $driver‐>getClient()‐>getProfile(); if (false === $profile) { throw new \RuntimeException("Profiler is disabled"); }
$collector = $profile‐>getCollector('swiftmailer'); Assert::assertCount(1, $collector‐>getMessages());}
You can wrap a "getProfiler" in a separate method
THEN I should be in the reserved area/** * @Then I should be in the reserved area */public function heShouldBeInTheReservedArea(){ $client = $this‐>mink ‐>getSession() ‐>getDriver() ‐>getClient(); $client‐>followRedirects(true); $client‐>followRedirect();
$this‐>mink‐>assertSession()‐>pageTextContains("Hello reserved area!");}
You can wrap the redirection in seperate, reusable steps
There are so many features in BehatBut, remember that the goal is to bridging the communication gap
between actors of the same project with different backgroundsDo not use it for functional testing
(use LiipFunctionalTestBundle for that or anything else)
Thanks for listening
Any question?