Loïc Faugeron Technical Blog

phpspec 03/08/2015

Reference: This article is intended to be as complete as possible and is kept up to date.

phpspec is a highly opinionated unit test framework: it was conceived to practice specBDD (test method names should be sentences) and Mockist TDD (collaborators should always be mocked) and tries to enforce some testing practices (see Marcello Duarte's top 10 favourite phpspec limitations).

The first version was created by Pádraic Brady as a port of rspec, but Marcello Duarte and Konstantin Kudryashov took over and released a second version which added code generation. Ciaran McNulty then took the lead from v2.1 and added many features like collaborator generation, better exception specification and currently for version 2.3 better constructor specification.

Usage example

Let's have a look at how phpspec works. For this we'll need to have a project configured with Composer. Here's the composer.json file:

{
    "name": "vendor/project",
    "autoload": {
        "psr-4": {
            "Vendor\\Project\\": "src/Vendor/Project"
        }
    },
    "require": {},
    "require-dev": {}
}

We can install phpspec with the following:

composer require --dev phpspec/phpspec:^2.2

Let's say we want to create a class that edits text files. We can call this class TextEditor:

phpspec describe 'Vendor\Project\TextEditor'

Tip: make your vendor's binaries available by adding vendor/bin to your $PATH. export PATH="vendor/bin:$PATH".

We should now have the spec/Vendor/Project/TextEditorSpec.php file, bootstraped for us by phpspec:

<?php

namespace spec\Vendor\Project;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class TextEditorSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType('Vendor\Project\TextEditor');
    }
}

Note: this test can only be used to test the TextEditor class.

Our first use case will be about creating a file if it doesn't already exist. Those are filesystem operations, so we decide to delegate the actual logic to a Filesystem class (we'll create it later).

So our first step will be to create a set up method (it will be executed before every test method) that initializes our object:

<?php
// File: spec/Vendor/Project/TextEditorSpec.php

namespace spec\Vendor\Project;

use Vendor\Project\Service\Filesystem;
use PhpSpec\ObjectBehavior;

class TextEditorSpec extends ObjectBehavior
{
    function let(Filesystem $filesystem)
    {
        $this->beConstructedWith($filesystem);
    }
}

Arguments passed to test methods are actually test doubles (phpspec uses the typehint to know what to mock).

Now we can create the actual test method:

<?php
// File: spec/Vendor/Project/TextEditorSpec.php

namespace spec\Vendor\Project;

use Vendor\Project\Service\Filesystem;
use Vendor\Project\File;
use PhpSpec\ObjectBehavior;

class TextEditorSpec extends ObjectBehavior
{
    const FILENAME = '/tmp/file.txt';
    const FORCE_FILE_CREATION = true;

    function let(Filesystem $filesystem)
    {
        $this->beConstructedWith($filesystem);
    }

    function it_can_force_file_creation_if_it_does_not_already_exists(File $file, Filesystem $filesystem)
    {
        $filesystem->exists(self::FILENAME)->willReturn(false);
        $filesystem->create(self::FILENAME)->willReturn($file);

        $this->open(self::FILENAME, self::FORCE_FILE_CREATION)->shouldBe($file);
    }
}

This is roughly the equivalent of the following with PHPUnit:

<?php

namespace Vendor\Project\Tests;

use Vendor\Project\Service\Filesystem;
use Vendor\Project\File;
use PhpSpec\ObjectBehavior;

class TextEditorTest extends PHPUnit_Framewor_TestCase
{
    const FILENAME = '/tmp/file.txt';
    const FORCE_FILE_CREATION = true;

    private $prophet;
    private $filesystem;
    private $textEditor;

    protected function setUp()
    {
        $this->prophet = new \Prophecy\Prophet();
        $this->filesystem = $this->prophet->prophesize('Vendor\Project\Service\Filesystem');
        $this->textEditor = new TextEditor($filesystem->reveal());
    }

    protected function tearDown()
    {
        $this->prophet->checkPredictions();
    }

