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:
In PHPSpec you start by creating the specification first. It will describe the Dog class we are going to build:
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);
}
}
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.
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
{
}
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.
Dogs make sounds
As all dog owners will tell you, dogs bark, so let's add this behavior to our doggo:
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');
}
}
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:
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.
Time to write some code and get us back to green!
<?php
# src/App
class Dog
{
public function makeSound(): string
{
return 'Vuf Vuf';
}
}
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:
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');
}
}
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?
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');
}
}
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
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';
}
}
Mocks and Spies
Let's say for business reason, every time a Dog greets someone we want to our application to log that information.
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);
}
}
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);
}
}
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();
}
}
As always, PHPSpec will help us by creating some methods or us:
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;
}
}
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)