Sunday, August 23, 2009

Spring Managed Singletons for Testability

I was recently writing some code that launched and external application and I never wanted to have more than one instance of this application running at once, so I immediately thought to make it a singleton. So, I started out with typical singleton code:

public class MySingleton {
private static final MySingleton INSTANCE
= new MySingleton();
private MySingleton() {
// initialization code here
}
public static MySingleton getInstance() {
return INSTANCE;
}
public Object getSomethingUseful() {
return somethingUseful;
}
}

I had a couple of classes that used this singleton, typically something like this (the method below doesn't do anything practical, it is just used for illustration):

public class SingletonUser {
public void doSomething() {
MySingleton s = MySingleton.getInstance();
Object o = s.getSomethingUseful();
if ( o == null ) {
throw new NullPointException("error");
}
System.out.println("Got the object o");
}
}

The test for the class looks something like:

public class SingletonUserTest {
private SingletonUser su;
@Before
public void setUp() {
su = new SingletonUser();
}
@Test(expected=NullPointerException.class)
public void testDoSomethingWhenGetSomethingUsefulIsNull() {
// do something to make
// su.getSomethingUseful to
// return null
su.doSomething();
fail("NullPointerException expected");
}
@Test
public void testDoSomethingWhenGetSomethingUsefulIsNotNull() {
// do something to make su.getSomethingUseful
// to return something non-null
// redirect System.out to a stream so we
// can test its contents
su.doSomething();
// do some asserts to make sure the stream
// was properly printed
}
}

So what's the problem with this? The line in the testDoSomethingWhenGetSomethingUsefulIsNull that sets the singleton object make sure O returns null. First, the test doesn't ensure that the singleton is reset back to the default state. The singleton may not provide this method (nor should it need to). Second, the singleton may not have easy methods for testing all of the conditions required. A typical solution is to add methods that are used for testing only, which I don't like at all. So what should you do?

Inject the singleton like you would any other bean in Spring and let Spring manage the singleton.

public class SingletonUser {
// A reference to the singleton
private MySingleton s;
public SingletonUser(MySingleton s) {
this.s = s;
}
public void doSomething() {
Object o = s.getSomethingUseful();
if ( o == null ) {
throw new NullPointException("oops");
}
System.out.println("Got the object o");
}
}

Now this class is much easier to test. You can mock out the MySingleton like you would any other class, but in production, the spring bean file would like something like:

<bean id="MySingleton" class="my.pack.MySingleton"
scope="singleton"/>
<bean id="SingletonUser"
class="my.pack.SingletonUser">
<constructor-arg type="my.pack.MySingleton"
ref="MySingleton"/>
</bean>

Now the nastiness of the getInstance calls will be removed and the code is easily tested.

No comments:

Post a Comment