The myth of the untestable controller

« »

It’s a persistent statement: controllers should have as little code as possible because they’re difficult, nay impossible, to test. Developers should force most of their code into the models instead, where business, validation and other logic can take place. This way, the models are reusable and the code is easily tested in isolation. After all, if the controller can’t be adequately tested, then the controller can’t be expected to contain very much crucial logic. The controller becomes just a data and information traffic cop.

But this is not true. Controllers are no more or less testable than any other kind of code. What’s more, the fact that people believe controllers are largely untestable is an excuse for writing untestable code, not a valid design decision.

Where the myth comes from

Back when I was working on Zend Framework, controllers were untestable. They had lots of dependencies, many of which were difficult if not impossible to mock. Testing a controller, at least in isolation, was nearly impossible.

This isnt designed as a slam against Zend Framework 1; in fact, Zend Framework has made tremendous improvements in testability in version 2. Other older frameworks, especially those that did not take advantage of PHP 5.3 features, had similar issues. And so, the focus was on promoting the logic to the model, which was essentially pure OOP, totally decoupled fro the dependencies introduced into the controller by the framework.

The controller is as testable as you want it to be

But the controller, being just an object, is as testable as you want it to be.

In a small framework that I’ve written, the controller is entirely testable. Dependencies are injected through use of a dependency inversion container (Aura.Di in fact). These dependencies can be effectively mocked. The controller is responsible for returning a response object that is decoupled from an HTTP response which can be inspected and evaluated for correctness.

So what strategies can we use to improve controller testability?

Strategies for improving testability

There are three strategies that I like best for improving the testability of controllers overall: dependency injection, the Abstract Factory design pattern, and configuration options.

Dependency injection/dependency inversion

Dependency injection is the easiest way to improve testability, because objects that are injected can be mocked and the object under test can be tested in isolation. When using this strategy with true dependency inversion (type hinting on abstractions like interfaces and abstract classes), you improve the overall testability as well.

When injecting your dependencies, you’re making other parts of your application responsible for instantiating and managing those dependencies. Of course, this can shift the untestable code elsewhere, but objects that are responsible for creating these dependencies can be tested for whether or not they constructed the objects correctly.

There are challenges with dependency injection, especially in complex controllers that have the need for many dependencies. Injecting these dependencies can be challenging, but you can reduce the overall challenge by using a dependency injection container. Always consider carefully what dependencies you really need, as well; you may be injecting too many dependencies (or need to abstract your code further).

The Abstract Factory design pattern

One of the responsibilities of the controller has long been to instantiate model objects. But this both violates the single responsibility principle and the idea that controllers should be testable. What is a developer to do?

For getting model objects from within the controller, using the abstract factory design pattern is a great choice.

The abstract factory design pattern delegates creation of an object to another object, whose sole responsibility is creating the object you requested. And, if you implement a common strategy for your models, you can even have the abstract factory determine which objects to create based on the parameters you give it.

Here’s a diagram of the abstract factory pattern:

Abstract Factory

Note that the Client (the Controller) has a reference to the Abstract Factory, but is not actually aware or involved in the creation of the objects. Whatever object the Abstract Factory returns is based on the concrete implementation of the Abstract Factory; in this way, swapping the Abstract Factory for a testable version is no hard feat. The object returned by our testable abstract factory must simply implement the interface the Client expects.

Configuration, not coupling

When I write code, I like to use driver names and anonymous functions to create new objects. In this way, I remove the “new ObjectName()” from my code as best as possible, reducing the coupling between layers and objects.

Anonymous functions (introduced in PHP 5.3) can serve very well for creating objects. For example:

<?php

$config = array(
    // ... a bunch of configs ...
    'database' => array(
        'user' => 'myuser',
        'pass' => 'mypass',
        'host' => '127.0.0.1',
        'name' => 'mydbname',
    ),
    'database_driver' => function($host, $dbname, $user, $pass) { return new Modus\Database\Mysql($host, $dbname, $user, $pass); },
);

How easy is it to swap out the “Modus\Database\Mysql” object for, say, “Testable\Objects\MysqlMock” that implements the correct interfaces? Quite. Just change the config for testing.

Controllers are testable – if you make them that way

The testability of controllers ultimately falls to developers. It’s up to us to make them easy to test, and then to test them appropriately. Even though the “fat model, skinny controller” mantra still applies, it has nothing to do with testing and everything to do with code reuse and library construction. So go forth and test your controllers; your lower bug count and regression count will thank you!

Brandon Savage is the author of Mastering Object Oriented PHP and Practical Design Patterns in PHP

Posted on 9/23/2013 at 7:00 am
Categories: Testing, SOLID, Web Architecture, System Architecture, Zend Framework, PHP

Hari K T (@harikt) wrote at 9/24/2013 2:07 am:

Nice to hear about the use case of Aura.Di .

Thank you

pctgrass (@pctgrass) wrote at 9/24/2013 5:26 pm:

s/decoupled fro the dependencies/decoupled from the dependencies

Thx!

Dani D wrote at 9/26/2013 2:29 am:

Interesting perspective. Some people question controller testability not because it can’t be done but because there is really not much to test if the controller is skinny. Regarding the abstract factory I think your example is a Factory Method according to GOF original book. Even a Factory Method pattern might be to much when you just have a single type of object that you need to create like a BookMapper.

« »

Copyright © 2023 by Brandon Savage. All rights reserved.