Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Practical PHP Testing Patterns: Humble Object

DZone's Guide to

Practical PHP Testing Patterns: Humble Object

· Web Dev Zone ·
Free Resource

Learn how error monitoring with Sentry closes the gap between the product team and your customers. With Sentry, you can focus on what you do best: building and scaling software that makes your users’ lives better.

The problem: we have to test a component which is tied to a framework or library. This time we cannot modify its design to aid testability, for example by introducing arguments in the constructor to allow for injection. The Api has to move between the constraints of other code.

This scenario is typical for framework components: you have to extend some base class and fill in the missing pieces of logic, so that the framework may perform the rest. A common example in the PHP world is that of Zend Framework controllers, which constructor is fixed and creation is taken care by the framework's MVC stack.

This Humble Object specialization is also called Skinny controller, fat model, but it's not necessary the Model who gets fat: the controller may call a Service Layer or even further abstractions.

How to test logic in framework objects

Naturally, you want to test these object, but in them you find two kinds of logic:
  • A: logic you inserted, and as so should be unit-tested.
  • B: Logic that was inherited or by the way featured by the framework, library, or ORM.

You should only test logic of type A, since the framework is already tested in its build for B. But how to separate them?

The Humble Object pattern solves this problem: the object the framework requires becomes an object which delegates to a Testable Component. Then:

  • you test A by instantiating the Testable Component.
  • B is tested by the framework's developers.
  • You test the whole application wiring with few, heavy and slow end-to-end tests.
Meszaros suggests not to write tests for Humble Object as it's only a delegation layer; in these scenarios we test it but with functional tests (Zend_Test), without isolating it (we're talking about controllers, remember.) The controller becomes just a small Adapter, which in fact in this case is named Humble Object: its purpose is to instantiate and execute the Testable Component..

Variations

  • Humble Dialog: should be called Humble View Script in PHP. Logic in the view is difficult to be tested, both for difficulties in rendering it in isolation and for difficulties in parsing the produced HTML; so we extract logic into testable components like View Helpers and just compose them. They are not only a matter of reuse but also of simple, independent testing.
  • Humble Executable: in PHP may be called Humble Executable Script. Since you cannot cleanly test a .php script (including it contaminates the variable scope), you put the logic in a class and in the script just instantiate an object and call run().
  • Humble Transaction Controller: extracts transaction management into an upper layer. All the code at the underlying one expects to be executed during a transaction; as a result, you can use Transaction Rollback Teardown while testing that layer.
  • Humble Container Adapter: our case of framework controllers that delegate everything, because it is impossible to instantiate them independently or test them in isolation. In languages like Java, typically this is because they should be run inside a container. In our PHP case,  they should be instantiated by the framework, which injects every kind of collaborator in it and manages its lifecycle like an evil J2EE container.
In other languages you know that Humble Object has to be introduced when you heard: "in order to test this, we would have to start the whole container...". For us the same phrase is: "in order to test this, we would have to start the whole MVC machine of the framework..."

Example

The code sample of today cannot be a running one since I cannot bundle a Zend Framework 1 application in this article. In this example, we show the extraction of logic from a Zend Framework controller, in order to test an independent object without strange requirements.

In this scenario, a Data Access Object takes away database-related code from the controller; again, in most of the cases other layers are inserted and the controllers does not interact with these low-level objects.

<?php
/**
 * How do we test this controller? We have to instantiate it, using a
 * constructor designed for the framework, and inject a lot of things like a
 * bootstrap object, which is a bit complex to build. Or we can mock it,
 * but it has lots of methods.
 * In fact, we can only do so with other goodies from the
 * framework, in our case Zend_Test. But the test will be slow since it will
 * be and end-to-end one.
 */
class PostController // extends Zend_Controller_Action
{
    public function indexAction()
    {
        $connection = $this->bootstrap->getResource('connection');
        $stmt = $connection->query('SELECT * FROM posts')->execute();
        $posts = array();
        foreach ($stmt->fetchAll() as $row) {
            $posts[] = $row;
        }
        // pass $posts to the view...
    }
}

/**
 * We separate the logic in an Humble Object (the controller) and the real
 * object which performs the work.
 * We can now test PostsDao in isolation, while the Humble Object short code
 * will be tested by very few end-to-end tests.
 * I don't show PostsDao implementation here for brevity reasons and because
 * it's really simple to grasp what goes inside: the PDO usage.
 */
class PostController // extends Zend_Controller_Action
{
    public function indexAction()
    {
        $connection = $this->bootstrap->getResource('connection');
        $postsDao = new PostsDao($connection);
        $posts = $postsDao->findAll();
        // pass $posts to the view...
    }
}

What’s the best way to boost the efficiency of your product team and ship with confidence? Check out this ebook to learn how Sentry's real-time error monitoring helps developers stay in their workflow to fix bugs before the user even knows there’s a problem.

Topics:

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}