DEV Community

Cover image for Fun Driven Development with PHPSpec
konrad_126
konrad_126

Posted on

Fun Driven Development with PHPSpec

The first time I saw the abbreviation BDD I thought it was a miss-print of TDD (Test Driven Development), but it’s not. It stands for Behaviour Driven Development and it is a technique derived from TDD. The difference between the two is a bit nuanced and best felt when you start using some BDD tools. One such tool is PHPSpec. It will help you write better code and have fun in the process. Sounds good? Let’s give it a try!

There is a dog

Dogs are fun so let's build a Dog game:

Alt Text

In PHPSpec you start by creating the specification first. It will describe the Dog class we are going to build:

Alt Text

PHPSpec generates the specification file for us:

<?php

namespace spec\App;

use App\Dog;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class DogSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType(Dog::class);
    }
}
Enter fullscreen mode Exit fullscreen mode

It doesn't say (describe) much at the moment - just that the class Dog should be initializable.

Let's run PHPSPec to see what happens.

Alt Text

Whoa, that's a lot of pink! What's up with that? Well, this is how PHPSPec indicates that the code is broken (doesn't even run), which is different than it running but returning wrong results. It is also asking us to fix it by creating the missing Dog class for us. So we comply and we get this:

<?php

namespace App;

class Dog
{
}
Enter fullscreen mode Exit fullscreen mode

After creating the Dog class, it runs again and now we have our first spec passing giving us that green light all devs love so much.

Alt Text

Dogs make sounds

As all dog owners will tell you, dogs bark, so let's add this behavior to our doggo:

Alt Text

We describe the desired behaviour in the spec class:

<?php

# spec/App

class DogSpec extends ObjectBehavior
{
    // ...

    function it_should_make_a_sound()
    {
        $this->makeSound()->shouldBe('Vuf Vuf');
    }

}
Enter fullscreen mode Exit fullscreen mode

I know. This looks a bit wired. We are calling the makeSound method but there is no such method in our spec class. And there won't be. The $this keyword in a spec class doesn't refer to the spec class itself, but to the class the spec is describing. In this case, DogSpec described the Dog class, so PHPSpec will try to call the makeSound method on the Dog class. (There is always one spec class per class).

Another wired thing is the shouldBe method chained to the makeSound method. This is what is called a matcher in PHPSpec and it's equivalent to PHPUnit's assertions (and there's a bunch of them). In this case, it will take the output of the makeSound method and check if it matches Vuf Vuf.

Now that we've straightened that out, we can run PHPSpec again:

Alt Text

Since we don't have the makeSound() method on our Dog class yet, there's that purple color again and there's PHPSpec again being nice and helpful offering to create the method for us.

After creating the method it runs again and now we finally have some red color meaning the code works but not as expected.

Alt Text

Time to write some code and get us back to green!

<?php

# src/App

class Dog
{
    public function makeSound(): string
    {
        return 'Vuf Vuf';
    }
}
Enter fullscreen mode Exit fullscreen mode

Stubs

Our object often time collaborate with other objects by asking them questions (and doing something with the answer). If we go back to our pet game, dogs are usually very happy to great people:

Alt Text

To describe this in our specification we'll need a stub:

<?php

# spec/App

class DogSpec extends ObjectBehavior
{
    // ...

    function it_can_greet_a_person(Person $person)
    {
        $person->name()->willReturn('Mike');
        $this->greet($person)->shouldBe('Hello Mike, Vuf Vuf');
    }
}
Enter fullscreen mode Exit fullscreen mode

Looking at this, you may be wondering where is some createTestDouble() method call? In PHPSpec you just inject classes/objects you need and PHPSpec will create test doubles out of them (using prophecy framework) so you can immediately configure them. Here, we are stating the person's class name() method will return Mike when called.

By now you can assume when we run PHPSpec it will offer to create the actual Person class for us, right?

Alt Text

But wait. It says "Would you like me to generate an interface". Why not a class? Hm, now that I think about it, dogs also like to greet other dogs not just humans, so maybe there is an abstraction here that we missed. Let's introduce an interface called Named and make our Dog class is dependent on an interface instead of a concrete class.

<?php

# spec/App

class DogSpec extends ObjectBehavior
{
    // ...

    function it_can_greet_a_person(Named $named)
    {
        $named->name()->willReturn('Mike');
        $this->greet($named)->shouldBe('Hello Mike, Vuf Vuf');
    }
}
Enter fullscreen mode Exit fullscreen mode

Now our Dog can greet any class that implements the Named interface. This design will make it easy to expand the list of earthlings our doggos can greet - just implement the Named interface. Thnx PHPSpec!

We can let PHPSpec make that interface and the greet method for us now

Alt Text

And now we can implement our greet method to get us back to green again:

<?php

# src/App

class Dog
{
    public function makeSound()
    {
        return 'Vuf Vuf';
    }

    public function greet(Named $named)
    {
        return 'Hello ' . $named->name() . 'Vuf Vuf';
    }
}
Enter fullscreen mode Exit fullscreen mode

Mocks and Spies

Let's say for business reason, every time a Dog greets someone we want to our application to log that information.

Alt Text

In this case, our object is going to issue commands to other objects, so we need a mock or spy. First, we are going to state that Dog will be constructed with a Logger:

<?php

# spec/App

class DogSpec extends ObjectBehavior
{
    function let(Logger $logger)
    {
        $this->beConstructedWith($logger);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can describe the interaction with a Logger:

<?php

# spec/App

class DogSpec extends ObjectBehavior
{
     /// ...
    function it_logs_the_greeting(Named $named, Logger $logger)
    {
        $named->name()->willReturn('Marry');
        $logger->log('Hello Marry, Vuf Vuf')->shouldBeCalled();
        $this->greet($named);
    }
}
Enter fullscreen mode Exit fullscreen mode

We are setting an expectation that the log method will be called with Hello Mike, Vuf Vuf once we execute our code. This is a mock. If we wish to use a spy we would describe it as:

<?php

# spec/App

class DogSpec extends ObjectBehavior
{
     /// ...
    function it_logs_the_greeting(Named $named, Logger $logger)
    {
        $named->name()->willReturn('Mike');
        $this->greet($named);
        $logger->log('Hello Mike, Vuf Vuf')->shouldHaveBeCalled();
    }
}
Enter fullscreen mode Exit fullscreen mode

As always, PHPSpec will help us by creating some methods or us:

Alt Text

Then it's up to us to add some code to satisfy the spec:

<?php

# src/App

class Dog
{

    // ...

    public function greet(Named $named)
    {
        $greeting = 'Hello ' . $named->name() . ', Vuf Vuf';
        $this->logger->log($greeting);
        return $greeting;
    }
}
Enter fullscreen mode Exit fullscreen mode

As you see, in PHPSpec we describe our object behaviors using a very descriptive syntax. There is always one spec per object/class which is why PHPSpec can only be used for testing at the unit layer/level. If you want to have some higher-level test you'll need to use some other tool. Maybe another BDD tool as Behat (don't get me started on how cool Behat is)? Another thing you probably don't want to do is introduce PHPSpec in a legacy codebase (with bad design). It will be painful and authors of PHPSpec never intended it to be used that way. But if you are starting a greenfield project, why not give PHPSpec a try. Hope you'll find is as fun as I do.

Top comments (0)