Test doubles - mocks, stubs and the others

on waitingforcode.com

Test doubles - mocks, stubs and the others

Mocks ? Spies ? These words seem to be clear for the most test-aware developers. They're related to common concept called test doubles.

This post is divided in 6 short parts. Each of them describes one of test doubles. It starts with maybe the most known one - mocks. The other parts present, in order: dummy, stub, spy, mock and fake objects.

Test double is an object or a function destined to be used in tests. The difference between it and production object/function is that test double only imitates production's behavior. Let's take an example of a method returning the last order from the database. In our test, if we don't want to check the correctness of this function at given test, we can simply tell: when calling getLastOrder() return the object Order (constructed manually in the test).

Dummy

The simplest test double is called dummy. This kind of object implements interface needed to execute given test but doesn't carry about the implementation details. It means that dummy object can simply define empty methods or methods returning null because these methods are not intended to be used in the test.

@Test
public void should_show_dummy_object_behavior() {
  Order dummyOrder = () -> null;

  // We create dummy object because the test is focused on checking if
  // new orders are successfully added to DailyOrders - 
  // it doesn't matter if Order has a total cost or not
  DailyOrders dailyOrders = new DailyOrders();
  dailyOrders.addOrder(dummyOrder);

  assertThat(dailyOrders.countOrders()).isEqualTo(1);
}

Stub

The role of stubs consists on creating test-specific objects providing expected inputs for tested methods. If given method is not expected to be used in the test, its implementation can be ignored exactly as in the case of dummies:

@Test
public void should_show_stubs_in_test() {
  // stubs are similar to dummy object but the difference is that
  // some implemented methods do matter because they are used directly
  // in test.
  Order stubOrder = () -> new BigDecimal(5);

  DailyOrders dailyOrders = new DailyOrders(stubOrder, stubOrder);

  assertThat(dailyOrders.getTotalCost()).isEqualTo(BigDecimal.TEN);
}

Spy

Spies are a special category of stubs able to maintain a state. The state is used further in test assertions. The state can be used to, for example, check if correct value was passed to the method or if given method was invoked expected number of times.

@Test
public void should_show_spy_in_tests() {
  OrderSpy spiedOrder = new OrderSpy("5");

  DailyOrders dailyOrders = new DailyOrders(spiedOrder);

  assertThat(dailyOrders.getTotalCost()).isEqualTo(new BigDecimal(6));
  assertThat(spiedOrder.initialCost).isEqualTo("5");
}

private static class OrderSpy implements Order {

  private final String initialCost;

  private BigDecimal totalCost;

  public OrderSpy(String cost) {
    this.initialCost = cost;
    this.totalCost = new BigDecimal(cost);
  }

  @Override
  public BigDecimal getTotalCost() {
    return totalCost.add(BigDecimal.ONE);
  }
}

Mock

Probably the most popular test doubles are mocks. Unlike previously described objects, mocks focus on different aspects. Instead of taking care of state, they focus on behavior. So not only they define what should be returned when given method is called, but they also check if this method is really invoked by tested method. The checks are also called expectations.

Generally, tests with mocks are easily distinguishable thanks to the existence of 3 steps:

  1. Mock creation
  2. Behavior definition
  3. Expectations check

Below you can find these 3 steps:

@Test
public void should_show_mocks_in_test_through_mockito() {
  // 1. Create a mock
  Order mockedOrder = EasyMock.mock(Order.class);
  // 2. Define its behavior
  expect(mockedOrder.getTotalCost()).andReturn(new BigDecimal(5));
  replay(mockedOrder);

  DailyOrders dailyOrders = new DailyOrders(mockedOrder);

  assertThat(dailyOrders.getTotalCost()).isEqualTo(new BigDecimal(5));
  // 3. Check if was called
  EasyMock.verify(mockedOrder);
  // Conceptually, the difference between mock and stub is that mock focuses more on behavior
  // while stubs on state. This distinction is visible in the last step when we check if
  // fake method was called
}

A special kind of mocks are partial mocks. As the name indicates, we can use it to imitate the behavior of only some methods.

Fake

Fake objects going further than dummies because they implement near-production code used in tests. A great example helping to understand that are Spring services with CRUD operations on given object (let's say Order). Production implementation will make these operations on real database. But the use of these components in tests could be difficult. It's the reason why we can prefer to use fake objects, i.e. objects with much simpler implementation than the production one. In the case of our OrderService, the implementation could, for example, create manually n orders and operate on them in memory instead of in real database.

@Test
public void should_show_fake_in_tests() {
  OrderService fakeOrderService = (LocalDate localDate) -> {
    // Imagine that this data should be in the database
    Map<LocalDate, List<Order>> ordersByDate = new HashMap<>();
    LocalDate yesterday = LocalDate.now().minusDays(1L);
    LocalDate today = LocalDate.now();
    LocalDate tomorrow = LocalDate.now().plusDays(1);
    ordersByDate.put(yesterday, Arrays.asList(new OrderImpl("5"), new OrderImpl("10")));
    ordersByDate.put(today, Arrays.asList(new OrderImpl("15"), new OrderImpl("20")));
    ordersByDate.put(tomorrow, Arrays.asList(new OrderImpl("25"), new OrderImpl("30")));

    return ordersByDate.get(localDate);
  };
  DailyOrders dailyOrders = new DailyOrders();
  dailyOrders.orderService = fakeOrderService;

  BigDecimal totalCostFromDate = dailyOrders.getTotalCostFromDate(LocalDate.now());

  assertThat(totalCostFromDate).isEqualTo(new BigDecimal("35"));
}

private interface OrderService {
  List<Order> getOrdersFromDate(LocalDate date);
}

// Imagine that OrderImpl is the Order implementation used in production code
private static class OrderImpl implements Order {

  private BigDecimal cost;

  public OrderImpl(String cost) {
    this.cost = new BigDecimal(cost);
  }

  @Override
  public BigDecimal getTotalCost() {
    return cost;
  }
}

Even if they look similar, test doubles all have their use cases. Some of them are used just to create not-null values, the others are more detailes since they return an exact value and even are checked against the number of invocations in tested method.

Share on: