Advertisement

A BDD Workflow With Behat and Phpspec

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

In this tutorial, we will have a look at two different BDD tools, Behat and phpspec, and see how they can support you in your development process. Learning BDD can be confusing. New methodology, new tools and many questions, such as "what to test?" and "which tools to use?". I hope that this rather simple example will give you ideas to how you can incorporate BDD into your own workflow.

My Inspiration

I got inspired to write this tutorial by Taylor Otwell, creator of the Laravel framework. Several times, I have heard Taylor explain why he mostly does not do TDD/BDD by saying that he likes to first plan out the API of his code, before actually starting to implement it. I have heard this from many developers, and every time I am thinking to myself: "But that is the perfect use case for TDD/BDD!". Taylor says that he likes to map out the API of his code, by writing the code he wish he had. He will then start coding and not be satisfied until he has achieved that exact API. The argument makes sense if you are only testing/speccing on the unit level, but using a tool like Behat, you begin with the external behavior of your software, which is basically, as far as I understand, what Taylor wants to accomplish.

What We'll Be Covering

In this tutorial, we will build a simple configuration file loader class. We will start by using Taylor's approach and then shift to a BDD approach instead. The examples are minimalistic, but still we will have to worry about fixtures, static methods etc., so all-in-all, I think they should be enough to show how Behat and phpspec can complement each other.

Disclaimer: First of all, this article is not a getting started guide. It assumes basic knowledge of BDD, Behat and phpspec. You have probably already looked into these tools, but are still struggling with how to actually use them in your daily workflow. If you want to brush up on phpspec, take a look at my getting started tutorial. Second of all, I am using Taylor Otwell as an example. I do not know anything about how Taylor works, besides what I heard him say in podcasts etc. I use him as an example because he is an awesome developer (he made Laravel!) and because he is well-known. I might as well have used someone else, since most developers, including myself, do not do BDD all the time, yet. Also, I am not saying that the workflow Taylor describes is bad. I think it is a brilliant idea to put some thought into your code before actually writing it. This tutorial is just meant to show the BDD way of doing this.

Taylor's Workflow

Let us begin by taking a look at how Taylor might go about designing this configuration file loader. Taylor says that he likes to just fire up a blank text file in his editor and then write how he would like developers to be able to interact with his code (the API). In a BDD context, this is normally referred to as testing the external behavior of software and tools like Behat are great for this. We will see this in a short while.

First, maybe Taylor will make a decision about the configuration files. How should they work? As in Laravel, let us just use simple PHP arrays. An example configuration file could look like this:

# config.php

<?php

return array(

    'timezone' => 'UTC',

);

Next, how should the code that makes use of this configuration file work? Let us do this the Taylor way and just write the code we wish we had:

$config = Config::load('config.php');

$config->get('timezone'); // returns 'UTC'

$config->get('timezone', 'CET'); // returns 'CET' if 'timezone' is not configured

$config->set('timezone', 'GMT');
$config->get('timezone'); // returns 'GMT'

Okay, so this looks pretty good. First we have a static call to a load() function, followed by three use cases: 

  1. Getting the "timezone" from the configuration file. 
  2. Getting a default value, if the "timezone" is not yet configured. 
  3. Changing a configuration option by setting it to something else. We will describe each of these use cases, or scenarios, with Behat in a short while.

It makes sense to use Behat for these kinds of things. Behat will not force us to make any design decisions - we have phpspec for that. We will simply move the requirements, that we just described, into a Behat feature in order to make sure that we get it right, when we start building. Our behat feature will serve as an acceptance test for our requirements so to speak.

If you look at the code we wrote, another reason to use Behat, instead of only phpspec, is the static call. It is not easy to test static methods, especially not if you are only using a tool like phpspec. We will see how we can go about this when we have both Behat and phpspec available.

Setup

Assuming you are using Composer, setting up Behat and phspec is a super simple two step process.

First, you need a basic composer.json file. This one includes Behat and phpspec, and uses psr-4 to autoload the classes:

