Test Doubles

Goal

Concepts

Library

Dependencies

Lesson

Your first unit tests were easy, because the units you were testing were simple. The classes under scrutiny were small and self-contained, with no dependencies. But as applications gain functionality, the classes that perform the business logic have to specialize and collaborate: they develop dependencies.

Unit tests should be quick. They should focus on small, self-contained units of functionality. If an implementation has dependencies, this presents a complication. What if its dependency needs to access a database, or download a file over the Internet, for example? This would not only slow down the unit test, it would introduce other points of failure, such as the Internet connection going down.

Test Doubles

To keep a unit test fast and focused, even when the unit being tested has dependencies, you can to create substitutes that will take the place of the dependencies, just long enough for the tests to run. These substitutes are known as test doubles, and they come in various types. Like stunt doubles in a movie, they play the role of the real actor only in certain circumstances; in production the application will use the real implementations.

Here are some reasons you might want to use a test double for a dependency:

Dummy

A dummy object is a double used in testing that doesn't need to actually do anything. Let's go back to our example of vehicles and racetracks, and imagine that there is a RaceManager interface for business logic that will determine the prize money to be given when a vehicle wins first, second, or third place.

public interface RaceManager {

  /** Determines the prize one by the given car.
   * @param The race car.
   * @param driver The race car driver.
   * @param place The order crossing the finish line; 1, 2, or 3.
   * @return The amount of prize money.
   */
  BigInteger getPrize(@Nonnull RaceCar car, @Nonnull String driver, int place);

}

We want to test RaceManagerImpl to ensure that a car coming in at first place will receive $1000 in prize money. But to test these methods, we need an instance of the RaceCar interface.

public interface RaceCar implements Vehicle {

  String getMake();

  String getModel();

}

Do we have to create a full-fledged implementation of RaceCar just to see if the prize money is awarded correctly? If you have one handy, of course, you can use it, but it may come with its own dependencies. If you don't expect the RaceManagerImpl to care about the car make and model, you can just pass in a dummy object.

@Test
public void testCorrectPrizeMoney {
  final RaceCar dummyRaceCar = new RaceCar() {
      @Override String getMake() {return "foo";}
      @Override String getModel() {return "bar";}
    };

  final RaceManagerImpl raceManager = new RaceManagerImpl();
  assertThat(raceManager.getPrize(dummyRaceCar, "Test Driver", 1), is(BigInteger.valueOf(1000)));
}

Stub

A test stub is slightly smarter than a dummy in that it provides real answers. But this sort of test double only provides pre-programmed answers—it has no real intelligence to provide answers based upon input data or otherwise make decisions. It just provides canned responses.

Maybe we are required to set aside some of the winnings for tax purposes. To find out the tax rate, RaceManagerImpl must query the TaxStrategy interface, which unfortunately is full of methods we're not interested in for this particular test.

public interface TaxStrategy {

  /** @return The tax rate as a fractional value. */
  BigDecimal getTaxRate();

  /** Does something complicated and unrelated to the tax rate.
   * @param input Some necessary input value.
   */
  void calculateComplicatedPayrollExpenses(final int input);

  …

}

Rather than implementing TaxStrategy in its entirety, we could simply stub out an implementation to return a tax rate we expect, and base our test assertions with that taken into consideration.

