Welcome to this series about developing Laravel applications using a behavior-driven development (BDD) approach. Full stack BDD can seem complicated and intimidating. There are just as many ways of doing it as there are developers.
In this series, I will walk you through my approach of using Behat and PhpSpec to design a Laravel application from scratch.
There are many resources on BDD in general, but Laravel specific material is hard to find. Therefore, in this series, we will focus more on the Laravel related aspects and less on the general stuff that you can read about many other places.
Describing Behavior
When describing behavior, which is also known as writing stories and specs, we will be using an outside-in approach. This means that every time we build a new feature, we will start by writing the overall user story. This is normally from the clients or stakeholders perspective.
What are we expecting to happen when we do this?
We are not allowed to write any code until we have a failing, red step for example, unless we are refactoring existing code.
Sometimes, it will be necessary to iteratively solve a small part of a story or feature (two words that I use interchangeably) on which we are working. This will often mean writing specs with PhpSpec. Sometimes it will take many iterations on a integration or unit level before the whole story (on an acceptance level) is passing. This all sounds very complicated but it's really not. I am a big believer of learning by doing, so I think that everything will make more sense once we start writing some actual code.
We will be writing stories and specs on four different levels:
1. Acceptance
Most of the time, our functional suite will serve as our acceptance layer. The way we will be describing our features in our functional suite will be very similar to how we would write acceptance stories (using an automated browser framework) and would as such create a lot of duplication.
As long as the stories describe the behavior from the client's point of view, they serve as acceptance stories. We will use the Symfony DomCrawler to test the output of our application. Later in the series, we might find that we need to test through an actual browser that can run JavaScript as well. Testing through the browser adds some new concerns, since we need to make sure that we load hour test environment when the suite is run.
2. Functional
In our functional suite, we will have access to the Laravel application, which is very convenient. First of all, it makes it easy to differentiate between environments. Second of all, not going through a browser makes our test suite a lot faster. Whenever we want to implement a new feature, we will write a story in our functional suite using Behat.
3. Integration
Our integration suite will test the behavior of the core part of our application that do not neccessarily need to have access to Laravel. The integration suite will normally be a mixture of Behat stories and PhpSpec specs.
4. Unit
Our unit tests will be written in PhpSpec and will test isolated small units of the application core. Our entities, value objects etc. will all have specs.
The Case
Throughout this series, starting from the next article, we will be building a system for tracking time. We will start by describing the behavior from the outside by writing Behat features. The internal behavior of our application will be described using PhpSpec.
Together these two tools will help us feel comfortable with the quality of the application we are building. We will primarily write features and specs on three levels:
- Functional
- Integration
- Unit
In our functional suite, we will crawl the HTTP responses of our application in a headless mode, meaning that we will not go through the browser. This will make it easier to interact with Laravel and make our functional suite serve as our acceptance layer, as well.
Later on, if we end up having a more complicated UI and might need to test some JavaScript as well, we might add a dedicated acceptance suite. This series is still work-in-progress, so feel free to drop your suggestions in the comments section.
Our Setup
Note that for this tutorial, I assume you have a fresh install of Laravel (4.2) up and running. Preferably you are using Laravel Homestead as well, which is what I used when I wrote this code.
Before we get started with any real work, let's make sure we have Behat and PhpSpec up and running. First though, I like to do a little bit of cleaning whenever I start a new laravel project and delete the stuff I do not need:
git rm -r app/tests/ phpunit.xml CONTRIBUTING.md
If you delete these files, make sure to update your composer.json
file accordingly:
"autoload": { "classmap": [ "app/commands", "app/controllers", "app/models", "app/database/migrations", "app/database/seeds" ] },
And, of course:
$ composer dump-autoload
Now we are ready to pull in the BDD tools we need. Just add a require-dev
section to your composer.json
:
"require": { "laravel/framework": "4.2.*" }, "require-dev": { "behat/behat": "~3.0", "phpspec/phpspec": "~2.0", "phpunit/phpunit": "~4.1" },
"Why are we pulling in PHPUnit?" you might be thinking? We are not going to write good ol' PHPUnit test cases in this series, but the assertions are a handy tool together with Behat. We will see that later in this article when we write our first feature.
Remember to update you dependencies after modifying composer.json
:
$ composer update --dev
We are almost done installing and setting up stuff. PhpSpec works out of the box:
$ vendor/bin/phpspec run 0 specs 0 examples 0ms
But Behat needs to do a quick run with the --init
option in order to set everything up:
$ vendor/bin/behat --init +d features - place your *.feature files here +d features/bootstrap - place your context classes here +f features/bootstrap/FeatureContext.php - place your definitions, transformations and hooks here $ vendor/bin/behat No scenarios No steps 0m0.14s (12.18Mb)
The first command created a shiny new FeatureContext
class, where we can write the step definitions needed for our features:
<?php use Behat\Behat\Context\SnippetAcceptingContext; use Behat\Gherkin\Node\PyStringNode; use Behat\Gherkin\Node\TableNode; /** * Behat context class. */ class FeatureContext implements SnippetAcceptingContext { /** * 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() { } }
Writing Our First Feature
Our first feature will be very simple: We will simply make sure that our new Laravel install greets us with a "You have arrived." on the homepage. I put in a rather silly Given
step as well, Given I am logged in
, which only serves to show how easy interacting with Laravel in our features is.
Technically, I would categorize this type of feature as a functional test, since it interacts with the framework, but it also serves as an acceptance test, since we would not see any different results from running a similar test through a browser testing tool. For now we will stick with our functional test suite.
Go ahead and create a welcome.feature
file and put it in features/functional
:
# features/functional/welcome.feature Feature: Welcoming developer As a Laravel developer In order to proberly begin a new project I need to be greeted upon arrival Scenario: Greeting developer on homepage Given I am logged in When I visit "/" Then I should see "You have arrived."
By putting the functional features in a functional
directory, it will be easier for us to manage our suites later on. We do not want integration type features that does not require Laravel to have to wait for the slow functional suite.
I like to keep things nice and clean, so I believe we should have a dedicated feature context for our functional suite that can give us access to Laravel. You can just go ahead and copy the existing FeatureContext
file and change the class name to LaravelFeatureContext
. For this to work, we also need a behat.yml
configuration file.
Create one in the root directory of you project and add the following:
default: suites: functional: paths: [ %paths.base%/features/functional ] contexts: [ LaravelFeatureContext ]
I think the YAML here is pretty self explanatory. Our functional suite will look for features in the functional
directory and run them through the LaravelFeatureContext
.
If we try to run Behat at this point, it will tell us to implement the necessary step definitions. We can have Behat add the empty scaffold methods to the LaravelFeatureContext
with the following command:
$ vendor/bin/behat --dry-run --append-snippets $ vendor/bin/behat Feature: Welcoming developer As a Laravel developer In order to proberly begin a new project I need to be greeted upon arival Scenario: Greeting developer on homepage # features/functional/welcome.feature:6 Given I am logged in # LaravelFeatureContext::iAmLoggedIn() TODO: write pending definition When I visit "/" # LaravelFeatureContext::iVisit() Then I should see "You have arrived." # LaravelFeatureContext::iShouldSee() 1 scenario (1 pending) 3 steps (1 pending, 2 skipped) 0m0.28s (12.53Mb)
And now, as you can see from the output, we are ready to start implement the first of our steps: Given I am logged in
.
The PHPUnit test case that ships with Laravel allows us to do stuff like $this->be($user)
, which logs in a given user. Ultimately, we want to be able to interact with Laravel as if we were using PHPUnit, so let's go ahead and write the step definition code "we wish we had":
/** * @Given I am logged in */ public function iAmLoggedIn() { $user = new User; $this->be($user); }
This will not work of course, since Behat have no idea about Laravel specific stuff, but I will show you in just a second how easy it is to get Behat and Laravel to play nicely together.
If you take a look in the Laravel source and find the Illuminate\Foundation\Testing\TestCase
class, which is the class that the default test case extends from, you will see that starting from Laravel 4.2, everything has been moved to a trait. The ApplicationTrait
is now responsible for booting an Application
instance, setting up an HTTP client and give us a few helper methods, such as be()
.
This is pretty cool, mainly because it means that we can just pull it in to our Behat contexts with almost no setup required. We also have access to the AssertionsTrait
, but this is still tied to PHPUnit.
When we pull in the trait, we need to do two things. We need to have a setUp()
method, like the one in theIlluminate\Foundation\Testing\TestCase
class, and we need a createApplication()
method, like the one in the default Laravel test case. Actually we can just copy those two methods and use them directly.
There is only one thing to notice: In PHPUnit, the method setUp()
will automatically be called before each test. In order to achieve the same in Behat, we can use the @BeforeScenario
annotation.
Add the following to your LaravelFeatureContext
:
use Illuminate\Foundation\Testing\ApplicationTrait; /** * Behat context class. */ class LaravelFeatureContext implements SnippetAcceptingContext { /** * Responsible for providing a Laravel app instance. */ use ApplicationTrait; /** * 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() { } /** * @BeforeScenario */ public function setUp() { if ( ! $this->app) { $this->refreshApplication(); } } /** * Creates the application. * * @return \Symfony\Component\HttpKernel\HttpKernelInterface */ public function createApplication() { $unitTesting = true; $testEnvironment = 'testing'; return require __DIR__.'/../../bootstrap/start.php'; }
Pretty easy, and look what we get when we run Behat:
$ vendor/bin/behat Feature: Welcoming developer As a Laravel developer In order to proberly begin a new project I need to be greeted upon arival Scenario: Greeting developer on homepage # features/functional/welcome.feature:6 Given I am logged in # LaravelFeatureContext::iAmLoggedIn() When I visit "/" # LaravelFeatureContext::iVisit() TODO: write pending definition Then I should see "You have arrived." # LaravelFeatureContext::iShouldSee() 1 scenario (1 pending) 3 steps (1 passed, 1 pending, 1 skipped) 0m0.73s (17.92Mb)
A green first step, which means that our setup is working!
Next up, we can implement the When I visit
step. This one is super easy, and we can simply use the call()
method that the ApplicationTrait
provides. One line of code will get us there:
/** * @When I visit :uri */ public function iVisit($uri) { $this->call('GET', $uri); }
The last step, Then I should see
, takes a little more and we need to pull in two dependencies. We will need PHPUnit for the assertion and we will need the Symfony DomCrawler to search for the "You have arrived." text.
We can implement it like this:
use PHPUnit_Framework_Assert as PHPUnit; use Symfony\Component\DomCrawler\Crawler; ... /** * @Then I should see :text */ public function iShouldSee($text) { $crawler = new Crawler($this->client->getResponse()->getContent()); PHPUnit::assertCount(1, $crawler->filterXpath("//text()[. = '{$text}']")); }
This is pretty much the same code as you would write if you were using PHPUnit. The filterXpath()
part is a little confusing and we will not worry about it now, since it is a little out of the scope of this article. Just trust me that it works.
Running Behat one final time is good news:
$ vendor/bin/behat Feature: Welcoming developer As a Laravel developer In order to proberly begin a new project I need to be greeted upon arival Scenario: Greeting developer on homepage # features/functional/welcome.feature:6 Given I am logged in # LaravelFeatureContext::iAmLoggedIn() When I visit "/" # LaravelFeatureContext::iVisit() Then I should see "You have arrived." # LaravelFeatureContext::iShouldSee() 1 scenario (1 passed) 3 steps (3 passed) 0m0.82s (19.46Mb)
The feature is working as expected and the developer is greeted upon arrival.
Conclusion
The complete LaravelFeatureContext
should now look similar to this:
<?php use Behat\Behat\Context\SnippetAcceptingContext; use Behat\Gherkin\Node\PyStringNode; use Behat\Gherkin\Node\TableNode; use PHPUnit_Framework_Assert as PHPUnit; use Symfony\Component\DomCrawler\Crawler; use Illuminate\Foundation\Testing\ApplicationTrait; /** * Behat context class. */ class LaravelFeatureContext implements SnippetAcceptingContext { /** * Responsible for providing a Laravel app instance. */ use ApplicationTrait; /** * 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() { } /** * @BeforeScenario */ public function setUp() { if ( ! $this->app) { $this->refreshApplication(); } } /** * Creates the application. * * @return \Symfony\Component\HttpKernel\HttpKernelInterface */ public function createApplication() { $unitTesting = true; $testEnvironment = 'testing'; return require __DIR__.'/../../bootstrap/start.php'; } /** * @Given I am logged in */ public function iAmLoggedIn() { $user = new User; $this->be($user); } /** * @When I visit :uri */ public function iVisit($uri) { $this->call('GET', $uri); } /** * @Then I should see :text */ public function iShouldSee($text) { $crawler = new Crawler($this->client->getResponse()->getContent()); PHPUnit::assertCount(1, $crawler->filterXpath("//text()[. = '{$text}']")); } }
We now have a really nice foundation to build upon as we continue developing our new Laravel application using BDD. I hope I have proven to you how easy it is to get Laravel and Behat to play nicely together.
We have touched on a lot of different topics in this first article. No need to worry, we will take a more in-depth look at everything as the series continues. If you have any questions or suggestions, please leave a comment.
Comments