PHP best practices - Unit tests (pt. I)
It took me a long time to get used to unit tests in PHP. I never really liked them cause they’re time consuming and, that was my main problem, you barely find any useful real word examples in even advanced PHP books. Most of those examples are about testing custom string functions, like the ever-popular sluggify() method, or plain and simple objects. No wonder it was pretty hard for me to figure out why unit tests were so popular among the agile community.
Today, I can’t even image how I could survive without them. Not only it’s a great method for giving you confidence that your apps run stable (even while you’re sleeping), but it will also help you to produce much better code. When you write a new class or method you’ll always keep in mind to write it in a way that’ll keep it being testable. It’ll help you to avoid dependencies inside your code, or to rethink certain strategies.
Let me show you some best practices that I use when writing unit tests.
Isolate hard-coded dependecies
Consider the following situation:
class Foo { public function doAwesomeStuff() { // ... $bar = new Bar($someVar); // ... } } class Bar { protected $v; public function __construct($v) { $this->v = $v } }
The doAwesomeStuff() method of the Foo class is nearly untestable cause it depends on another class, in this case: Bar. It might be trivial for this example, but try to think of a more complex class Bar hat might behave unpredictable for certain values of $someVar How are we able to solve this? It seems impossible.
Well, think of the following approach:
class Foo { public function doAwesomeStuff() { // ... $bar = $this->createBarObject($someVar); // ... } protected function createBarObject($someVar) { return new Bar($someVar); } } class Bar { protected $v; public function __construct($v) { $this->v = $v } }
We have isolated the hard-coded dependency by refactoring the $bar object’s creation into its own factory method. But wait, we still have to deal with this (unpredictable) Bar class, – isn’t that whole unit testing thing about mocking and stubbing objects?
Yes it is. We will use a mock $bar object to make sure that we’ll always have the same behavior within the doAwesomeStuff() method. But how can we use the mock object for our test? After all, the $bar object is created in a protected factory method, so there’s no change of reaching it.
Time for some OOP magic (to be honest – it’s no magic, though, it’s pretty slick):
// Assuming $barMock == mocked bar object class FooTestHelper extends Foo { protected $barMockObject = null; public function setBarMockObject($barMock) { $this->barMockObject = $barMock; } protected function createBarObject() { return $this->barMockObject; } }
The FooTestHelper class extends the Foo class and adds an instance variable to hold the mocked object and a method setMockBarObject() to actually inject the mocked object (it’s basically following the dependency injection pattern). In addition to that it overrides the createBarObject() by returning the formerly injected mocked object. Now it fits perfectly for our testing needs:
(This example shows how to use this best practice with the great Lime2 testing framework by Bernhard Schussek and Fabien Potencier)
$t = new LimeTest(); //@BeforeAll class FooTestHelper extends Foo { protected $barMockObject = null; public function setBarMockObject($barMock) { $this->barMockObject = $barMock; } protected function createBarObject() { return $this->barMockObject; } } //@Before $barMock = $t->mock('Bar'); $foo = new FooTestHelper(); $foo->setMockBarObject($barMock); //@After unset($barMock); unset($foo); //@Test: ->doAwesomeStuff() // Define expected behavior, e.g. $barMock->any(/* ... */); // ... // replay it $barMock->replay(); // Call the method $foo->doAwesomeStuff(); // Verify the expected bahavior $barMock->verify();
It’s pretty straight forward, so I guess that fans of the popular PHPUnit testing framework will understand it as well, even if they’re not familiar with Lime2.
(to be continued …)
