PHPUnit testing singleton dependencies

Unit testing should be simple, but when you have a code base which uses static methods, singletons and a lack of dependency injection, you can run into some serious problems.

The key to writing maintainable and testable code is dependency injection. Without dependency injection, our objects have to rely on systems that may or may not be under our control. We want our unit tests to be executable in an environment which does require a database or a file system or another systems API etc. Dependency injection allows us to provide mocks of these external and frankly irrelevant sources. The mocks will simply tell us what we want to hear and then we can test our own system with this data. Normally we would use PHPUnits getMockBuilder method to create mock objects, this however cannot be used in the case of a singleton for a few reasons:

  1. Singletons make use of static methods which cannot be stubbed
  2. Therefore we would then be forced to try and stub the __constructor, but obviously that would be stupid. The constructor is magic after all and needs to return an instance of the object in question.

Say we have this classic database Singleton.

interface iDatabase {
    private function ___construct() { }
    public static function getInstance() { }
    public function connect() { }
}

class Database implements iDatabase {
  
    private static $instance;
    private function __construct() { }
  
    public static function getInstance() {
        if(!self::$instance) {
            self::$instance = new self();
        }
    }

    public function connect() {
        // .... Connect to the database with PDO etc etc etc
        return $db;
    }
 
}

Then inside a repository class, we want to select a user based on their ID

class UserRepository {
  
    public function find($user_id) {
        $db = Database::getInstance()->connect();
        $load_user = $db->query("SELECT * FROM users WHERE user_id=" . (int) $user_id);
        $user = $load_user->fetchObject('User');
        if(!$user instanceof User) {
            throw new Exception('User not found');
        }
        return $user;
    } 
 
}

Here we have defined a concrete dependency inside the UserRepository which obviously is Database. The problem with this is, as we know, unit tests should not be touching the database, so how can the find() method be tested? The solution is with a bit of refactoring, the Database can be injected to remove UserRepository’s dependency, this way when writing tests, the Database class can be stubbed for something which doesn’t require a real database connection. So here is the re-factored version.

class Repository {
    protected $db;

    public setDatabaseConnection($db) {
        $this->db = $db;
    }
}

class UserRepository extends Repository {
  
    public function find($user_id) {
        $load_user = $this->db->query("SELECT * FROM users WHERE user_id=" . (int) $user_id);
        $user = $load_user->fetchObject('User');
        if(!$user instanceof User) {
            throw new Exception('User not found');
        }
        return $user;
    } 
 
}

Now any database instance can be injected into the UserRepository, we could write a test like so

class MockPDO {
    
    public function query($query) {
        $mock_pdo_statement = new MockPDOStatement();
        return $mock_pdo_statement;
    }


}

class MockPDOStatement {
    
    public function fetchObject($class) {
        $return = new $class();
        return $return;
    }

}

class TestUserRepository extends PHPUnit_Framework_TestCase {
   
    public function testFind() {

        $mock_pdo = new MockPDO();

        $user_repository = new UserRepository();
        $user_repository->setDatabaseConnection($mock_pdo);
        $user = $user_repository->find(1);

        $this->assertTrue($user instanceof User);

    }

}

As the mock objects stand, they will only be useful for this test. If you want to make them useful system wide, you should give each mock object a bunch of attributes which can be modified from inside the tests, these attributes can then be used to determine what the stubs return.

If you have any questions, please do leave a comment.

8 Love This

Leave a Reply

Your email address will not be published.

Time limit is exhausted. Please reload the CAPTCHA.