One of the idea’s behind behavior driven development is the writing human readable features. As example i am using my own owncloud charts application. The code is quite bad, but ultimately fit my needs at the time. This blog post describes how to start with behavior driven development, setting up behat, writing a feature and putting it in practice.

Setup

In the world of PHP there is a tool for this, called behat. It makes sense installing behat through composer if you are not familiar with composer, you should check it out.

arno@pc:~$ composer require --dev behat/behat
Using version ^3.0 for behat/behat
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing behat/transliterator (v1.0.1)
    Loading from cache

  - Installing behat/gherkin (v4.3.0)
    Loading from cache

  - Installing behat/behat (v3.0.15)
    Loading from cache

    Skipped installation of bin bin/behat for package behat/behat: name conflicts with an existing file
behat/behat suggests installing behat/symfony2-extension (for integration with Symfony2 web framework)
behat/behat suggests installing behat/yii-extension (for integration with Yii web framework)
behat/behat suggests installing behat/mink-extension (for integration with Mink testing framework)
Writing lock file
Generating autoload files

Before we could actually write a test, we let behat initialize the directory structure and setup a default context:

arno@pc:~$ ./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

Writing features

Behat forces you to write features in the gherkin language, gherkin is a “Business Readable, Domain Specific Language that lets you describe software’s behaviour without detailing how that behaviour is implemented.” A good start is to actually know what the business value of your features are, for my charts app I wanted to define 1 of its features.

Feature: Displaying current storage usage in percentage
  As a user
  I want to see current storage usage in percentages
  So that i can keep track of storage used

  Background:
    Given an owncloud user named "ferdinand" was added to owncloud
    And an owncloud user named "dan" was added to owncloud
    And an owncloud user named "elton" was added to owncloud

  Scenario: Showing current storage information of ferdinand
    Given "15" GB owncloud storage was measured by owncloud user "ferdinand"
    And "80" GB owncloud storage was measured by owncloud user "elton"
    And "5" GB owncloud storage was measured by owncloud user "dan"
    And owncloud has "100" GB of free storage space left
    When calculating storage usage in percentages
    Then the percentage for owncloud user "ferdinand" should be "7.5"%
    And the percentage for owncloud user "elton" should be "40"%
    And the percentage for owncloud user "dan" should be "2.5"%
    And the remaining percentage should be "50"%

  Scenario: Showing current storage information of ferdinand
    Given "15" GB owncloud storage was measured by owncloud user "ferdinand"
    And "5" GB owncloud storage was measured by owncloud user "dan"
    And owncloud has "100" GB of free storage space left
    When calculating storage usage in percentages
    Then the percentage for owncloud user "ferdinand" should be "7.5"%
    And the remaining percentage should be "50"%

Configuration

A great blog post by everzet states that you should test the api and the domain separately, both could break independently. Everzet uses tags ( @critical ) in his examples, I am not. They are technical implementations, and I find they should not belong in tests but in configuration.

default:
    suites:
        domain_features:
            paths:    [ %paths.base%/features/Storage ]
            contexts: [ Domain\CurrentStorageUsageContext ]
        api_features:
            paths:    [ %paths.base%/features/Storage ]
            contexts: [ Api\CurrentStorageUsageContext ]

After configuration, create 2 classes as context. File: features/Domain/CurrentStorageUsageContext.php

<?php
namespace Domain;

use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;

class CurrentStorageUsageContext implements Context, SnippetAcceptingContext
{

}

File: features/Api/CurrentStorageUsageContext.php

<?php
namespace Api;

use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;

class CurrentStorageUsageContext implements Context, SnippetAcceptingContext
{

}

With the contexts in place you could easily generate all steps with PHPStorm through the shortcut alt+enter. This generates all methods for the feature within your specified context.

Implementation

Now that the steps are created, we need to fill them.

class CurrentStorageUsageContext implements Context, SnippetAcceptingContext
{
    /**
     * @var MeasurementResults
     */
    private $measurementResults;

    /**
     * CurrentStorageUsageContext constructor.
     */
    public function __construct()
    {
        $this->owncloud = new Owncloud();
    }

    /**
     * @Given /^an owncloud user named "([^"]*)" was added to owncloud$/
     */
    public function anOwncloudUserNamedWasAddedToOwncloud($name)
    {
        $owncloudUser = Owncloud\User::named($name);
        $this->owncloud->add($owncloudUser);
    }

    /**
     * @Given /^"([^"]*)" GB owncloud storage was measured by owncloud user "([^"]*)"$/
     * @param $name
     */
    public function gbOwncloudStorageWasMeasuredByOwncloudUser($gbOfStorage, $name)
    {
        $GBs = GigaByteMetric::gigabytes($gbOfStorage);
        $owncloudUser = $this->owncloud->getUserByName($name);
        $owncloudStorage = new Owncloud\Storage($GBs);
        $owncloudUser->measure($owncloudStorage);
    }

    /**
     * @Given /^owncloud has "([^"]*)" GB of free storage space left$/
     */
    public function owncloudHasGBOfFreeStorageSpaceLeft($gbOfStorage)
    {
        $GBs = GigaByteMetric::gigabytes($gbOfStorage);

        $this->owncloud->hasFreeStorageSpaceLeft(
            new Owncloud\FreeSpace(
                new Owncloud\Storage($GBs)
            )
        );
    }

    /**
     * @When /^calculating storage usage in percentages$/
     */
    public function calculatingStorageUsageInPercentages()
    {
        $this->measurementResults = $this->owncloud->calculateStorageUsageIn(new Percentage());
    }

    /**
     * @Then /^the percentage for owncloud user "([^"]*)" should be "([^"]*)"%$/
     */
    public function thePercentageForOwncloudUserShouldBe($name, $percentage)
    {
        $owncloudUser = $this->owncloud->getUserByName($name);
        $this->shouldBe(
            $this->measurementResults->forUser($owncloudUser),
            $percentage
        );
    }

    /**
     * @Given /^the remaining percentage should be "([^"]*)"%$/
     */
    public function theRemainingPercentageShouldBe($percentage)
    {
        $this->shouldBe(
            $this->measurementResults->remaining(),
            $percentage
        );
    }

    /**
     * @param $expectedPercentage
     * @param $percentage
     */
    private function shouldBe($expectedPercentage, $percentage)
    {
        PHPUnit_Framework_Assert::assertEquals(
            $expectedPercentage,
            $percentage
        );
    }
}

As you could see, the code is tried to keep as close as possible to the actual feature, making it more readable.

Conclusion

Having to think about features in advance enables you to let go of technical terminology and try to describe what should happen. Keeping the code as close as possible to ubiquitous language makes the code and feature readable and more maintainable.