    /**
     * @test
     */
    public function it_can_force_file_creation_if_it_does_not_already_exists()
    {
        $file = $this->prophet->prophesize('Vendor\Project\File');
        $this->filesystem->exists(self::FILENAME)->willReturn(false);
        $this->filesystem->create(self::FILENAME)->willReturn($file);

        self::assertSame($file, $this->textEditor->open(self::FILENAME, self::FORCE_FILE_CREATION));
    }
}

Note: We tried to make the test method as descriptive as possible (e.g. not testOpen()). This is the whole point of specBDD (specification Behavior Driven Development).

phpspec uses the mocking library prophecy (also available in PHPUnit since version 4.4), which tries to be as less verbose as possible: to describe interractions, test doubles can almost be used as the actual object, except we need to add a ->willreturn() or ->shouldBeCalled() call afterwards.

Note: By mocking all collaborators, we are forced to think how our object interracts with them. This is the whole point of Mockist TDD (Mockist Test Driven Development).

Now that we have a test, we are going to execute the test suite (as advocated by TDD):

phpspec run

It will ask the following 7 questions:

  1. Would you like me to generate an interface Vendor\Project\Service\Filesystem for you?
  2. Would you like me to generate an interface Vendor\Project\File for you?
  3. Would you like me to generate a method signature Vendor\Project\Service\Filesystem::exists() for you?
  4. Would you like me to generate a method signature Vendor\Project\Service\Filesystem::create() for you?
  5. Do you want me to create Vendor\Project\TextEditor for you?
  6. Do you want me to create Vendor\Project\TextEditor::__construct() for you?
  7. Do you want me to create Vendor\Project\TextEditor::open() for you?

By accepting everytime, phpspec will bootstrap the following src/Vendor/Project/Vendor/TextEditor.php file:

<?php

namespace Vendor\Project;

class TextEditor
{

    public function __construct($argument1)
    {
        // TODO: write logic here
    }

    public function open($argument1, $argument2)
    {
        // TODO: write logic here
    }
}

In our specification, we make use of non existing class (File and Filesystem) but phpspec also bootstraped them for us, for example src/Vendor/Project/Vendor/Filesystem.php:

<?php

namespace Vendor\Project\Service;

interface Filesystem
{

    public function exists($argument1);

    public function create($argument1);
}

This is extremely usefull to kickstart our TDD cycle!

Once the code is written, we'll execute the test suite again, and then we'll add more use cases to the test class (e.g. what happens if the file already exists?).

For further usage example, have a look at: articles tagged with phpspec.

SpecGen

phpspec's code generator is a big part of its value, but it could do more for us. Hence the SpecGen extension!

We can install it as follow:

composer require --dev memio/spec-gen:^0.4
echo 'extensions:' > phpspec.yml
echo '  - Memio\SpecGen\MemioSpecGenExtension' >> phpspec.yml

If we remove the code generated by phpspec:

rm src/Vendor/Project/TextEditor.php

And re-run the tests:

phpspec run

Then the generated class will be:

<?php

namespace Vendor\Project;

use Vendor\Project\Service\Filesystem;

class TextEditor
{
    private $filesystem;

    public function __construct(Filesystem $filesystem)
    {
        $this->filesystem = $filesystem;
    }

    public function open($argument1, $argument2)
    {
    }
}

Emergent Design

Test are more than simple regression checks: when a class is hard to test it indicates that it does too many things or is too coupled.

They can also be used as a design tool: in our test method we can define exactly how we would like to use the future object. We can even decide on the go that some logic could be done by collaborators that don't exist yet and start defining how we like to interract with them.

This is what Emergent Design is all about: the design emerges on the go from the tests. Then they stop being simple tests, they become specifications: a live documentation of your code, which never become out of date.

Tips

Here are some personal tips:

If the tested object (official name: System Under Test, SUT) creates a value object and passes it to a collaborator you have two choices:

Conclusion

phpspec is a nice unit testing framework, with many advantages:

Give it a try!