BDD (Behavior Driven Development) is a complicated subject for
many developers, and getting started with it the right way often does not come easy – especially
when needing to implement it into existing frameworks. This tutorial aims to
help you get a BDD-powered Laravel project up and running in very little time,
introducing you to the basic concepts and workflow you’ll need to proceed on
your own. We’ll be installing and using
Behat and PhpSpec.
In the tutorial, we assume you’re working on a Unix system and
have basic theoretical knowledge of what BDD is about, but little or no
practical experience.
We’ll also assume that us saying “Run the command” implies the
command should be run in the terminal of the operating system.
Prerequisites
·
Unix OS (e.g. Ubuntu)
·
PHP 5.6+
·
Git
Optionally, if you intend to build what we set up here into a
proper application, add in:
·
a database (MySQL)
·
Caching layers (Redis, Memcached…)
Creating a New Laravel App
To create a new Laravel application, we run the following
command:
composer create-project laravel/laravel bdd-setup
The sample application is now created, and
should greet you with “Laravel 5” if you visit the
root of the app.
Setting up Behat
Several packages are required in order to make
Behat play well with Laravel. Let’s install them all into our application’s
development environment (with the
--dev
flag) and explain
each.composer require behat
/behat behat
/mink behat
/mink
-extension laracasts
/behat
-laravel
-extension
--dev
sudoln
-
s
/home
/vagrant
/Code
/bdd
-setup
/vendor
/bin
/behat
/usr
/local
/bin
/behat
behat/behat is the main package for Behat. The behat/mink package is used to emulate a browser, so we can have the
test suite check our URLs and their output. behat/mink-extension is the glue for Mink and Behat, and the
last package, behat-laravel-extension is Jeffrey Way’s
own implementation of Behat bindings, specifically made for Laravel.
The last sudo ln -s line is optional, and adds Behat’s executable to a
location in the $PATH, so thebehat command can be
executed without the vendor/bin prefix from our project’s root folder.
In other words, behat
--init instead of vendor/bin/behat --init.
Finally, we initialize
a Behat project:
behat
--init
This has created a new folder called
features
in our project’s
directory:Context and Configuration
Behat uses definitions from the auto-generated
FeatureContext
class to understand what we’re testing for – phrasing like
“Given that I’m on the URL this and that…”.
To get some typical browser-related definitions, we make sure
the
FeatureContext
class extends theMinkContext
class which contains them.
Thus, we alter the source code of
FeatureContext.php
from:classFeatureContext
implements
Context,
SnippetAcceptingContext
to
classFeatureContext
extends
Behat\MinkExtension\Context\MinkContext
implements
Context,
SnippetAcceptingContext
With this change, we
made FeatureContext inherit the definitions within MinkContext.
The behat -dl command is used to list out all defined definitions. It
will now output something like the following:
default
|Given
/^(?:|I
)am on
(?:|the
)homepage$
/
default
|When
/^(?:|I
)go to
(?:|the
)homepage$
/
default
|Given
/^(?:|I
)am on
"(?P<page>[^"]+)"$/
default | When /^(?:|I )go to "(?P
<page
>[^"]+)"$
/
default
|When
/^(?:|I
)reload the page$
/
Next, we need to set up the Laravel specific
package. As per instructions, this is done by adding a
behat.yml
file to the project
root:default:
extensions:
Laracasts\Behat:
# env_path: .env.behat
Behat\MinkExtension:
default_session: laravel
base_url: http://localhost:8888
laravel: ~
The
.env.behat
file referenced above
contains environment variables specific to the Behat testing session. This file
does not exist by default, so we can create it by copying the already included.env.example
one:cp.env.
example
.env.behat
Note: Due to varying
installation procedures between different Laravel versions, you might have to add a custom made key into
APP_KEY in both
config/app.php
and .env
,
as well as .env.behat
.
Keeping it at under 32 characters (default is “Some random string”) will throw
errors.Writing Features
Features are what we test for with Behat. We write them out as
human readable stories, and expect the test suite to not only understand them,
but also to make sure they work.
One such feature can be checking for whether or not we see
“Laravel 5” when we visit the home page. To write this feature, we create a
hometest.feature
file in the features
folder and give it the following contents:Feature:
In order to prove that Behat works as intended
We want to test the home page for a phrase
Every feature begins with such a description.
This is for humans only – the test suite is not intended to understand this.
Then follow the Scenarios – the specific, computer-readable
steps the suite should follow.
Feature:
In order to prove that Behat works as intended
We want to test the home page for a phrase
Scenario: Root Test
When I am on the homepage
Then I should see "Laravel 5"
Every scenario starts
with the word “Scenario”, indented to the level of the Feature’s description. Every
scenario should also have a name.
Immediately beneath it
and another indentation level in, the scenario will have specific instructions
for Behat to follow. These instructions are parsed from definitions we defined
in the FeatureContext class. In our case, we defined them by
extending MinkContext.
When I am on the homepage is a specific definition in MinkContext which states:
/**
* Opens homepage.
*
* @Given /^(?:|I )am on (?:|the )homepage$/
* @When /^(?:|I )go to (?:|the )homepage$/
*/
public
function
iAmOnHomepage()
{
$this->visitPath('/');
}
In other words, two
phrases will trigger this: Given I am on the homepage andWhen
I am on the homepage. The function will
simulate a visit to the root URL: /.
The next
definition, Then
I should see "Laravel 5" calls on:
/**
*
Checks
,that page contains specified text
.
*
*
@Then
/^(?:|I
)should see "
(?P
<text>(?:[^"]|\\")*)"$
/
*/
public
function
assertPageContainsText($text)
{
$this->assertSession()->pageTextContains($this->fixStepArgument($text));
}
The function grabs all
the text from the rendered page, and checks if our string is a substring of it.
Before testing for
this, however, we need to boot up a local PHP server, just so Mink can actually
access the URLs we ask it to access.
php
-S localhost
:8888-
t public
The above command
launches a server (-S), on the url localhost, listening on the port 8888 in the target
directory public.
Finally, we can test
the feature:
> behat
Feature
:
In order to prove that Behat works as intended
We want to
testthe home page
fora phrase
Scenario
:Root Test
# features/hometest.feature:5
When I am on the homepage
# FeatureContext::iAmOnHomepage()
Then I should see
"Laravel 5"# FeatureContext::assertPageContainsText()
1scenario
(1passed
)
2steps
(2passed
)
0m0
.64s
(22.13Mb
)
The basics of Behat are now in place. We’ll work on some
in-depth integrations in a future post.
Note: By using the
behat-laravel-extension
package, we made sure all Laravel functionality is instantly
available in the FeatureContext
. Getting to the
main $app
object is now as simple as app()
, getting a
configuration variable is just a config("somevar")
away. These bindings are all automatically available and ready to be
used.Using PHPUnit’s Assertions
Behat doesn’t have assertions per-se. As such, you may want to
use PHPUnit’s. Seeing as PHPUnit comes bundled with new Laravel apps, it’s
already available, and all one needs to do to access the assertions is import
the class in the
FeatureContext
class, like so:useYou will then have access to assertions, like so:PHPUnit_Framework_Assert
as
PHPUnit
;
PhpSpec
PhpSpec is more and more a common replacement for PHPUnit in
people’s arsenals. Laravel does come with PHPUnit, but that doesn’t mean
there’s no room for replacing or supplementing it with PhpSpec.
The most noticeable difference between PhpSpec and PHPUnit is
the syntax – PhpSpec is much more readable and human friendly, thus fitting in
nicely with the whole concept of BDD and Behat. The tests don’t have to begin
with the word
test
and the methods are all phrased as sentences, as actions we
intend to do or properties we want objects to have. Even the docs say so:
In addition, PhpSpec
helps with scaffolding of tests and classes, and with mocking. We’ll see how in
another, more in-depth tutorial, but for now let’s install and set it up, then
go through some basics.
Let’s install PhpSpec:
composer require phpspec
/phpspec
--dev
Again, we can add the installed executable to
our path, so it’s runnable without the
vendor/bin
prefix. Either execute
the command below to do so (modify the paths to match yours), or just add the
wholevendor/bin
folder to your path –
which ever way you prefer.sudoln
-
s
/home
/vagrant
/Code
/bdd
-setup
/vendor
/bin
/phpspec
/usr
/local
/bin
/phpspec
PhpSpec is more or less ready to roll out of
the box, we just need one more minor edit. In
phpspec.yml
in
the root of our project folder, under all the lines in there, we add:spec_path: tests
This tells PhpSpec where to put our spec files.
Feel free to change this as you wish.
Writing Specs
Specs are classes containing tests, much like test classes in
PHPUnit. To create a new spec for a class, we use the
desc
command (for describe). Let’s imagine we’re making a calculator
class we intend to build into Laravel as a service. In version 1, a calculator
should at the very least be able to sum two numbers. Let’s build this version
1.phpspec desc bddsetup\\Calculator
Note that bddsetup is this tutorial’s demo namespace, and you should change
it to yours if you picked a different one.
This has created a
specification file in tests/spec/CalculatorSpec.php, containing:
<?php
namespacespec\bddsetup;
usePhpSpec\ObjectBehavior;
useProphecy\Argument;
classCalculatorSpec
extends
ObjectBehavior
{
functionit_is_initializable()
{
$this->shouldHaveType('bddsetup\Calculator');
}
}
Note: The $this keyword refers
to the instance of the class being tested (Calculator),
and not the test class itself!
If we run phpspec now, it will ask us for permission to create the
missing Calculator class for us. Let’s allow it.
bddsetup
/Calculator
10-
it is initializable
class
bddsetup\Calculatordoes not exist
.
100%1
1 specs
1example
(1broken
)
105ms
Do you want me to create `bddsetup\Calculator`
foryou
?
[Y
/n
]
Y
Class
bddsetup\Calculatorcreated
in/
home
/vagrant
/Code
/bdd
-setup
/app
/Calculator
.php
.
100%
1
1 specs
1example
(1passed
)
135ms
This automatically
passes the test because the it_is_initializable test succeeds – the class exists now, after all.
Let’s use Behat and
PhpSpec in tandem to create a sum method, now.
The Duet
In true BDD fashion, we envision a feature first, write it out,
and then test for its existence. Let’s create a new feature file at
features/calc.feature
:Feature:
In order to make sure the calculator works
As a developer
I need to get the correct output from its functions
Scenario: Summing
Given the method "sum" receives the numbers 4 and 7
Then the calculated value should be 11
The two definitions in the Summing scenario do
not exist. We need to add them into the
FeatureContext
so that Behat can
understand them. An easy way to generate empty snippets for us to fill out is
by using the --append-snippets
command.behat
--append
-snippets
The
FeatureContext
class should now have
two additional methods:/**
* @Given the method :arg1 receives the numbers :arg2 and :arg3
*/
publicfunction
theMethodReceivesTheNumbersAnd($arg1,
$arg2,
$arg3)
{
throw
new
PendingException();
}
/**
* @Then the calculated value should be :arg1
*/
publicfunction
theCalculatedValueShouldBe($arg1)
{
throw
new
PendingException();
}
Behat automatically extracted the arguments it
recognized. This means the methods (and by extension the definitions) are
flexible – we can alter the parameters as we see fit. Let’s fill those stubs
out now.
/**
* @Given the method :arg1 receives the numbers :arg2 and :arg3
*/
publicfunction
theMethodReceivesTheNumbersAnd($arg1,
$arg2,
$arg3)
{
$this->calculator
=
new
Calculator();
$this->calculator->$arg1($arg2,
$arg3);
}
/**
* @Then the calculated value should be :arg1
*/
publicfunction
theCalculatedValueShouldBe($arg1)
{
PHPUnit::assertEquals($arg1,
$this->calculator->result());
}
You can see here we’re
using the PHPUnit assertions from before, despite having both PhpSpec and Behat
at our disposal.
If we run Behat now,
we should get:
[Symfony\Component\Debug\Exception\FatalErrorException
]
Call to undefined method bddsetup\Calculator
::sum()
That’s normal. After all, we didn’t implement
it. Let’s have PhpSpec help us out with that. Add a new method into the
CalculatorSpec
:functionit_should_sum()
{
$this->sum(4,
7);
$this->result()->shouldBe(11);
}
When we run it, PhpSpec will ask for permission
to stub out the
sum
and result
methods:> phpspec run
bddsetup
/Calculator
15-
it should
sum
method bddsetup\Calculator
::sumnot found
.
50%50%
2
1 specs
2examples
(1passed
,1
broken
)
153ms
Do you want me to create `bddsetup\Calculator
::sum()`
foryou
?
Y
/n
]
Y
Method bddsetup\Calculator
::sum()has been created
.
bddsetup
/Calculator
15-
it should
sum
method bddsetup\Calculator
::result not found
.
50%50%
2
1 specs
2examples
(1passed
,1
broken
)
136ms
Do you want me to create `bddsetup\Calculator
::result
()`
foryou
?
[Y
/n
]
Y
Method bddsetup\Calculator
::result
()has been created
.
bddsetup
/Calculator
15-
it should
sum
expected
[integer
:11],but got null
.
50%50%
2
1 specs
2examples
(1passed
,1
failed
)
144ms
At the same time, the run fails because the
methods don’t do what they’re expected to do. This is perfectly fine. Let’s
edit the
Calculator
class and implement
them completely now.<?php
namespacebddsetup;
classCalculator
{
protected
$result
=
0;
public
function
sum($argument1,
$argument2)
{
$this->result
=
(
int
)$argument1+
(
int
)$argument2;
}
public
function
result()
{
return
$this->result;
}
}
If we now run Behat
with behat and PhpSpec with phpspec run, we should get all green results – all tests should pass.
It is now much easier
to imagine extending the class quickly and effectively:
·
omitting the second
argument could add the one that was passed in to the result from a previous
operation
·
the sum method could
return the Calculator instance to enable chaining, playing nicely with the
point above
·
etc…
Conclusion
With powerful BDD tools such as Behat and PhpSpec in place,
writing out stories and testing your classes for future upgrades becomes a
breeze rather than a tedious night of writing mocks.
This tutorial showed you how to get started with BDD tools in a
fresh Laravel application. What was shown in this post is just enough to whet
your appetite. Future posts will go into more detail and some use-case specific
implementations.
Sign up here with your email
ConversionConversion EmoticonEmoticon