{
    "require-dev": {
        "behat/behat": "~3.0",
        "phpspec/phpspec": "~2.0"
    },
    "autoload": {
        "psr-4": {
            "": "src/"
        }
    }
}

Run composer install to fetch the dependencies.

phpspec does not need any configuration in order to run, whereas Behat needs you to run the following command to generate a basic scaffold:

$ vendor/bin/behat --init

That is all it takes. This can not be your excuse for not doing BDD!

Planning The Features With Behat

So, now that we are all set up, we are ready to start doing BDD. Because doing BDD means installing and using Behat and phpspec, right?

As far as I am concerned, we already started doing BDD. We have effectively described the external behavior of our software. Our "clients" in this example are developers, who are going to interact with our code. By "effectively", I mean that we have described the behavior in a way they will understand. We could take the code we already outlined, put it in a README file, and every decent PHP developer would understand the use of it. So this is pretty good actually, but I have two important things to note on this. First of all, describing the behavior of software using code only works in this example because the "clients" are programmers. Normally, we are testing something that is going to be used by "normal" people. A human language is better than PHP when we want to communicate with humans. Second of all, why not automate this? I am not going to argue why this might be a good idea.

That being said, I think starting to use Behat now would be a reasonable decision.

Using Behat, we wish to describe each of the scenarios that we outlined above. We do not want to extensively cover every edge case involved in using the software. We have phpspec available if this should be needed to fix bugs along the way etc. I think many developers, maybe including Taylor, feels like they have to think everything through and decide everything before they can write tests and specs. That is why they choose to start out without BDD, because they do not want to decide everything beforehand. This is not the case with Behat, since we are describing the external behavior and usage. In order to use Behat to describe a feature, we do not need to decide anything more than in the above example with using a raw text file. We just need to define the requirements of the feature - in this case the external API of the configuration file loader class.

Now, let us take the above PHP code and turn it into a Behat feature, using the English language (actually using the Gherkin language).

Make a file in the features/ directory, called config.feature, and fill in the following scenarios:

Feature: Configuration files
    In order to configure my application
    As a developer
    I need to be able to store configuration options in a file

    Scenario: Getting a configured option
        Given there is a configuration file
        And the option 'timezone' is configured to 'UTC'
        When I load the configuration file
        Then I should get 'UTC' as 'timezone' option

    Scenario: Getting a non-configured option with a default value
        Given there is a configuration file
        And the option 'timezone' is not yet configured
        When I load the configuration file
        Then I should get default value 'CET' as 'timezone' option

    Scenario: Setting a configuration option
        Given there is a configuration file
        And the option 'timezone' is configured to 'UTC'
        When I load the configuration file
        And I set the 'timezone' configuration option to 'GMT'
        Then I should get 'GMT' as 'timezone' option

In this feature, we are describing, from the outside, how "a developer" would be able to store configuration options. We do not care about the internal behavior - we will when we start using phpspec. As long as this feature is running green, we do not care what happens behind the scenes.

Let us run Behat and see what it thinks of our feature:

$ vendor/bin/behat --dry-run --append-snippets 

With this command, we tell Behat to add the necessary step definitions to our feature context. I will not go much into details about Behat, but this adds a bunch of empty methods to our FeatureContext class that maps to our feature steps above.

As an example, take a look at the step definition that Behat added for the there is a configuration file step that we use as the "Given" step in all three scenarios:

/**
 * @Given there is a configuration file
 */
public function thereIsAConfigurationFile()
{
    throw new PendingException();
}

Now, all we have to do is to fill in some code to describe this.

Writing Step Definitions

Before we begin, I have two important points to make about the step definitions:

  1. The point is to prove that the behavior now, is not as we want it to be. After that, we can start to design our code, most of the time using phpspec, in order to get to green.
  2. The implementation of the step definitions are not important - we just need something that works and achieves "1". We can refactor later.

If you run vendor/bin/behat, you will see that all scenarios now have pending steps.

