Thanaton
Thanaton

Reputation: 370

Dependencies in Unit Testing

Currently, I'm trying to apply unit testing to a project for the first time. Two questions arose:

  1. Is it bad practice if multiple tests depend on each other? In the code below, several test need the outcome of the other tests to be positive, is this general best practice?

  2. How far do you go with mocking objects the SUT depends on? In the code below, the 'Router' depends on 'Route', which depends on 'RouteParameter'. To mock, or not to mock?

The code below is to test my 'Router' object, which accepts routes via Router::addRoute($route) and routes an URL via Router::route($url).

class RouterTest extends PHPUnit_Framework_TestCase {
    protected function createSimpleRoute() {
        $route = new \TNT\Core\Models\Route();
        $route->alias = 'alias';
        $route->route = 'route';
        $route->parameters = array();

        return $route;
    }

    protected function createAlphanumericRoute() {
        $route = new \TNT\Core\Models\Route();
        $route->alias = 'alias';
        $route->route = 'test/[id]-[name]';

        $parameterId = new \TNT\Core\Models\RouteParameter();
        $parameterId->alias = 'id';
        $parameterId->expression = '[0-9]+';

        $parameterName = new \TNT\Core\Models\RouteParameter();
        $parameterName->alias = 'name';
        $parameterName->expression = '[a-zA-Z0-9-]+';

        $route->parameters = array($parameterId, $parameterName);

        return $route;
    }

    public function testFilledAfterAdd() {
        $router = new \TNT\Core\Helpers\Router();

        $router->addRoute($this->createSimpleRoute());

        $routes = $router->getAllRoutes();

        $this->assertEquals(count($routes), 1);

        $this->assertEquals($routes[0], $this->createSimpleRoute());

        return $router;
    }

    /**
    * @depends testFilledAfterAdd
    */
    public function testOverwriteExistingRoute($router) {
        $router->addRoute(clone $this->createSimpleRoute());

        $this->assertEquals(count($router->getAllRoutes()), 1);
    }

    /**
    * @depends testFilledAfterAdd
    */
    public function testSimpleRouting($router) {
        $this->assertEquals($router->route('route'), $this->createSimpleRoute());
    }

    /**
    * @depends testFilledAfterAdd
    */
    public function testAlphanumericRouting($router) {
        $router->addRoute($this->createAlphanumericRoute());

        $found = $router->route('test/123-Blaat-and-Blaat');

        $data = array('id' => 123, 'name' => 'Blaat-and-Blaat');

        $this->assertEquals($found->data, $data);
    }

    /**
    * @expectedException TNT\Core\Exceptions\RouteNotFoundException
    */
    public function testNonExistingRoute() {
        $router = new \TNT\Core\Helpers\Router();

        $router->route('not_a_route');
    }
}

Upvotes: 2

Views: 237

Answers (2)

Wouter de Kort
Wouter de Kort

Reputation: 39898

1) Yes it is definitely a bad practice if tests depend on each other.

A Unit Test should be constructed in such a way that when it fails it immediately points to a specific area in your code. Good Unit Tests will reduce the time you spend debugging. If Tests depend on each other you will loose this benefit because you can't tell which error in your code made the test fail. Also, it's a maintenance nightmare. What if something changes in your 'shared test', then you would have to change all depended tests.

Here you can find some good guidance on how to solve the interacting test problems (The whole xUnit Test pattern book is a must read!)

2) Unit Testing is about testing the smallest thing possible.

Let's say you have an alarm clock (C# code):

public class AlarmClock
{
    public AlarmClock()
    {
        SatelliteSyncService = new SatelliteSyncService();
        HardwareClient = new HardwareClient();
    }

    public void Execute()
    {
        HardwareClient.DisplayTime = SatelliteSyncService.GetTime();

        // Check for active alarms 
        // ....
    }
}

This is not testable. You will need a real satellite connection and a hardware client to check if the right time is set.

The following however will let you mock both hardwareClient and satelliteSyncService.

public AlarmClock(IHardwareClient hardwareClient, ISatelliteSyncService satelliteSyncService)
{
    SatelliteSyncService = satelliteSyncService;
    HardwareClient = hardwareClient;
}

You should however never mock an object that you are actually testing (sounds logical but sometimes I see it happening).

So, how far should you go with mocking. You should mock everything that the class your testing depends on. In such a way you can test your class in complete isolation. And you can control the outcome of your dependencies so you can make sure that your SUT will go trough all code paths.

For example, let SatelliteSyncService throw an exception, let it return an invalid time, and off course let it return the correct time and then at specific moments so you can test if your alarm is activated on the right moment.

For creating your Route test data. Consider using the Builder Pattern. That will help you in only setting what is required for your test to succeed. It will make your tests more expressive and easier to read for others. It will also lower your test maintenance because you have fewer dependencies.

I wrote a blog post about unit testing that expands on the ideas mentioned here. It uses C# to explain the concepts but it applies to all languages.

Upvotes: 5

David Harkness
David Harkness

Reputation: 36542

Your example doesn't show tests depending on each other but rather a single test for class Router that makes use of related classes Route and RouteParameter. Since these others are data holders, I would have no problem with using them in the Router tests.

Using mock objects can be very helpful when testing as Wouter de Kort pointed out, but keep in mind that there are trade-offs. The tests can be harder to read and maintain, and since test code doesn't have its own tests, any complexity is a risk.

Upvotes: 0

Related Questions