Testable Maintainable v2

Writing Testable and Maintainable code

Frameworks provide a tool for rapid application development, but often accrue technical debt as rapidly as they allow you to create functionality.

Technical debt is created when maintainability isn't a purposeful focus of the developer. Future changes and debugging becomes costly due to lack of unit testing and structure.

Here's how to begin structuring your code to achieve testability and maintainability - and save you time.

We'll cover (loosely)

  1. DRY
  2. Dependency Injection
  3. Interfaces
  4. Containers
  5. Unit Tests with PHPUnit

Let's start with some contrived but typical code. This might be a model class in any given framework.

class User {

    public function getCurrentUser()
    {
        $user_id = $_SESSION['user_id'];

        $user = App::db->select('id, username')
                        ->where('id', $user_id)
                        ->limit(1)
                        ->get();

        if ( $user->num_results() > 0 )
        {
                return $user->row();
        }

        return FALSE;
    }
}

This code will work, but needs improvement:

  1. This isn't testable.
    • We're relying on the $_SESSION global variable. Unit-testing frameworks such as PHPUnit rely on the command-line, where $_SESSION and many other global variables aren't available.
    • We're relying on the database connection. Ideally, actual database connections should be avoided in a unit-test. Testing is about code, not about data.
  2. This code isn't as maintainable as it could be. For instance, if we change the data source, we'll need to change the database code in every instance of App::db used in our application. Also, what about instances where we don't want just the current user's information?

An attempted Unit Test

Here's an attempt to create a unit test for the above functionality.

class UserModelTest extends PHPUnit_Framework_TestCase {

	public function testGetUser()
	{
		$user = new User();
		
		$currentUser = $user->getCurrentUser();
		
		$this->assertEquals(1, $currentUser->id);
	}

}

Let's examine this.

First, this unit test will fail. The $_SESSION variable used in the User object doesn't exist in a unit test, as it runs PHP in the command line.

Second, there's no database connection setup. This means that in order to make this work, we will need to bootstrap our application in order to get the App object and its db object. We'll also need a working database connection to test against.

To make this unit test work, we would need to:

  1. Setup a config setup for a CLI (PHPUnit) run in our application
  2. Rely on a database connection. Doing this means relying on a data source separate from our unit test. What if our test database doesn't have the data we're expecting? What if our database connection is slow?
  3. Relying on an application being bootstrapped increases the overhead of the tests, slowing the unit tests down dramatically. Ideally, most of our code can be tested independent of the framework being used.

So, let's get down to how we can improve this.

Keep code DRY

The function retrieving the current user is unnecessary in this simple context. This is a contrived example, but in the spirit of DRY principles, the first optimization I'm choosing to make is to generalize this method.

class User {

    public function getUser($user_id)
    {
        $user = App::db->select('user')
                        ->where('id', $user_id)
                        ->limit(1)
                        ->get();

        if ( $user->num_results() > 0 )
        {
            return $user->row();
        }

        return FALSE;
    }                              
}

This provides a method we can use across our entire application. We can pass in the current user at the time of the call, rather than passing that functionality off to the model. Code is more modular and maintainable when it doesn't rely on other functionalities (such as the session global variable).

However, this is still not testable nor as maintainable as it could be. We're still relying on the database connection.

Dependency Injection

Let's help improve the sitation by adding some Dependency Injection. Here's what our model could look like when we pass the database connnection into the class.

class User {

	protected $_db;
	
    public function __construct($db_connection)
    {
        $this->_db = $db_connection;
    }

    public function getUser($user_id)
    {
        $user = $this->_db->select('user')
                        ->where('id', $user_id)
                        ->limit(1)
                        ->get();

        if ( $user->num_results() > 0 )
        {
            return $user->row();
        }

        return FALSE;
    }
}

Now the dependencies of our User model are provided for. Our class no longer assumes a certain database connection nor relies on any global objects.

At this point, our class is basically testable. We can pass in a data-source of our choice (mostly) and a user id, and test the results of that call. We can also switch out separate database connections (Assuming the both implement the same methods for retrieving data). Cool.

