PHP - - By Bruno Skvorc

Testing Frenzy – Can We BDD Test the Units?

I’ll be honest, I don’t do much testing. When it’s really necessary and I’m working on big enterprise projects, I do, but in general, my personal projects are usually one-man-army proofs of concept, or fixes on already tested apps.

We’ve done our share of testing posts here at SitePoint, with more coming soon, but I wanted to show you a relatively new testing tool I found that caught my attention because of how unconventional it seemed.

Peridot Logo

Peridot

Peridot is a BDD testing framework (so behavior driven testing) but for your units of code – not your business logic.

Wait, what?

Yes.

If you’re familiar with Behat, you’ll recognize this syntax (it should be fairly readable even if you’re not familiar with it):

Feature: adding a todo

As a user
I want my todos to be persisted
So I don't have to retype them

Scenario: adding a todo
  Given I am on "/"
  When I fill in "todo" with "Get groceries"
  And I press "add"
  And I reload the page
  Then I should see "Get groceries"

Scenario: adding a duplicate todo
  Given I have a done todo "Pick up dinner"
  And I am on "/"
  When I fill in "todo" with "Pick up dinner"
  And I press "add"
  Then I should see "Todo already exists" after waiting
  And I should see 1 "#todos li" elements

The individual phrases are defined in FeatureContext classes like so:

    /**
     * @Then I should see :arg1 after waiting
     */
    public function iShouldSeeAfterWaiting($text)
    {
        $this->getSession()->wait(10000, "document.documentElement.innerHTML.indexOf('$text') > -1");
        $this->assertPageContainsText($text);
    }
    /**
     * @Given I have a todo :arg1
     */
    public function iHaveATodo($todoText)
    {
        $collection = self::getTodoCollection();
        $collection->insert(['label' => $todoText, 'done' => false]);
    }

The framework recognizes them, substitutes the arguments for their values, and tests the conditions.

But these are called “user stories” – they are what the user does, or is expected to do. Behat will literally open a browser when needed, test things, and report back. How could we possibly apply this to units of code?

Like so:

describe('ArrayObject', function() {
    beforeEach(function() {
        $this->arrayObject = new ArrayObject(['one', 'two', 'three']);
    });

    describe('->count()', function() {
        it('should return the number of items', function() {
            $count = $this->arrayObject->count();
            assert($count === 3, 'expected 3');
        });
    });
});

In a similarly story-based style with cascading nested contexts, Peridot will “describe” and ArrayObject‘s count() method (notice how it first describes the ArrayObject, then describes the method later), and add an it for an explanation on what the described entity should do.

The nested context approach also allows you to execute some bootstrapping logic before each entity’s creation, or before each sub-entity’s creation, making the testing framework incredibly flexible. That’s what the beforeEach function does – it initiates a new ArrayObject with three elements, so the count is always executed on the proper instance.

Ok so… what? It’s just an alternative syntax for PHPUnit? Well, yes and no. While it is focused on units and being able to test them in a story-based way, you can also see it as a broker language between clients and developers. That’s what makes Behat so appealing, too – if you give a Behat feature to a client, they will immediately be able to tell what it means and what it’s supposed to test. With a little bit of training, they’ll even be able to write more stories for you – and clients writing the tests for their own app is the holy grail of every developer’s project, I’m sure you agree.

Peridot is similar in that it offers a human readable way to test units – it is still PHP syntax, but its phrasing and nesting make it user-friendly enough to be understood by the client. And the phrasing also lends itself well to some really verbose reporting:

Verbose report

Concurrency

Peridot also offers “just works” concurrency with the concurrency plugin.

This is very handy when you’re testing with database connections, remote calls, or browser actions – all those are very slow, and executing them one after the other would take ages on any system. Instead, the concurrency plugin makes sure tests are executed in separate worker processes, spawned as needed. Once done, each worker communicates back to the parent process with the results, speeding up testing dramatically. There’s no configuration necessary, other than activating a plugin and passing along a flag (--concurrent and --processes) when launching the tests.

It’s important to note that one should be careful about tests that might depend on one another in such cases – for example, if you’re executing tests that do something in a database, it wouldn’t make sense to have them all do something on the same database, because they’d probably end up in conflicts. Instead, the concurrency plugin makes available the ID of the process that’s currently executing the spec, which makes it easy to create process-specific databases during tests, keeping all specs independent and safe:

$id = getenv('PERIDOT_TEST_TOKEN');
$dbname = "mydb_$id";

Focus and Skip

To rival PHPUnit, Peridot also offers a way to completely skip some tests, and to only run others. If you prefix a function with f, the spec being tested will become focused. That’s a fancy way of saying only that test will be executed, and everything not f-ed in that suite will not run. Helpful when fine tuning a test.

<?php
describe('A suite with nested suites', function() {
    fcontext('A focused suite', function() {
        it('should execute this spec', function() {
            // ...
        });

        it('should also execute this spec', function() {
            // ...
        });
    });

    describe('An unfocused suite', function() {
        it('should not execute this spec', function() {
            // ...
        });
    });
});

