Test Doubles: creating a Test Spy

In this series of posts about Test Doubles we’ve looked at the manual and tool-aided creation of a dummy object and the use of the test stub. Now it’s time to discuss the creation of a Test Spy. The Test Spy is defined as follows.

A Test Spy is a more capable version of a Test Stub. It can be used to verify the indirect outputs of the SUT by giving the test a way to inspect them after exercising the SUT.

We’ve already covered the Test Stub, so this post is about the verification of indirect outputs. An example of an indirect output that we may want to test is logging. Logging is a somewhat underappreciated discipline and in some cases a critical requirement.
If you want, you can clone / download the examples used in this post on github.

The SUT
We want to write a unit test for the remove method of UserService and in that unit test want to ensure the logger logs the correct message at info level.

public class UserService {
  
  private static final Logger LOG = LoggerFactory.getLogger(UserService.class);

  public void remove(String userName) {
    /* code that removes user,
     * implementation omitted */
    LOG.info("Removed " + userName + " from the database at " + System.nanoTime());
  }
  
}

The indirect output looks something like this INFO app.spy.UserService - Removed John Doe from the database at 342806181474227. Verifying this output is a challenge.

PowerMock To The Rescue
Mockito cannot mock the static method getLogger. But PowerMock comes to the rescue!

@RunWith(PowerMockRunner.class)
@PrepareForTest(LoggerFactory.class)
public class UserServiceTest {

  @Test
  public void expect_removal_to_be_logged() {
    // PowerMock enables us to insert our spy logger into the static method getLogger
    Logger spyLogger = mock(Logger.class);
    mockStatic(LoggerFactory.class);
    when(LoggerFactory.getLogger(any(Class.class)))
      .thenReturn(spyLogger);

    String userToRemove = "John Doe";
    // exercise SUT
    new UserService().remove(userToRemove);

    // check that the info message is OK
    verify(spyLogger).info(startsWith("Removed " + userToRemove + " from the database"));
  }

}

With the JUnit annotation @RunWith we tell JUnit to use the PowerMockRunner. PrepareForTest indicates that we are going to manipulate the LoggerFactory. In fact we are manipulating the byte-code, because getLogger is static and is set at compile/link time (early binding). Now that we can influence the LoggerFactory, we tell it to return the logger we can verify.
Notice that we use the real UserService and have only created a test double for logging. The verify checks whether we have a message at info level that starts with the expected text.

The Use Of A Test Spy
A Test Spy should be used when you want to create a partial mock. That is what the example demonstrates: use the real object, stub it partially and verify the indirect output.

Leave a Reply

Your email address will not be published. Required fields are marked *