We begin with the first step Given there is a configuration file. We will use a fixture of the configuration file, so we can use the static load() method later on. We care about the static load() method because it gives us a nice API Config::load(), much like the Laravel Facades. This step could be implemented in numerous ways. For now, I think we should just make sure that we have the fixture available and that it contains an array:

/**
 * @Given there is a configuration file
 */
public function thereIsAConfigurationFile()
{
    if ( ! file_exists('fixtures/config.php'))
        throw new Exception("File 'fixtures/config.php' not found");

    $config = include 'fixtures/config.php';

    if ( ! is_array($config))
        throw new Exception("File 'fixtures/config.php' should contain an array");
}

We are going to get to green with this step without implementing any code besides making the fixture. The purpose of a Given step is to put the system in a known state. In this case, that means making sure we have a configuration file.

To get to our first green step, we just need to create the fixture:

# fixtures/config.php
<?php

return array();

Next, we have an And step, which in this case is just an alias for Given. We want to make sure that the configuration file contains an option for the timezone. Again, this is only related to our fixture, so we do not care much about it. I slammed the following (hackish) code together to accomplish this:

/**
 * @Given the option :option is configured to :value
 */
public function theOptionIsConfiguredTo($option, $value)
{
    $config = include 'fixtures/config.php';

    if ( ! is_array($config)) $config = [];

    $config[$option] = $value;

    $content = "<?php\n\nreturn " . var_export($config, true) . ";\n";

    file_put_contents('fixtures/config.php', $content);
}

The above code is not pretty, but it accomplishes what it needs to. It lets us manipulate our fixture from within our feature. If you run Behat, you will see that it added the "timezone" option to the config.php fixture:

<?php

return array (
  'timezone' => 'UTC',
);

Now is the time to bring in some of the original "Taylor code"! The step When I load the configuration file will consist of code that we actually care about. We will bring in some of the code from the raw text file from earlier, and make sure that it runs:

/**
 * @When I load the configuration file
 */
public function iLoadTheConfigurationFile()
{
    $this->config = Config::load('fixtures/config.php'); // Taylor!
}

Running Behat, of course this will fail, since Config does not yet exist. Let us bring phpspec to the rescue!

Designing With Phpspec

When we run Behat, we will get a fatal error:

PHP Fatal error: Class 'Config' not found...

Thankfully, we have phpspec available, including its awesome code generators:

$  vendor/bin/phpspec desc "Config"              
Specification for Config created in .../spec/ConfigSpec.php.

$ vendor/bin/phpspec run --format=pretty
Do you want me to create `Config` for you? y

$ vendor/bin/phpspec run --format=pretty

      Config

  10  ✔ is initializable


1 specs
1 examples (1 passed)
7ms

With these commands, phpspec created the following two files for us:

spec/
`-- ConfigSpec.php
src/
`-- Config.php

This got us rid of the first fatal error, but Behat is still not running:

PHP Fatal error: Call to undefined method Config::load() in ...

load() is going to be a static method and as such, is not easily specced with phpspec. For two reasons this is OK, though:

  1. The behavior of the load() method is going to be very simple. If we need more complexity later on, we can extract logic to small testable methods.
  2. The behavior, as for now, is covered well enough by Behat. If the method does not load the file into an array properly, Behat will squirk at us.

This is one of those situations where a lot of developers will hit the wall. They will throw away phpspec and conclude that it sucks and is working against them. But, see how nicely Behat and phpspec complement each other here?

Instead of trying to get 100% coverage with phpspec, let us just implement a simple load() function and be confident that it is covered by Behat:

<?php

class Config
{
    protected $settings;

    public function __construct()
    {
        $this->settings = array();
    }

    public static function load($path)
    {
        $config = new static();

        if (file_exists($path))
            $config->settings = include $path;

        return $config;
    }
}

We are pretty confident that our configuration options are now loaded. If not, the rest of our steps will fail and we can look into this again.

Building the Feature With Iteration

Back to green with both Behat and phpspec, we can now look at our next feature step Then I should get 'UTC' as 'timezone' option.

/**
 * @Then I should get :value as :option option
 */
