BDD in Laravel: Getting Started with Behat and PhpSpec

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.

--ADVERTISEMENT--

We’ll also assume that us saying “Run the command” implies the command should be run in the terminal of the operating system.


Prerequisites

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.

Laravel Greeting Screen

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:

Features bootstrap folder

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:

Autocomplete dialog for assertions in imported PHPUnit class

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.