This post was originally published in the community tutorials section of Semaphore CI, the hosted continuous integration and deployment service.
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
- Composer (preferably installed globally)
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
sudo ln -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 the behat
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 the MinkContext
class which contains them.
Thus, we alter the source code of FeatureContext.php
from:
class FeatureContext implements Context, SnippetAcceptingContext
to
class FeatureContext 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
and When 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 test the home page for a phrase
Scenario: Root Test # features/hometest.feature:5
When I am on the homepage # FeatureContext::iAmOnHomepage()
Then I should see "Laravel 5" # FeatureContext::assertPageContainsText()
1 scenario (1 passed)
2 steps (2 passed)
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:
use PHPUnit_Framework_Assert as PHPUnit;
You will then have access to assertions, like so:
See a full list of available assertions here.
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:
There is no real difference between SpecBDD and TDD. The value of using an xSpec tool instead of a regular xUnit tool for TDD is the language.
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 whole vendor/bin
folder to your path – which ever way you prefer.
sudo ln -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
namespace spec\bddsetup;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class CalculatorSpec extends ObjectBehavior
{
function it_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\Calculator does not exist.
100% 1
1 specs
1 example (1 broken)
105ms
Do you want me to create `bddsetup\Calculator` for you?
[Y/n]
Y
Class bddsetup\Calculator created in /home/vagrant/Code/bdd-setup/app/Calculator.php.
100% 1
1 specs
1 example (1 passed)
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
*/
public function theMethodReceivesTheNumbersAnd($arg1, $arg2, $arg3)
{
throw new PendingException();
}
/**
* @Then the calculated value should be :arg1
*/
public function 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
*/
public function theMethodReceivesTheNumbersAnd($arg1, $arg2, $arg3)
{
$this->calculator = new Calculator();
$this->calculator->$arg1($arg2, $arg3);
}
/**
* @Then the calculated value should be :arg1
*/
public function 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
:
function it_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::sum not found.
50% 50% 2
1 specs
2 examples (1 passed, 1 broken)
153ms
Do you want me to create `bddsetup\Calculator::sum()` for you?
[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
2 examples (1 passed, 1 broken)
136ms
Do you want me to create `bddsetup\Calculator::result()` for you?
[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
2 examples (1 passed, 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
namespace bddsetup;
class Calculator
{
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.
Bruno is a blockchain developer and technical educator at the Web3 Foundation, the foundation that's building the next generation of the free people's internet. He runs two newsletters you should subscribe to if you're interested in Web3.0: Dot Leap covers ecosystem and tech development of Web3, and NFT Review covers the evolution of the non-fungible token (digital collectibles) ecosystem inside this emerging new web. His current passion project is RMRK.app, the most advanced NFT system in the world, which allows NFTs to own other NFTs, NFTs to react to emotion, NFTs to be governed democratically, and NFTs to be multiple things at once.