Let's look at what a unit test could look like for that.

<?php

use Mockery as m;
use Fideloper\User;

class SecondUserTest extends PHPUnit_Framework_TestCase {

    public function testGetCurrentUserMock()
    {
        $db_connection = $this->_mockDb();

        $user = new User( $db_connection );

        $result = $user->getUser( 1 );

        $expected = new StdClass();
        $expected->id = 1;
        $expected->username = 'fideloper';

        $this->assertEquals( $result->id,        $expected->id,          'User ID set correctly' );
        $this->assertEquals( $result->username,  $expected->username,    'Username set correctly' );
    }

    protected function _mockDb()
    {
        // "Mock" (stub) database row result object
        $returnResult = new StdClass();
        $returnResult->id = 1;
        $returnResult->username = 'fideloper';

        // Mock database result object
        $result = m::mock('DbResult');
        $result->shouldReceive('num_results')->once()->andReturn( 1 );
        $result->shouldReceive('row')->once()->andReturn( $returnResult );

        // Mock database connection object
        $db = m::mock('DbConnection');

        $db->shouldReceive('select')->once()->andReturn( $db );
        $db->shouldReceive('where')->once()->andReturn( $db );
        $db->shouldReceive('limit')->once()->andReturn( $db );
        $db->shouldReceive('get')->once()->andReturn( $result );

        return $db;
    }

}

I've added something new to this unit test: Mockery. Mockery lets you "mock" (fake) php objects. In this case, we're mocking the database connection. With our mock, we can skip over testing a database connection and simply test our model.

In this case, we're mocking a SQL connection. We're telling the mock object to expect to have the select, where, limit and get methods called on it. I am returning the Mock itself, to mirror how the SQL connection object returns itself ($this), thus making its method calls "chainable". Note that for the get method, I return the database call result - a stdClass object with the user data populated.

This solves a few problems:

  1. We're testing only our model class. We're not also testing a database connection
  2. We're able to control the inputs and outputs of mock database conection and therefore can reliably test against the result of the database call. I know I'll get a user ID of "1" as a result of the mocked database call.
  3. We don't need to bootstrap our application, nor have any configuration or database present to test

We can still do much better. Here's where it gets interesting.

Interfaces

To improve this further, we can define and implement an interface. Consider this code.

interface UserRepositoryInterface {
    public function getUser($user_id);
}

class MysqlUserRepository implements UserRepositoryInterface {
	
	protected $_db;
	
    public function __construct($db_conn)
    {
        $this->_db = $db_conn;
    }

    public function getUser($user_id)
    {
        $user = $this->_db->select('user')
        			->where('id', $user_id)
        			->limit(1)
        			->get();

        if ( $user->num_results() > 0 )
        {
            return $user->row();
        }

        return FALSE;
    }
}

class User {
	
	protected $userStore;
	
    public function __construct(UserRepositoryInterface $user)
    {
        $this->userStore = $user;
    }

    public function getUser($user_id)
    {
        return $this->userStore->getUser($user_id);
    }
}

There's a few things happening here.

  1. First we define an interface for our user data source. This defines the addUser() method.
  2. Next, we implement that interface. In this case, we create a MySQL implementation. We accept a database connection object, and use it to grab a user from the database.
  3. Lastly, we enforce the use of a class implementing our UserInterface in our User model. This guarantees that the data source will always have a getUser() method available, no matter what data source is used to implement UserInterface.

Note that our User object type-hints UserInterface in its constructor. This means that a class implementing UserInterface MUST be passed into the User object. This is a guarantee we are relying on - we need the getUser method to always be available.

What is the result of this?

  • Our code is now fully testable. For the User class, we can easily mock the datasource. (Testing the implementations of the datasource would be the job of a separate unit test).
  • Our code is much more maintainable. We can switch out different data sources without having to change code throughout our application.
  • We can create ANY data source. ArrayUser, MongoDbUser, CouchDbUser, MemoryUser, etc.
  • We can easily pass any data source to our User object if we need to - If you decide to ditch SQL, you can just create a different implementation (for instance MongoDbUser) and pass that into your User model.