public function iShouldGetAsOption($value, $option)
{
    $actual = $this->config->get($option); // Taylor!

    if ( ! strcmp($value, $actual) == 0)
        throw new Exception("Expected {$actual} to be '{$option}'.");
}

In this step we write more of that code we wish we had. Running Behat though, we will see that we do not have a get() method available:

PHP Fatal error: Call to undefined method Config::get() in ...

It is time to return to phpspec and sort this out.

Testing accessors and mutators, AKA getters and setters, is almost like that old chicken or egg dillemma. How can we test the get() method if we do not yet have the set() method and vice versa. How I tend to go about this is to just test them both at once. This means that we are actually going to implement the functionality to set a configuration option, even though we did not reach that scenario yet.

The following example should do:

function it_gets_and_sets_a_configuration_option()
{
    $this->get('foo')->shouldReturn(null);

    $this->set('foo', 'bar');

    $this->get('foo')->shouldReturn('bar');
}

First, we will have the phpspec generators help us get started:

$ vendor/bin/phpspec run --format=pretty
Do you want me to create `Config::get()` for you? y

$ vendor/bin/phpspec run --format=pretty
Do you want me to create `Config::set()` for you? y

$ vendor/bin/phpspec run --format=pretty

      Config

  10  ✔ is initializable
  15  ✘ gets and sets a configuration option
        expected "bar", but got null.

        ...

1 specs
2 examples (1 passed, 1 failed)
9ms

Now, let us get back to green:

public function get($option)
{
    if ( ! isset($this->settings[$option]))
        return null;

    return $this->settings[$option];
}

public function set($option, $value)
{
    $this->settings[$option] = $value;
}

And, there we go:

$ vendor/bin/phpspec run --format=pretty

      Config

  10  ✔ is initializable
  15  ✔ gets and sets a configuration option


1 specs
2 examples (2 passed)
9ms

That got us a long way. Running Behat, we see that we are well into the second scenario now. Next, we need to implement the default option feature, since get() is just returning null right now.

First feature step is similar to the one we wrote earlier. Instead of adding the option to the array, we will unset it:

/**
 * @Given the option :option is not yet configured
 */
public function theOptionIsNotYetConfigured($option)
{
    $config = include 'fixtures/config.php';

    if ( ! is_array($config)) $config = [];

    unset($config[$option]);

    $content = "<?php\n\nreturn " . var_export($config, true) . ";\n";

    file_put_contents('fixtures/config.php', $content);
}

This is not pretty. I know! We could surely refactor it, since we are repeating ourselves, but that is not the scope of this tutorial.

The second feature step also looks familiar, and is mostly copy and paste from earlier:

/**
 * @Then I should get default value :default as :option option
 */
public function iShouldGetDefaultValueAsOption($default, $option)
{
    $actual = $this->config->get($option, $default); // Taylor!

    if ( ! strcmp($default, $actual) == 0)
        throw new Exception("Expected {$actual} to be '{$default}'.");
}

get() is returning null. Let us jump over to phpspec and write an example to solve this:

function it_gets_a_default_value_when_option_is_not_set()
{
    $this->get('foo', 'bar')->shouldReturn('bar');

    $this->set('foo', 'baz');

    $this->get('foo', 'bar')->shouldReturn('baz');
}

First, we check that we get the default value if "option" is not yet configured. Second, we make sure that the default option does not overwrite a configured option.

At first glance, phpspec might seem like overkill in this instance, since we are almost testing the same thing with Behat already. I like to use phpspec to spec the edge-cases though, which are sort of implied in the scenario. And also, the code generators of phpspec are really great. I use them for everything and I find myself working faster whenever I am using phpspec.

Now, phpspec confirms what Behat already told us:

$ vendor/bin/phpspec run --format=pretty

      Config

  10  ✔ is initializable
  15  ✔ gets and sets a configuration option
  24  ✘ gets a default value when option is not set
        expected "bar", but got null.

        ...

1 specs
3 examples (2 passed, 1 failed)
9ms

In order to get back to green, we will add an "early return" to the get() method:

