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:

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());
}
}

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.

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:

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);
}
}

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.