Using Package-private constructor to Test With Static Objects
Tuesday, August 11, 2009 at 2:05PM In my previous post, I explained the difficulty of testing with static objects. This post shows how to use a package-private constructor to deal with that problem.
Here's the class we need to fix:
final class Foo
{
private static final String KEY = "SOME_KEY";
private final Preferences preferences;
public Foo()
{
preferences = Preferences.systemNodeForPackage(SomeOtherClass.class);
}
public void disable()
{
preferences.putBoolean(KEY, false);
}
public void enable()
{
preferences.putBoolean(KEY, true);
}
public boolean isEnabled()
{
final boolean defaultValue = false;
return this.preferences.getBoolean(KEY, defaultValue);
}
}
This class needs fixing because it's hard to write independent tests because each Foo instance uses the same Preferences object. We fix the problem by introducing a package-private constructor:final class Foo
{
private static final String KEY = "SOME_KEY";
private final Preferences preferences;
public Foo()
{
this(Preferences.systemNodeForPackage(Foo.class));
}
Foo(final Preferences preferences)
{
this.preferences = preferences;
}
public void enable()
{
preferences.putBoolean(KEY, true);
}
public void disable()
{
preferences.putBoolean(KEY, false);
}
public boolean isEnabled()
{
final boolean defaultValue = false;
return this.preferences.getBoolean(KEY, defaultValue);
}
}
Note that the package-private constructor does not impact normal consumers of Foo in any way. Consumers continue to use the default constructor that (unbeknown to them) delegates to the package-private constructor.Rather than use Foo's default constructor, our tests use the package-private constructor and Mockito to inject a mock Preferences object that each Foo instance uses:
Now testEnable fails because its Foo instance calls Preferences.putBoolean on a mock. By default, when a void method (such as Preferences.putBoolean) is called on a mock created by Mockito, the mock does nothing. Therefore, we need to change our testing approach.public class FooTest extends TestCase
{
private Foo foo;public void setUp() throws Exception
{
final Preferences preferences = mock(Preferences.class);
this.foo = new Foo(preferences);
}
public void testDisable() throws Exception
{
this.foo.disable();
assertFalse(this.foo.isEnabled());
}
public void testEnable() throws Exception
{
this.foo.enable();
assertTrue(this.foo.isEnabled());
}
public void testConstructor() throws Exception
{
assertFalse(this.foo.isEnabled());
}
}
To test a Foo method, we assert that the Foo instance delegates to its Preferences object. To test the Foo constructor, we assert that no delegation to its Preferences object has occurred:
Note that to test delegation, we changed Foo.KEY from a private member to a package-private member. That's a perfectly natural event that occurs during testing. Treat the test class as a first-class consumer of the production class and let the test class dictate the API of the production class.public class FooTest extends TestCase
{
private Preferences preferences;
private Foo foo;public void setUp() throws Exception
{
this.preferences = mock(Preferences.class);
this.foo = new Foo(preferences);
}
public void testDisable() throws Exception
{
this.foo.disable();
verify(this.preferences).putBoolean(Foo.KEY, false);
}
public void testEnable() throws Exception
{
this.foo.enable();
verify(this.preferences).putBoolean(Foo.KEY, true);
}
public void testConstructor() throws Exception
{
verifyNoMoreInteractions(this.preferences);
}
}
Reader Comments