Defining clear domain responsibilities, and speccing objects accordingly is a critical part of development that too many skip (...this actually prompted this post).  If you're shaking your head at this point thinking: "Pfft.  I get by."  ...this article's for you, besides, specs are easy.  Here's my two step process:

  • Let go of all the complexities driven by your framework and think about the object (no service managers for example).  You're not a Laravel programmer, you are a programmer...right?
  • Let your specs emerge and define your object (this is why you write your test first, but, it's never too late to graft specs into your existing projects)
 

Preface 1: The Law of Demeter

If you've never heard of this, it's a good rule to live by when writing highly-testable code.  Put simply, it says that you're only allowed to interact with your closest friends.  

Its rigor would have that a method in a "Foo" object is only allowed to work with:

  1. Other methods within Foo
  2. Methods on Foo's properties ( $this->bar->doIt() )
  3. Methods on arguments that are in turn passed into your method
  4. Methods on new objects created within the called method

If you break these rules, your specs aren't likely to emerge into a proper architecture.

 

Preface 2: What is phpspec?

It bills itself on its homepage as:

A php toolset to drive emergent design by specification.

Which is true, yet I've found in exercises where I'm asked to roll in new libraries on old platforms (read: someone else's mess) that it could doubly be billed as:

Will quickly point out questionable architecture decisions.

Part of the magic lies in the thought process and its application of the LoD.  Ever try to write a spec for an object that depends on ZF2's ServiceManager?  You can't, but this is good.  You shouldn't be using the SM to draw dependencies into your classes and its methods in the first place.  Therein lies a third ad:

Will quickly shame you for not having used DI where you should.

Proceed.


The Guide

Here's where the blog post starts.  This is the post I wish I'd read when I started off.  

Installation

Assuming that you are using composer, modify your composer.json and include:

"require-dev": {
    "phpspec/phpspec": "@stable"
}

That'll give you enough to have composer install phpspec for you.  Go ahead and run composer update.  Then, symlink the phpspec executable from your project root:

ln -s ./vendor/bin.phpspec ./phpspec

If you issue a ./phpspec after this, you should see success.

Success!

Success!

Configuration

You'll need to tell phpspec how to behave using its phpspec.yml (Yaml, yes, ouch) file.  You essentially define 'suites' (like phpunit) whose keys contain namespace, spec prefix, where you store your specs and where the source resides.  Create a phpspec.yml in your project root, and define it (here's mine, ZF2 project):

suites:
    Application:
        namespace: Application
        spec_prefix: Spec
        src_path: module/Application/src/
        spec_path: module/Application/bundle

Now we're ready to go!

 

Create a Spec

Your first step is to describe a class.  You can be at this step describing an existing class if you are retrofitting tests to existing code, doesn't matter, do:

./phpspec describe Application/Service/Foo
Notice the forward slashes instead of the usual namespace backslashes.

This yields:

Specification for Application\Service\Foo created in .../module/Application/bundle/Spec/Application/Service/FooSpec.php.

Here's the really cool part if your class doesn't yet exist.

./phpspec run
Do you want me to create `Application\Service\Foo` for you?  Y/n

Answer 'Y' and phpspec will automatically create the code for Foo for you.  Nice!  

Now, open up the spec file (FooSpec) it originally created under module/Application/bundle/Spec.  Put that on your left screen in a horizontal split with Application\Service\Foo, and put this cheat-sheet on the right: https://github.com/yvoyer/phpspec-cheat-sheet

 

Quick-Start to "Examples"

Specs are actually fun to write.  You can conjure your object's role without worrying about implementation (too much).  Worry about spec!  

 

Simple Example

Here's an example spec for a domain-lookup object that should return whether or not a domain is available:

class FooSpec extends ObjectBehavior
{
    function it_checks_domain_availability()
    {
        $this->isAvailable('google.com')->shouldReturn(false);
    }
}

Infer from that snippet:

  • functions (called examples) start with it (or its)
  • you are using '$this' as you would "Foo" despite being "FooSpec" (cool!)
  • your Foo objects return something special that the cheat sheet is a big help with (Subjects)
 

Mocking Objects

It could be then, that the 'isAvailable' method on 'Foo' accepts a Domain object on which it calls 'getName()' rather than a domain name.  Simply change your example:

use Application\Entity\Domain;

class FooSpec extends ObjectBehavior
{
    function it_checks_domain_availability( Domain $domain )
    {
        $domain->getName()->willReturn('google.com');
        $this->isAvailable($domain)->shouldReturn(false);
    }
}

Here then:

  • we mock a domain very simply adding a 'use' statement and using the class in the example's signature
  • the mocked Domain (Prophecy object) needs to return 'google.com' on any invocation of getName(), so we use ->willReturn to specify what it'll give
 

Constructor Magic

Chances are that you need to use a Domain object that returns 'google.com' in many places, and, that your Foo service needs something like an EntityManager as constructor argument:

public function __construct( Doctrine\ORM\EntityManager $em ){ ... }

Let serves this purpose:

use Application\Entity\Domain;
use Doctrine\ORM\EntityManager;

class FooSpec extends ObjectBehavior
{
    function let( Domain $domain, EntityManager $em )
    {
        $domain->getName()->willReturn('google.com');
        $this->beConstructedWith( $em );
    }

    function it_checks_domain_availability( $domain )
    {
        $this->isAvailable($domain)->shouldReturn(false);
    }
}

Some magic happens here:

  • Simply by virtue of having the same variable name ($domain), the $domain configured in let will actually be the same that is used in it_checks_domain_availability
  • We're satisfying a constructor requirement by passing in a mocked EntityManager to beConstructedWith which is a variadic function (accepts n parameters)

These last two are a particular surprise to folks who don't RTM, here is a reference.


Emerging Design

Here's the claim that's made on the phpspec website that you'll understand after a few scars. It will push you into better/good design; the indicator is that things don't feel right or "can't work without a Framework something or other".  Consider for example something like this in Zend Framework 2:

namespace Application\Service;

class CarService implements ServiceLocatorAwareInterface
{
    use ServiceLocatorAwareTrait;

    function changeTires( Car $car )
    {
        $sm = $this->getServiceLocator();
        $tire_manager = $sm->get( TireManager::class );
        if( $tire_manager->hasTires( 4, $car->getTireType() )
        {
            $tires = $tire_manager->markSold( 4, $car->getTireType() );
            $car->installTires( $tires );
        }        
    }
}

Too often do we see code that relies on the SLA to invoke everything.  If you try to write a spec to test this code you will run into errors. The SL is not available as though the ZF bootstrap had run.  

This test would never work:

use Application\Entity\Car;

class CarServiceSpec extends ObjectBehavior
{
    function it_changes_tires( Car $car )
    {
        $this->changeTires($domain)->shouldReturn(false);
    }
}

 

To write a good example, you will have to inject the TireManager into your service -- which is what you should do to keep your model disparate from the framework.  In most cases, you can achieve a setup where your model can be hot-swapped between frameworks even.

use Application\Entity\Car;
use Application\Service\TireManager;

class CarServiceSpec extends ObjectBehavior
{
    function let( TireManager $m )
    {
       $m->hasTires( 4, 'Toyo Proxes' )->willReturn(true);
       $m->markSold( 4, 'Toyo Proxes' )->willReturn('1234');

       $this->beConstructedWith( $m );
    }

    function it_changes_tires( Car $car )
    {
        $car->getTireType()->willReturn( 'Toyo Proxes' );
        $car->installTires( '1234' )->willReturn(true); 

        $this->changeTires($car)->shouldReturn(true);
    }
}

A rough example, but it works.  It follows that you should resort to using a Factory to instantiate your ZF2 Service (which is the right way to go).

So!  Good luck in speccing your objects, if you have any questions or clarifications or fixes -- I do scan comments quickly!