Félix Delval
Published

Thu 09 April 2015

←Home

Writing expressive unit test with injected dependancy

Recently I have been facing problem writing maintainable unit test for code that is relying heavily on injected dependancy.

Sometimes you are facing code that is using multiple injected dependancies that should all be mocked for testing purpose. Mocking is a very effective pattern to test part of a system in isolation. Though when the unit test code is mock intensive it begins to be hard to understand, maintain and serve it first purpose help us in refactoring.

Here is a pattern I found usefull to solve this issue.

Let's take the example of an class and a interface it uses as a dependancy. Here is the interface:

<?php

namespace My\App;

interface CassetteInterface
{
    public function read();
    public function isOver();
    public function rewind();
}

And here the class that uses it :

<?php

namespace My\App;

class CassettePlayer
{
    private $cassette;

    public function __construct(CassetteInterface $cassette)
    {
        $this->cassette = $cassette;
    }

    public function playAndRewind()
    {
        while (!$this->cassette->isOver()) {
            $this->cassette->read();
        }

        $this->cassette->rewind();
    }
}

If you would go writing test for this class, once all code as written, you would most likely start writing a test like this :

<?php

namespace My\App;

class CassettePlayerTest extends \PHPUnit_Framework_TestCase
{
    public function testPlayLoop()
    {
        $cassette = $this->getMock('My\App\CassetteInterface');

        $cassette->expects($this->at(0))
            ->method('isOver')
            ->willReturn(false);
        $cassette->expects($this->at(1))
            ->method('read');
        $cassette->expects($this->at(2))
            ->method('isOver')
            ->willReturn(true);
        $cassette->expects($this->at(3))
            ->method('rewind');

        $player = new CassettePlayer($cassette);

        $player->playAndRewind();
    }
}

Writing the unit test for this class in such a manner leads to complex unit test code that is not maintanable and harder to read. Even though the code is short, the way the mock is around leads to complex formulation, more so when you start setting the expecting parameters and the desired output.

Let's try to restructure the test so it is a bit more modular and reusable.

I like doing the instantiation of the mock in private properties inside of the TestCase::setUp method. It gives two big advantages.

Firstly the mock dependancies declaration needs to be in only one place, making sure that all your tests are using the same objects.

Secondly using them in a property, makes it easy to extract the behaviour definition of the mock to a method without parameters, creating easily readable and reusable behavioural methods.

Here is how it would end :

<?php

namespace My\App;

class CassettePlayerExpressiveTest extends \PHPUnit_Framework_TestCase
{
    private $cassette;
    private $cassetteCounter;

    public function setUp()
    {
        $this->cassette = $this->getMock('My\App\CassetteInterface');
        $this->cassetteCounter = 0;
    }

    public function testPlayLoop()
    {
        $this->cassetteWillNotBeOver();
        $this->cassetteWillBeRead();
        $this->cassetteWillBeOver();
        $this->cassetteWillBeRewinded();

        $player = new CassettePlayer($this->cassette);

        $player->playAndRewind();
    }

    private function cassetteWillNotBeOver()
    {
        $this->cassette->expects($this->at($this->cassetteCounter++))
            ->method('isOver')
            ->willReturn(false);
    }

    private function cassetteWillBeRead()
    {
        $this->cassette->expects($this->at($this->cassetteCounter++))
            ->method('read');
    }

    private function cassetteWillBeOver()
    {
        $this->cassette->expects($this->at($this->cassetteCounter++))
            ->method('isOver')
            ->willReturn(true);
    }

    private function cassetteWillBeRewinded()
    {
        $this->cassette->expects($this->at($this->cassetteCounter++))
            ->method('rewind');
    }
}

The code from the examples is available here

Go Top
comments powered by Disqus