If you compare PhpSpec to other testing frameworks, you will find that it is a very sophisticated and opinionated tool. One of the reasons for this, is that PhpSpec is not a testing framework like the ones you already know.
Instead, it is a design tool that helps describing behavior of software. A side effect of describing the behavior of software with PhpSpec, is that you will end up with specs that will also serve as tests afterwards.
In this article, we will take a look under the hood of PhpSpec and try to gain a deeper understanding of how it works and how to use it.
If you want to brush up on phpspec, take a look at my getting started tutorial.
In This Article...
- A Quick Tour Of PhpSpec Internals
- The Difference Between TDD and BDD
- How is PhpSpec different (from PHPUnit)
- PhpSpec: A Design Tool
A Quick Tour Of PhpSpec Internals
Let's start out by looking at some of the key concepts and classes that forms PhpSpec.
Understanding $this
Understanding what $this
refers to is key to understand how PhpSpec differs from other tools. Basically, $this
refer to an instance of the actual class under test. Let's try to investigate this a little more in order to better understand what we mean.
First of all, we need a spec and a class to play around with. As you know, PhpSpec's generators makes this super easy for us:
$ phpspec desc "Suhm\HelloWorld" $ phpspec run Do you want me to create `Suhm\HelloWorld` for you? y
Next up, open the generated spec file and let's try to get a little more information about $this
:
<?php namespace spec\Suhm; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class HelloWorldSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('Suhm\HelloWorld'); var_dump(get_class($this)); } }
get_class()
returns the class name of a given object. In this case, we just throw $this
in there to see what it returns:
$ string(24) "spec\Suhm\HelloWorldSpec"
Okay, so not too surprisingly, get_class()
tells us that $this
is an instance of spec\Suhm\HelloWorldSpec
. This makes sense since, after all, this is just plain old PHP code. If instead we used get_parent_class()
, we would getPhpSpec\ObjectBehavior
, since our spec extends this class.
Remember, I just told you that $this
actually referred to the class under test, which would beSuhm\HelloWorld
in our case? As you can see, the return value of get_class($this)
is contradicting with $this->shouldHaveType('Suhm\HelloWorld');
.
Let's try something else out:
<?php namespace spec\Suhm; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class HelloWorldSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('Suhm\HelloWorld'); var_dump(get_class($this)); $this->dumpThis()->shouldReturn('spec\Suhm\HelloWorldSpec'); } }
With the above code, we try to call a method named dumpThis()
on the HelloWorld
instance. We chain an expectation to the method call, expecting the return value of the function to be a string containing"spec\Suhm\HelloWorldSpec"
. This is the return value from get_class()
on the line above.
Again, the PhpSpec generators can help us with some scaffolding:
$ phpspec run Do you want me to create `Suhm\HelloWorld::dumpThis()` for you? y
Let's try to call get_class()
from within dumpThis()
too:
<?php namespace Suhm; class HelloWorld { public function dumpThis() { return get_class($this); } }
Again, not surprisingly, we get:
10 ✘ it is initializable expected "spec\Suhm\HelloWorldSpec", but got "Suhm\HelloWorld".
It looks like we are missing something here. I started out by telling you that $this
is not referring to what you think it does, but so far our experiments have shown nothing unexpected. Except one thing: How could we call $this->dumpThis()
before it was existing without PHP squeaking at us?
In order to understand this, we need to dive into the PhpSpec source code. If you want to take a look yourself, you can read the code on GitHub.
Take a look a the following code from src/PhpSpec/ObjectBehavior.php
(the class that our spec extends):
/** * Proxies all call to the PhpSpec subject * * @param string $method * @param array $arguments * * @return mixed */ public function __call($method, array $arguments = array()) { return call_user_func_array(array($this->object, $method), $arguments); }
The comments give most of it away: "Proxies all call to the PhpSpec subject"
. The PHP __call
method is a magic method called automatically whenever a method is not accessible (or non-existing).
This means that when we tried to call $this->dumpThis()
, the call was apparently proxied to the PhpSpec subject. If you look at the code, you can see that the method call is proxied to $this->object
. (The same goes for properties on our instance. They are all proxied to the subject as well, using other magic methods. Take a look in the source to see for yourself.)
Let's consult get_class()
one more time and see what it has to say about $this->object
:
<?php namespace spec\Suhm; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class HelloWorldSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('Suhm\HelloWorld'); var_dump(get_class($this->object)); } }
And look what we get:
string(23) "PhpSpec\Wrapper\Subject"
More on Subject
Subject
is a wrapper and implements the PhpSpec\Wrapper\WrapperInterface
. It is a core part of PhpSpec and allows for all the [seemingly] magic that the framework can do. It wraps an instance of the class we are testing, so that we can do all kinds of things such as calling methods and properties that does not exists and set expectations.
As mentioned, PhpSpec is very opinionated towards how you should write and spec your code. One spec maps to one class. You have only one subject per spec, which PhpSpec will carefully wrap for you. The important thing to note about this is that this allows you to use $this
as if it was the actual instance and makes for really readable and meaningful specs.
PhpSpec contains a Wrapper
which takes care of instantiating the Subject
. It packs the Subject
with the actual object we are spec'ing. Since Subject
implements the WrapperInterface
it must have a getWrappedObject()
method that gives us access to the object. This is the object instance we were searching for earlier with get_class()
.
Let's try it out again:
<?php namespace spec\Suhm; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class HelloWorldSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('Suhm\HelloWorld'); var_dump(get_class($this->object->getWrappedObject())); // And just to be completely sure: var_dump($this->object->getWrappedObject()->dumpThis()); } }
And there you go:
$ vendor/bin/phpspec run string(15) "Suhm\HelloWorld" string(15) "Suhm\HelloWorld"
Even though a lot of things are going on behind the scene, in the end we are still working with the actual object instance of Suhm\HelloWorld
. All is well.
Earlier, when we called $this->dumpThis()
, we learned how the call was actually proxied to the Subject
. We also learned that Subject
is only a wrapper and not the actual object.
With this knowledge, it is clear that we are not able to call dumpThis()
on Subject
without another magic method. Subject
has a __call()
method as well:
/** * @param string $method * @param array $arguments * * @return mixed|Subject */ public function __call($method, array $arguments = array()) { if (0 === strpos($method, 'should')) { return $this->callExpectation($method, $arguments); } return $this->caller->call($method, $arguments); }
This method does one of two things. First, it checks if the method name begins with 'should'. If it does, it is an expectation, and the call is delegated to a method called callExpectation()
. If not, the call is instead delegated to an instance of PhpSpec\Wrapper\Subject\Caller
.
We will ignore the Caller
for now. It, too, contains the wrapped object and knows how to call methods on it. The Caller
returns a wrapped instance when it calls methods on the subject, allowing us to chain expectations to methods, like we did with dumpThis()
.
Instead, let's take a look at the callExpectation()
method:
/** * @param string $method * @param array $arguments * * @return mixed */ private function callExpectation($method, array $arguments) { $subject = $this->makeSureWeHaveASubject(); $expectation = $this->expectationFactory->create($method, $subject, $arguments); if (0 === strpos($method, 'shouldNot')) { return $expectation->match(lcfirst(substr($method, 9)), $this, $arguments, $this->wrappedObject); } return $expectation->match(lcfirst(substr($method, 6)), $this, $arguments, $this->wrappedObject); }
This method is responsible for building an instance of PhpSpec\Wrapper\Subject\Expectation\ExpectationInterface
. This interface dictates a match()
method, which the callExpectation()
calls to check the expectation. There are four different kinds of expectations: Positive
, Negative
, PositiveThrow
and NegativeThrow
. Each of these expectations contains an instance of PhpSpec\Matcher\MatcherInterface
that the match()
method uses. Let's look at matchers next.
Matchers
Matchers are what we use to determine the behavior of our objects. Whenever we write should...
or shouldNot...
, we are using a matcher. You can find a comprehensive list of PhpSpec matchers on my personal blog.
There are many matchers included with PhpSpec, all of which extends the PhpSpec\Matcher\BasicMatcher
class, which implements the MatcherInterface
. The way matchers work is pretty straight forward. Let's take a look at it together and I encourage you to take a look at the source code as well.
As an example, let's look at this code from the IdentityMatcher
:
/** * @var array */ private static $keywords = array( 'return', 'be', 'equal', 'beEqualTo' ); /** * @param string $name * @param mixed $subject * @param array $arguments * * @return bool */ public function supports($name, $subject, array $arguments) { return in_array($name, self::$keywords) && 1 == count($arguments) ; }
The supports()
method is dictated by the MatcherInterface
. In this case, four aliases are defined for the matcher in the $keywords
array. This will allow the matcher to support either: shouldReturn()
, shouldBe()
, shouldEqual()
orshouldBeEqualTo()
, or shouldNotReturn()
, shouldNotBe()
, shouldNotEqual()
or shouldNotBeEqualTo()
.
From the BasicMatcher
, two methods are inherited: positiveMatch()
and negativeMatch()
. They look like this:
/** * @param string $name * @param mixed $subject * @param array $arguments * * @return mixed * * @throws FailureException */ final public function positiveMatch($name, $subject, array $arguments) { if (false === $this->matches($subject, $arguments)) { throw $this->getFailureException($name, $subject, $arguments); } return $subject; }
The positiveMatch()
method throws an exception if the matches()
method (abstract method that matchers must implement) returns false
. The negativeMatch()
method works the opposite way. The matches()
method for theIdentityMatcher
uses the ===
operator to compare the $subject
with the argument supplied to the matcher method:
/** * @param mixed $subject * @param array $arguments * * @return bool */ protected function matches($subject, array $arguments) { return $subject === $arguments[0]; }
We could use the matcher like this:
$this->getUser()->shouldNotBeEqualTo($anotherUser);
Which would eventually call negativeMatch()
and make sure that matches()
returns false.
Take a look at some of the other matchers and see what they do!
Promises of More Magic
Before we end this short tour of PhpSpec's internals, let's have a look at one more piece of magic:
<?php namespace spec\Suhm; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class HelloWorldSpec extends ObjectBehavior { function it_is_initializable(\StdClass $object) { $this->shouldHaveType('Suhm\HelloWorld'); var_dump(get_class($object)); } }
By adding the type hinted $object
parameter to our example, PhpSpec will automagically use reflection to inject an instance of the class for us to use. But with the things we saw already, do we really trust that we really get an instance of StdClass
? Let's consult get_class()
one more time:
$ vendor/bin/phpspec run string(28) "PhpSpec\Wrapper\Collaborator"
Nope. Instead of StdClass
we get an instance of PhpSpec\Wrapper\Collaborator
. What is this about?
Like Subject
, Collaborator
is a wrapper and implements the WrapperInterface
. It wraps an instance of\Prophecy\Prophecy\ObjectProphecy
, which stems from Prophecy, the mocking framework that comes together with PhpSpec. Instead of an StdClass
instance, PhpSpec gives us a mock. This makes mocking laughably easy with PhpSpec and allows us to add promises to our objects like this:
$user->getAge()->willReturn(10); $this->setUser($user); $this->getUserStatus()->shouldReturn('child');
With this short tour of parts of PhpSpec's internals, I hope you see that it is more than a simple testing framework.
The Difference Between TDD And BDD
PhpSpec is a tool for doing SpecBDD, so in order to get a better understanding, let's take a look at the differences between test driven development (TDD) and behavior driven development (BDD). Afterward, we will take a quick look at how PhpSpec differs from other tools such as PHPUnit.
TDD is the concept of letting automated tests drive the design and implementation of code. By writing small tests for each feature, before actually implementing them, when we get a passing test, we know that our code satisfy that specific feature. With a passing test, after refactoring, we stop coding and write the next test instead. The mantra is "red", "green", "refactor"!
BDD has its origin from - and is very similar to - TDD. Honestly, it is mainly a question of wording, which is indeed important since it can change the way we think as developers. Where TDD talks about testing, BDD talks about describing behavior.
With TDD we focus on verifying that our code works the way we expect it to work, whereas with BDD, we focus on verifying that our code actually behave the way that we want it to. A main reason for the emergence of BDD, as an alternative to TDD, is to avoid using the word "test". With BDD we are not really interested in testing the implementation of our code, we are more interested in testing what it does (its behavior). When we do BDD, instead of TDD, we have stories and specs. These makes writing traditional tests redundant.
Stories and specs are closely tied to the expectations of the project stakeholders. Writing stories (with a tool such as Behat), would preferably happen together with the stakeholders or domain experts. The stories cover the external behavior. We use specs to design the internal behavior needed to fullfill the steps of the stories. Each step in a story might require multiple iterations with writing specs and implementing code, before it is satisfied. Our stories, together with our specs, helps us to make sure that not only are we building a working thing, but that it is also the right thing. As so, BDD has a lot to do with communication.
How is PhpSpec Different From PHPUnit?
A few months ago, a notable member of the PHP community, Mathias Verraes, posted "A unit testing framework in a tweet" on Twitter. The point was to fit the source code of a functional unit testing framework into one single tweet. As you can see from the gist, the code is truly functional, and allows you to write basic unit tests. The concept of unit testing is actually pretty simple: Check some sort of assertion and notify the user of the result.
Of course, most testing frameworks, such as PHPUnit, are indeed much more advanced, and can do much more than Mathias' framework, but it still shows an important point: You assert something and then you framework runs that assertion for you.
Let's take a look at a very basic PHPUnit test:
public function testTrue() { $this->assertTrue(false); }
Would you be able to write a super simple implementation of a testing framework that could run this test? I am pretty sure that the answer is "yes" you could do that. After all, the only thing the assertTrue()
method has to do is to compare a value against true
and throw an exception if it fails. At its core, what is going on is actually pretty straight forward.
So how is PhpSpec different? First of all, PhpSpec is not a testing tool. Testing your code is not the main goal of PhpSpec, but it becomes a side effect if you use it to design your software by incrementally adding specs for the behavior (BDD).
Second of all, I think the above sections should have already made it clear how PhpSpec is different. Still, let's compare some code:
// PhpSpec function it_is_initializable() { $this->shouldHaveType('Suhm\HelloWorld'); } // PHPUnit function testIsInitializable() { $object = new Suhm\HelloWorld(); $this->assertInstanceOf('Suhm\HelloWorld', $object); }
Because PhpSpec is highly opinionated and make some assertions as to how our code is designed, it gives us a very easy way to describe our code. On the other hand, PHPUnit does not make any assertions towards our code and lets us do pretty much what we want. Basically all PHPUnit does for us in this example, is to run $object
against theinstanceof
operator.
Even though PHPUnit might seem easier to get started with (i don't think it is), if you are not careful, you can easily fall into traps of bad design and architecture because it lets you do almost anything. That being said, PHPUnit can still be great for many use cases, but it is not a design tool like PhpSpec. There is no guidance - you have to know what you are doing.
PhpSpec: A Design Tool
From the PhpSpec website, we can learn that PhpSpec is:
A php toolset to drive emergent design by specification.
Let me say it one more time: PhpSpec is not a testing framework. It is a development tool. A software design tool. It is not a simple assertion framework that compares values and throws exceptions. It is a tool that assists us in designing and building well-crafted code. It requires us to think about the structure of our code and enforces certain architectural patterns, where one class maps to one spec. If you break the single responsibility principle and need to partially mock something, you will not be allowed to do it.
Happy spec'ing!
Oh! And finally, =since PhpSpec itself is spec'ed, I suggest that you go to GitHub and explore the source to learn more.
Comments