Testing Code That Uses Singletons
This article is part of a series on testing untestable code:
- Testing private methods
- Testing code that uses singletons
- Stubbing static methods
- Stubbing hard-coded dependencies
I frequently quote Miško Hevery with
"It is hard to test code that uses singletons."
And then my audience asks me ...
Why is it hard to test code that uses singletons?
Lets have a look at the default implementation of the Singleton design pattern in PHP:
<?php
class Singleton
{
private static $uniqueInstance = NULL;
protected function __construct() {}
private final function __clone() {}
public static function getInstance()
{
if (self::$uniqueInstance === NULL) {
self::$uniqueInstance = new Singleton;
}
return self::$uniqueInstance;
}
}
?>
The code above declares a class that cannot be instantiated (or cloned) by a client using the new (or clone) operator(s). To get a reference to the only instance of the class one has to use the static method getInstance(). Usually the code that uses the Singleton (which we will refer to as client) is strongly coupled to the getInstance() method:
<?php
class Client
{
public function doSomething()
{
$singleton = Singleton::getInstance();
// ...
}
}
?>
It is impossible to write a test for the doSomething() method without also invoking the singleton's getInstance() method. This means that we cannot get a fresh instance of the Singleton class and thus have no guarantee that there are no side effects in multiple tests that interact with the singleton.
Dependency Injection
Dependency Injection can help with decoupling the client from the getInstance() method:
<?php
class Client
{
public function doSomething(Singleton $singleton = NULL)
{
if ($singleton === NULL) {
$singleton = Singleton::getInstance();
}
// ...
}
}
?>
Instead of unconditionally invoking the getInstance() method inside the doSomething() we can now optionally pass in an instance of the Singleton class. This allows us to pass in a test-specific equivalent such as a mock object or stub:
<?php
class ClientTest extends PHPUnit_Framework_TestCase
{
public function testSingleton()
{
$singleton = $this->getMock(
'Singleton', /* name of class to mock */
array(), /* list of methods to mock */
array(), /* constructor arguments */
'', /* name for mocked class */
FALSE /* do not invoke constructor */
);
// ... configure $singleton ...
$client = new Client;
$client->doSomething($singleton);
// ...
}
}
?>
Alternative Singleton Implementations
Either as an alternative or in addition to rewriting the clients to optionally accept an instance of the Singleton class as an argument, we can also rewrite the Singleton class to make testing easier.
Resettable Singleton
The first approach is to add a reset() method to the Singleton class:
<?php
class Singleton
{
private static $uniqueInstance = NULL;
protected function __construct() {}
private final function __clone() {}
public static function getInstance()
{
if (self::$uniqueInstance === NULL) {
self::$uniqueInstance = new Singleton;
}
return self::$uniqueInstance;
}
public static function reset() {
self::$uniqueInstance = NULL;
}
}
?>
Invoking the reset() method causes the getInstance() method to create a fresh object of the Singleton class the next time it is called.
Singleton with Test Context
The second approach is to add a testing context to the Singleton class:
<?php
class Singleton
{
private static $uniqueInstance = NULL;
public static $testing = FALSE;
protected function __construct() {}
private final function __clone() {}
public static function getInstance()
{
if (self::$uniqueInstance === NULL ||
self::$testing) {
self::$uniqueInstance = new Singleton;
}
return self::$uniqueInstance;
}
}
?>
Setting Singleton::$testing = TRUE; causes the getInstance() method to create a fresh object of the Singleton class each time it is called.
PHPUnit Can Help, Too
PHPUnit has a backup/restore mechanism for static attributes of classes.
This is yet another feature of PHPUnit that makes the testing of code that uses global state (which includes, but is not limited to, global and superglobal variables as well as static attributes of classes) easier.
Just Because You Can, Does Not Mean You Should
Yes, it is possible write testable code that uses singletons.
This does not mean, however, that you should use them without thinking twice.
11/02/2010 at 16:06 Permalink
function &get_instance($force = false) {
static $instance;
if (is_object($force)) {
$instance = array($force);
}
if (!$instance || ($force === true)) {
$instance = array(new MyClass());
}
return $instance[0];
}
Reply
11/02/2010 at 16:28 Permalink
Reply
11/02/2010 at 16:36 Permalink
Reply
11/02/2010 at 18:18 Permalink
In fact, it's exactly my experiences in unit testing Zend_Controller_Front that have pushed me to ban singletons from ZF 2.0 unless there's a really, really good use case. That said, any singleton does need to be testable -- so methods such as reset() or a context switch (like the $testing property) will need to be in place. Yes, they can be abused, but if you're abusing it, you know why you're doing it.
Reply
11/02/2010 at 16:30 Permalink
you have an error in your reset method:
self::$instance = NULL;
I think it must be:
self::$uniqueInstance = NULL
Greetz Peter
Reply
11/02/2010 at 16:36 Permalink
Reply
11/02/2010 at 17:11 Permalink
Reply
11/02/2010 at 18:09 Permalink
Also, all "behavior" that exists for testability lies in MockSingleton, rather than any code whatsoever existing in Singleton specifically to make it testable. I had started to show and/or explain more fully in here, but figured that would be overkill.
Reply
11/02/2010 at 17:40 Permalink
My preferred approach is to provide a setter method that takes a Singleton argument (and enforces that its argument is of the correct type!). Then the test code can wire in a mock singleton with the desired behavior.
This does not, admittedly, test the "new Singleton" code path, but that's presumably something you want to test in the test class for Singleton itself, not in a test class for a client that *uses* the Singleton object.
Reply
11/02/2010 at 17:45 Permalink
The purpose of the reset() method is not to introduce dependency injection or some other refactoring that actually improves the design. The reset() method's purpose is rather to improve the testability of code that uses singletons with only making a single change which is adding the reset() method to the singleton.
Reply
11/02/2010 at 17:59 Permalink
Reply
11/02/2010 at 22:10 Permalink
Reply