Why is a singleton class hard to test?
What if your singleton was performing operations on a database or writing data to a file? You would not want that occurring in a unit test. You would want to mock out the object to perform some operations in memory instead so you could verify them without having permanent side effects. Unit tests should be self contained and should not create connections to databases or perform other operations with outside systems that could fail and then cause your unit test to fail for an unrelated reason.
Example with pseudo-java (I'm a C# dev):
public class MySingleton {
private static final MySingleton instance = new MySingleton();
private MySingleton() { }
public int doSomething() {
//create connection to database, write to a file, etc..
return something;
}
public static MySingleton getInstance() {
return instance;
}
}
public class OtherClass {
public int myMethod() {
//do some stuff
int result = MySingleton.getInstance().doSomething();
//do some other suff
return something;
}
}
In order to test myMethod
we have to make an actual database call, file operation etc
@Test
public void testMyMethod() {
OtherClass obj = new OtherClass();
//if this fails it might be because of some external code called by
//MySingleton.doSomething(), not necessarily the logic inside MyMethod()
Asserts.assertEqual(1, obj.myMethod());
}
If MySingleton
was instead something like:
public class MyNonSingleton implements ISomeInterface {
public MyNonSingleton() {}
@Override
public int doSomething() {
//create connection to database, write to a file, etc..
return something;
}
}
you could then inject it as a dependency into MyOtherClass like this:
public class OtherClass {
private ISomeInterface obj;
public OtherClass(ISomeInterface obj) {
this.obj = obj;
}
public int myMethod() {
//do some stuff
int result = obj.doSomething();
//do some other stuff
return something;
}
}
then you can test like this:
@Test
public void TestMyMethod() {
OtherClass obj = new OtherClass(new MockNonSingleton());
//now our mock object can fake the database, filesystem etc. calls to isolate the testing to just the logic in myMethod()
Asserts.assertEqual(1, obj.myMethod());
}
Personally I think this statement is totally wrong, because it assumes that singleton is not replacable (mockable) for unit tests. On the contrary. In Spring's dependency injection, for example, singleton is actually the default model for DI components. Singletons and dependency injection are not mutually exclusive, which statement above somehow tries to imply.
I agree that anything that can't be mocked makes application more difficult to test, but there is no reason to assume singletons are less mockable than any other objects in your application.
What might be the problem, is the fact that singleton is one global instance and when it can be in too many different states, unit tests might show unpredictable results because of changing state of the singleton. But there are simple solutions to this - mock your singleton and make your mock to have less states. Or write your tests in such a fashion, that singleton is recreated (or reinitialized) before each unit test that depends on it. Or, the best solution, test your application for all possible states of the singleton. Ultimately, if reality requires multiple states, like, for example, a database connection (disconnected/connecting/connected/error/...), then you will have to deal with it regardless of whether you use singletons or not.