The goal of this project is to increase testing productivity by leveraging the power of dependency injection and self-initializing fixtures for PHPUnit tests.

The tools contain 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 playback).

Here’s an example of a test case built with TestTools – 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.

To define services, simply create a config.yml (optionally config.local.yml for local modifications) in your base test directory.

TestTools can be used to test any application, framework or library, just like PHPUnit_Framework_TestCase. It is not limited to the Symfony ecosystem. The Symfony Components DI container was chosen, because of it's easy to understand container configuration in YAML.

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.

To cover some of the most common use cases, Doctrine DBAL (SQL), Guzzle and Buzz (HTTP) are supported out of the box.

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());
    }
}

Classic vs mockist style of unit testing

These tools simplify writing unit tests using real objects and test doubles via dependency injection, so some developers might be concerned that the resulting tests are not true unit tests as class dependencies are not mocked by default. Mocking is creating objects that simulate the behaviour of real objects. 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 required to be able to test sometimes, but creating and maintaining mocks can be a boring, time-consuming endeavour. Therefore, you should think about avoiding their widespread usage 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. Actually, the need for excessive mocking is an indicator for bad software design.

In theory, the mockist style can 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.

Even code that depends on databases or Web services, can be easily tested using self-initializing fixtures instead of hand-written mocks. The only thing they can not properly simulate is state, but robust unit tests shouldn't depend on state anyways. If you want to test state, use functional tests of the user interface or API instead.

Composer

If you are using composer, simply add "lastzero/test-tools" to your composer.json file and run composer update:

"require-dev": {
    "lastzero/test-tools": "~2.0"
}

For PHP 5.4 compatibility, use version "~1.2".