Why less mocking can improve everyone’s testing experience

6996747396_f995a49477_bThere’s some confusion in the community concerning the use of mocks for unit testing. Mocking is creating objects that simulate the behaviour of real objects. I personally don’t mock class dependencies by default but use real objects and test doubles via dependency injection.

Despite the success of this approach, many developers are still plagued by concerns that the resulting tests are just some sort of component or integration tests and not true unit tests.

Are tests without mocks still real unit tests? Yes!

These apparently conflicting approaches are referred to as the classic and mockist styles of unit testing:

The classical TDD style is to use real objects if possible and a double if it’s awkward to use the real thing. So a classical TDDer would use a real warehouse and a double for the mail service. The kind of double doesn’t really matter that much.

A mockist TDD practitioner, however, will always use a mock for any object with interesting behavior. In this case for both the warehouse and the mail service.
Martin Fowler

Mocks and test doubles are necessary for writing tests sometimes, but creating and maintaining mocks can be a time-consuming endeavour. If you care about productivity, you should think about avoiding their widespread use and prefer using real objects instead. From my experience, they do no harm – quite the contrary: You can instantly see, how the real objects interact with each other instead of waiting for functional tests. Indeed, the need for excessive mocking can be in indicator for bad software design.

Finding the broken line of code is not an issue in practice

The mockist style might be a bit more precise when it comes to finding a broken line of code, because all classes are tested in complete isolation. In practice, classic unit tests will also provide you with a stack trace that points you to the right line of code:

We didn’t find it difficult to track down the actual fault, even if it caused neighboring tests to fail. So we felt isolation wasn’t an issue in practice.
Martin Fowler

In the worst case, more than one test case fails, if just one class or function is broken. This will give you even more information about the issue and allows to find and fix affected code easily.

Think of a unit not as an isolated class but as a certain functionality

Good classes abstract away their dependencies and inner workings (encapsulation), so you don’t have to care or to worry about them in your tests. The only exception are databases and other external services that you should replace with test doubles like self-initializing fakes. Fixtures that return the same data every time are perfectly fine, since unit tests don’t like state by definition. If you have to test state, use functional tests of the user interface or API instead.

TestTools for PHPUnit

To improve the testing productivity of my team, I created a library called TestTools for PHPUnit. It contains two independent components: A test case class with integrated DI container for easy dependency injection using YAML config files and self-initializing fixtures as test doubles for storage backends such as SQL databases or REST services (record and replay). We’ve been using them for many years with great success in projects large and small.

Here’s an example of a test case – note the setUp() method, which get’s the ready-to-use object from the dependency injection container:

use TestTools\TestCase\UnitTestCase;

class FooTest extends UnitTestCase
{
    protected $foo;

    public function setUp()
    {
        $this->foo = $this->get('foo');
    }

    public function testBar()
    {
        $result = $this->foo->bar('Pi', 2);
        $this->assertEquals(3.14, $result);
    }
}

You’ll get fresh instances in every test, so there is no global state that could harm our tests. From that point of view, they run in isolation. The compiled service definitions in the container are reused however for performance reasons.

This approach let’s you create tests much faster, you’ll get a higher code coverage and need to invest less effort in maintenance. It can be applied to all programming languages and is not limited to PHP.

Self-initializing fixtures

The concept of self-initializing fakes as test doubles can be applied to all types of external data stores (databases) and services like SOAP or REST APIs.

SelfInitializingFixtureTrait enables existing classes to work with file based fixtures (record and playback):

use TestTools\Fixture\SelfInitializingFixtureTrait;

class Foo extends SomeBaseClass
{
    use SelfInitializingFixtureTrait;

    public function bar($name, $type, array $baz = array())
    {
        return $this->callWithFixtures('bar', func_get_args());
    }
}

To cover some of the most common use cases, Doctrine DBAL (SQL) is supported out of the box.

GitHub repository

If you’re developing in PHP and like to give the tools a try, you can find them on GitHub:

https://github.com/lastzero/test-tools

Leave a Reply