Using Package-private constructor to Test With Static Objects
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);
}
}