We've simplified our unit test as well!

<?php

use Mockery as m;
use Fideloper\User;

class ThirdUserTest extends PHPUnit_Framework_TestCase {

    public function testGetCurrentUserMock()
    {
        $userRepo = $this->_mockUserRepo();

        $user = new User( $userRepo );

        $result = $user->getUser( 1 );

        $expected = new StdClass();
        $expected->id = 1;
        $expected->username = 'fideloper';

        $this->assertEquals( $result->id,        $expected->id,          'User ID set correctly' );
        $this->assertEquals( $result->username,  $expected->username,    'Username set correctly' );
    }

    protected function _mockUserRepo()
    {
        // Mock expected result
        $result = new StdClass();
        $result->id = 1;
        $result->username = 'fideloper';

        // Mock any user repository
        $userRepo = m::mock('Fideloper\Third\Repository\UserRepositoryInterface');
        $userRepo->shouldReceive('getUser')->once()->andReturn( $result );

        return $userRepo;
    }

}

We've taken the work of mocking a database connection out completely. Instead of we simply mock our data source, and tell it what to do when getUser is called.

But, we can still do better!

Containers

Consider the usage of our current code:

// In some controller
$user = new User( new MysqlUser( App:db->getConnection('mysql') ) );
$user_id = App::session('user_id');

$currentUser = $user->getUser($user_id);

Our final step will be to introduce containers. In the above code, we need to create and use a bunch of objects just to get our current user. This code might be littered across your application. If you need to switch from MySQL to MongoDB, you'll still need to edit everywhere the above code appears. That's hardly DRY. Containers can fix this.

A container simply "contains" an object or functionality. It's similar to a registry in your application. We can use a container to automatically instantiate a new User object with all needed dependencies. Below I use Pimple, a popular container class.

// Somewhere in a configuration file
$container = new Pimple();
$container['user'] = function() {
    return new User( new MysqlUser( App::db->getConnection('mysql') ) );
}

// Now, in all of our controllers, we can simply do this:
$currentUser = $container['user']->getUser( App::session('user_id') );

I've moved the creation of the User model into one location in the application configuration. As a result of adding containers:

  1. We have kept our code DRY. The User object and the data store of choice is defined in one location in our application.
  2. We can switch out our User model from using MySQL to any other data source in ONE location. That is vastly more maintainable.

Final Thoughts

We've accomplished the following:

  1. Kept our code DRY and reusable
  2. Created maintainable code - We can switch out data sources for our objects in one location for the entire application if needed
  3. Made our code testable - We can mock objects easily and neither rely on bootstrapping our application nor on having an available database available to test
  4. Learned about using Dependency Injection and Interfaces in order to enable creating testable and maintainable code
  5. Saw how containers can aid in making our application more maintainable

I'm sure you've noticed that we've added much more code in the name of maintainability and testability. A strong argument can be made against this implementation - We are increasing the complexity. Indeed, this requires a deeper knowledge of code, both for the main author and for collaborators of a project.

However, the cost of explanation and understanding is far out-weighed by the extra overall decrease technical debt.

  • The code is vastly more maintainable - making changes possible in ONE location, rather than several.
  • Being able to unit test (quickly) will reduce bugs in code by a large margin - especially in long-term or community-driven (open-source) projects.

Doing the extra-work up front will save time and headache later.

Resources

You can include Mockery and PHPUnit into your application easily using Composer. Add these to your "require-dev" section in your composer.json file:

"require-dev": {
    "mockery/mockery": "0.8.*",
    "phpunit/phpunit": "3.7.*"
}

You can then install your Composer-based dependencies with the "dev" requirements:

$ php composer.phar install --dev
  • You can find Mockery here
  • You can find PHPUnit here
  • For PHP, consider using Laravel 4, as it makes exceptional use of Containers and other concepts written about here.