In other cases, you’ll want to skip a test and mark it as temporarily unimportant or work-in-progress. When would you do this? For example, when a particularly awful implementation of the “drop-in-replacement-but-not-really-drop-in-if-we-are-being-honest” JSON extension for PHP broke my Diffbot PHP Client tests, I could do little else but mark it as skipped and wait for an extension that actually worked. Prefixing with x in Peridot is the same as marking the test skipped in PHPUnit.

xdescribe('A pending suite', function() {
    xcontext('when using a pending context', function() {
        xit('should have a pending spec', function() {
            // ...
        });
    });
});

These two features in combination are able to rival similar features of other testing frameworks.

Events and Plugins

Peridot is highly extensible via plugins and events that fire at certain stages of the testing process.

The events can be listened for in the main peridot.php file, and their full list is available here. With such an open architecture, all sorts of plugins can be easily developed. The Yo Plugin, for example, will send a Yo! notification via the Yo service when tests pass or fail (depending on yo [sic] configuration). The Watcher plugin will monitor your test files and re-run the suite when changes have been detected (compatibility depends on OS – please use non-Windows. Homestead Improved would be best).

Code Coverage

With a powerful event architecture, simple syntax, and effective structure, Peridot also lends itself perfectly to integration with traditional code coverage reporting tools.

Here’s how to integrate it with PHPUnit’s code coverage report.

Custom DSL

If you don’t like the describe-it syntax and aim to replicate something that feels more at home, or something more fitting for your business case, custom DSLs (Domain Specific Languages) can be developed as a replacement or augmentation. With custom DSLs, this becomes possible in Peridot:

Feature("chdir","
    As a PHP user
    I need to be able to change the current working directory",
    function () {

        Scenario(function () {

            Given('I am in this directory', function () {
                chdir(__DIR__);
            });

            When('I run getcwd()', function () {
                $this->cwd = getcwd();
            });

            Then('I should get this directory', function () {
                if ($this->cwd != __DIR__) {
                    throw new \Exception("Should be current directory");
                }
            });

        });

    });

A full example of how to make this happen is available here.

Custom Scopes

It is also possible to natively extend the scope of a test. For example, if we wanted to be able to use a webdriver (web browser for testing URLs) in our tests, we could activate it by installing WebDriver like so:

composer require facebook/webdriver peridot-php/webdriver-manager

Then, you create a class that extends Peridot’s Scope, so it can be tied into the tests:

<?php // src/Example/WebDriverScope
namespace Peridot\Example;

use Evenement\EventEmitter;
use Peridot\Core\Scope;

class WebDriverScope extends Scope
{
    /**
     * @var \RemoteWebDriver
     */
    protected $driver;

    /**
     * @var \Evenement\EventEmitter
     */
    protected $emitter;

    /**
     * @param \RemoteWebDriver $driver
     */
    public function __construct(\RemoteWebDriver $driver, EventEmitter $emitter)
    {
        $this->driver = $driver;
        $this->emitter = $emitter;

        //when the runner has finished lets quit the driver
        $this->emitter->on('runner.end', function() {
            $this->driver->quit();
        });
    }

    /**
     * Add a getPage method to our tests
     *
     * @param $url
     */
    public function getPage($url)
    {
        $this->driver->get($url);
    }

    /**
     * Adds a findElementById method to our tests
     *
     * @param $id
     * @return \WebDriverElement
     */
    public function findElementById($id)
    {
        return $this->driver->findElement(\WebDriverBy::id($id));
    }
}

This is then activated in the main peridot.php file:

<?php // peridot.php
use Peridot\Core\Suite;
use Peridot\Example\WebDriverScope;

require_once __DIR__ . '/vendor/autoload.php';

return function($emitter) {

    //create a single WebDriverScope to port around
    $driver = RemoteWebDriver::create('http://localhost:4444/wd/hub', array(
        WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::FIREFOX
    ));
    $webDriverScope = new WebDriverScope($driver, $emitter);

    /**
     * We want all suites and their children to have the functionality provided
     * by WebDriverScope, so we hook into the suite.start event. Suites will pass their child
     * scopes to all child tests and suites.
     */
    $emitter->on('suite.start', function(Suite $suite) use ($webDriverScope) {
        $scope = $suite->getScope();
        $scope->peridotAddChildScope($webDriverScope);
    });
};

… and suddenly available in tests!

<?php
describe('The home page', function() {
    it('should have a greeting', function() {
        $this->getPage('http://localhost:4000');
        $greeting = $this->findElementById('greeting');
        assert($greeting->getText() === "Hello", "should be Hello");
    });
});

Notice how we can even fully replicate Behat’s behavior with this!

More info about scopes here.

Conclusion

Peridot is a way to make your units of code come to life, following in the steps of the app itself when it’s being BDD-tested. On its own, it’s a great unit testing tool, but in tandem with something like Behat, it can cover the whole app rather nicely.

The describe-it syntax is a bit odd to us who aren’t familiar with other tools using it, but it matches the context in which it’s being executed (describing the units) so meaning-wise, it works rather well. Besides – one can make their own DSL if describe-it doesn’t feel good.

Including Peridot, we now have four major players on the testing market: PHPUnit, Behat, PHPSpec, and Peridot.

Which of these do you use? Why? Do you use a combination? If so, why that specific combination? Can you show us examples? We’d love to take a look at your setup and your code. Tell us in the comments below!

The code examples above were taken from Peridot’s todo app example

Sponsors