public function get($option, $defaultValue = null)
{
    if ( ! isset($this->settings[$option]) and ! is_null($defaultValue))
        return $defaultValue;

    if ( ! isset($this->settings[$option]))
        return null;

    return $this->settings[$option];
}

We see that phpspec is now happy:

$ vendor/bin/phpspec run --format=pretty

      Config

  10  ✔ is initializable
  15  ✔ gets and sets a configuration option
  24  ✔ gets a default value when option is not set


1 specs
3 examples (3 passed)
9ms

And so is Behat, and so are we.

We are done with our second scenario and have one left to go. For the last scenario, we only need to write the step definition for the And I set the 'timezone' configuration option to 'GMT' step:

/**
 * @When I set the :option configuration option to :value
 */
public function iSetTheConfigurationOptionTo($option, $value)
{
    $this->config->set($option, $value); // Taylor!
}

Since we already implemented the set() method, this step is already green:

$ vendor/bin/behat                      
Feature: Configuration files
    In order to configure my application
    As a developer
    I need to be able to store configuration options in a file

  Scenario: Getting a configured option              # features/config.feature:6
    Given there is a configuration file              # FeatureContext::thereIsAConfigurationFile()
    And the option 'timezone' is configured to 'UTC' # FeatureContext::theOptionIsConfiguredTo()
    When I load the configuration file               # FeatureContext::iLoadTheConfigurationFile()
    Then I should get 'UTC' as 'timezone' option     # FeatureContext::iShouldGetAsOption()

  Scenario: Getting a non-configured option with a default value # features/config.feature:12
    Given there is a configuration file                          # FeatureContext::thereIsAConfigurationFile()
    And the option 'timezone' is not yet configured              # FeatureContext::theOptionIsNotYetConfigured()
    When I load the configuration file                           # FeatureContext::iLoadTheConfigurationFile()
    Then I should get default value 'CET' as 'timezone' option   # FeatureContext::iShouldGetDefaultValueAsOption()

  Scenario: Setting a configuration option                 # features/config.feature:18
    Given there is a configuration file                    # FeatureContext::thereIsAConfigurationFile()
    And the option 'timezone' is configured to 'UTC'       # FeatureContext::theOptionIsConfiguredTo()
    When I load the configuration file                     # FeatureContext::iLoadTheConfigurationFile()
    And I set the 'timezone' configuration option to 'GMT' # FeatureContext::iSetTheConfigurationOptionTo()
    Then I should get 'GMT' as 'timezone' option           # FeatureContext::iShouldGetAsOption()

3 scenarios (3 passed)
13 steps (13 passed)
0m0.04s (8.92Mb)

Wrap up

Everything is nice and green, so let us have a quick wrap up and see what we have accomplished.

We have effectively described the external behavior of a configuration file loader, first by using Taylor's approach, and then by using a traditional BDD approach. Next, we have implemented the feature, using phpspec to design and describe the internal behavior. The example we have been working on is pretty simple, but we have covered the basics. If we need more complexity, we can just extend what we have already. Using BDD, we have at least three options:

  1. If we observe a bug or need to change some internals of our software, we can describe that using phpspec. Write a failing example that showcases the bug and write the code necessary to get to green.
  2. If we need to add a new use case to what we have, we can add a scenario to  config.feature. We can then iteratively work our way through each step, using Behat and phpspec.
  3. If we need to implement a new feature, such as supporting YAML config files, we can write a whole new feature and start over, using the approach we have used throughout this tutorial.

With this basic setup, we have no excuses not to write a failing test or spec, before we write our code. What we have built is now covered by tests, which will make it much easier to work with it in the future. Add to that, that our code is also fully documented. The intended use cases are described in plain English and the internal workings are described in our specs. These two things will make it a breeze for other developers to understand and work with the codebase.

It is my hope that this tutorial helped you to better understand how BDD can be used in a PHP context, with Behat and phpspec. If you have any questions or comments, please do post them below in the comments section.

Thank you for reading along!

Advertisement