@Test
public void testCorrectPrizeMoney {
  final RaceCar dummyRaceCar = new RaceCar() {
      @Override String getMake() {return "foo";}
      @Override String getModel() {return "bar";}
    };
  final TaxStrategy stubTaxStrategy = new TaxStrategy() {
      @Override BigDecimal getTaxRate() {return new BigDecimal("0.10");}
      @Override void calculateComplicatedPayrollExpenses(final int input) {}
      …
    };

  final RaceManagerImpl raceManager = new RaceManagerImpl(stubTaxStrategy);
  assertThat(raceManager.getPrize(dummyRaceCar, "Test Driver", 1), is(BigInteger.valueOf(900)); //tax rate taken into account
}

Mock

Sometimes the predetermined values of a stub are not enough to test a particular unit. You might want the dependency to return values based upon what is passed to it. This type of test double is called a mock object.

You might have wondered earlier how RaceManagerImpl knew how much money to dole out for prizes in the first place. It may be that RaceManagerImpl actually queries a PrizeStrategy interface. The implementation may look like this:

public class RaceManagerImpl implements RaceManager {

  private final PrizeStrategy prizeStrategy;

  private final TaxStrategy taxStrategy;

  public RaceManagerImpl(@Nonnull final PrizeStrategy prizeStrategy,
      @Nonnull final TaxStrategy taxStrategy) {
    this.prizeStrategy = checkNotNull(prizeStrategy);
    this.taxStrategy = checkNotNull(taxStrategy);
  }

  …

  @Override
  public BigInteger getPrize(@Nonnull final RaceCar car, @Nonnull final String driver, final int order) {
    final BigDecimal prize = new BigDecimal(prizeStrategy.getPrize(place));
    final BigDecimal taxRate = taxStrategy.getTaxRate();
    return prize.multiply(BigDecimal.ONE.subtract(taxRate)).toBigInteger();  //prize * (1 - taxRate)
  }

}

The PrizeStrategy implementation we use must be a little smarter than a stub, because it must return a prize amount based upon which place the driver came in. We must add a little logic to the mock implementation.

@Test
public void testCorrectPrizeMoney {
  final RaceCar dummyRaceCar = new RaceCar() {
      @Override String getMake() {return "foo";}
      @Override String getModel() {return "bar";}
    };
  final PrizeStrategy mockPrizeStrategy = new PrizeStrategy() {
      @Override
      public BigInteger getPrize(final int place) {
        switch(place) {
          case 1:  //first place
            return BigInteger.valueOf(1000);
          case 2:  //second place
            return BigInteger.valueOf(500);
          case 3:  //third place
            return BigInteger.valueOf(200);
          default:
            throw new IllegalArgumentException();
        }
      }
    };
  final TaxStrategy stubTaxStrategy = new TaxStrategy() {
      @Override BigDecimal getTaxRate() {return new BigDecimal("0.10");}
      @Override void calculateComplicatedPayrollExpenses(final int input) {}
      …
    };

  final RaceManagerImpl raceManager = new RaceManagerImpl(mockPrizeStrategy, stubTaxStrategy);
  assertThat(raceManager.getPrize(dummyRaceCar, "Test Driver", 1), is(BigInteger.valueOf(900)); //tax rate taken into account
}

Spy

A spy object is one that makes notes about how it was called. Any test double could function as a spy in addition to its normal functionality. Most often, the spying consists of counting how many times a method was called.

public class SpyPrizeStrategy implements PrizeStrategy {

  private int prizeRequestCount = 0;

  /** @return The number of times {@link #getPrize} has beeen called. */
  public int getPrizeRequestCount() {
    return prizeRequestCount;
  }

  @Override
  public BigInteger getPrize(final int place) {
    prizeRequestCount++;
    switch(place) {
      case 1:  //first place
        return BigInteger.valueOf(1000);
      case 2:  //second place
        return BigInteger.valueOf(500);
      case 3:  //third place
        return BigInteger.valueOf(200);
      default:
        throw new IllegalArgumentException();
    }
  }
}

A unit test might thus want to verify that when RaceManagerImpl.getPrize(…) is called, the implementation uses the PrizeStrategy by making a single call to PrizeStrategy.getPrize(…).

@Test
public void testCorrectPrizeMoney {
  final RaceCar dummyRaceCar = new RaceCar() {
      @Override String getMake() {return "foo";}
      @Override String getModel() {return "bar";}
    };
  final SpyPrizeStrategy spyPrizeStrategy = new SpyPrizeStrategy();
  final TaxStrategy stubTaxStrategy = new TaxStrategy() {
      @Override BigDecimal getTaxRate() {return new BigDecimal("0.10");}
      @Override void calculateComplicatedPayrollExpenses(final int input) {}
      …
    };

  final RaceManagerImpl raceManager = new RaceManagerImpl(spyPrizeStrategy, stubTaxStrategy);
  assertThat(raceManager.getPrize(dummyRaceCar, "Test Driver", 1), is(BigInteger.valueOf(900)); //tax rate taken into account
  assertThat(spyPrizeStrategy.getPrizeRequestCount(), is(1)); //the race manager made a single request for the prize
}

Fake

A fake object is a full implementation, but it may take shortcuts for testing or otherwise not be robust enough for production use. The archetypal example is an in-memory repository. Suppose that a class has a dependency on a RaceCarRepository that stores the make, model, and other information about all the race cars ever used. There may be a situation in which the class we're testing has several interactions with the repository, so we can't simply create a mock with a couple of methods.

We happen to have handy an implementation RemoteDatabaseRaceCarRepository, which connects to a relational database in another country. But this would not be good to use in unit tests. It wouldn't be speedy, and the remote connection might not even be reliable while tests are running. Moreover unless we had a firm control over the remote database, we wouldn't even know what values it would give us back.

Rather than using a real implementation that actually contains a history of wins of various drivers, we can create a complete implementation of RaceCarRepository that allowed storing and retrieving race cars. But because we would only use this in tests, there would be no need to actually store and retrieve the objects from permanent storage—an in-memory collection would suffice.

public class FakeRaceCarRepository implements RaceCarRepository {

  private final Map<String, RaceCar> raceCars = new HashMap<>();

  @Override
  public void storeRaceCar(final RaceCar raceCar) {
    return raceCars.put(raceCar.getVin(), raceCar);
  }

  @Override
  public Optional<RaceCar> getRaceCarByVin(final String vehicleIdNumber) {
    return Optional.ofNullable(raceCars.get(vehicleIdNumber));
  }

  …

}

Mockito

Creating a new test double by hand for each unit test is cumbersome and repetitive. That's why several libraries have been created to ease the process, making the use of mocks almost transparent. One of the most popular libraries to use with JUnit is Mockito.

The class org.mockito.Mockito provides most of the static methods you'll use to get started. Going behind the scenes to find out how Mockito works isn't really necessary at this point.

It is sufficient to know how you can use Mockito in practice. Mockito comes with many options and variations in approach, but here are the basic steps:

  1. Tell Mockito to create a mock using Mockito.mock(…).
  2. Use Mockito.when(…) with OngoingStubbing.thenReturn(…) or OngoingStubbing.thenThrow(…) to define results to mock methods.
  3. Use the mock as a dependency to the class you are testing, making JUnit assertions afterwards as normal.
  4. (optional) Use Mockito.verify(…) or one of its variations to ensure that the mock was called

Mockito Dummy

Once you create a test double using Mockito.mock(…), the resulting instance can function as a test dummy with no further work—the object will return reasonable default values for all methods. We can thus recreating our example above. These examples assume you have statically imported the methods of the main Mockito class as explained above.

@Test
public void testCorrectPrizeMoney {
  final RaceCar dummyRaceCar = mock(RaceCar.class));

  final RaceManagerImpl raceManager = new RaceManagerImpl();
  assertThat(raceManager.getPrize(dummyRaceCar, "Test Driver", 1), is(BigInteger.valueOf(1000)));
}

Mockito Stub

To specify what a Mockito stub returns, you'll simply need to add a Mockito.when(…) followed by a OngoingStubbing.thenReturn(…) specifying the value you'd like back. Here's how it would work in the example above:

@Test
public void testCorrectPrizeMoney {
  final RaceCar dummyRaceCar = mock(RaceCar.class);
  final TaxStrategy stubTaxStrategy = mock(TaxStrategy.class);
  when(stubTaxStrategy.getTaxRate()).thenReturn(new BigDecimal("0.10"));

  final RaceManagerImpl raceManager = new RaceManagerImpl(stubTaxStrategy);
  assertThat(raceManager.getPrize(dummyRaceCar, "Test Driver", 1), is(BigInteger.valueOf(900)); //tax rate taken into account
}

If we wanted to verify that that RaceManagerImpl called TaxStrategy.getTaxRate(), we could use Mockito.verify(…) like this:

verify(stubTaxStrategy).getTaxRate();

This would throw an exception if the TaxStrategy.getTaxRate() method was not called once and only once. There are other variations of this method that allow you to check for multiple calls.

Mockito Mock

Creating an actual mock with Mockito requires only the additional step of specifying the appropriate method argument when you perform the method call for Mockito.when(…). Mockito will perform the magic to remember the argument(s) you used, so that the return value you provide for OngoingStubbing.thenReturn(…) will be returned only for the given input value(s) you gave.

@Test
public void testCorrectPrizeMoney {
  final RaceCar dummyRaceCar = mock(RaceCar.class));
  final PrizeStrategy mockPrizeStrategy = mock(PrizeStrategy.class);
  when(mockPrizeStrategy.getPrize(anyInt())).thenThrow(new IllegalArgumentException());
  doReturn(BigInteger.valueOf(1000)).when(mockPrizeStrategy).getPrize(1);
  doReturn(BigInteger.valueOf(500)).when(mockPrizeStrategy).getPrize(2);
  doReturn(BigInteger.valueOf(200)).when(mockPrizeStrategy).getPrize(3);
  final TaxStrategy stubTaxStrategy = mock(TaxStrategy.class);
  when(stubTaxStrategy.getTaxRate()).thenReturn(new BigDecimal("0.10");

  final RaceManagerImpl raceManager = new RaceManagerImpl(mockPrizeStrategy, stubTaxStrategy);
  assertThat(raceManager.getPrize(dummyRaceCar, "Test Driver", 1), is(BigInteger.valueOf(900)); //tax rate taken into account
}

Review

Gotchas

In the Real World

Think About It

Self Evaluation

Task

  1. Add a unit test to for BookerServicesImpl.
    • Use Mockito to mock the PublicationManager dependency.
  2. Add a unit test for PublicationManagerImpl.
    • Create a fake PublicationRepository without using Mockito to use as the dependency.

See Also

References

Resources

Copyright © 2015–2017 GlobalMentor, Inc. All Rights Reserved. Content may not be published or reproduced by any means for any purpose without permission. Version 2017-02-22.