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

Looking for a better data engineering position and skills?
You have been working as a data engineer but feel stuck? You don't have any new challenges and are still writing the same jobs all over again? You have now different options. You can try to look for a new job, now or later, or learn from the others! "Become a Better Data Engineer" initiative is one of these places where you can find online learning resources where the theory meets the practice. They will help you prepare maybe for the next job, or at least, improve your current skillset without looking for something else.
๐ I'm interested in improving my data engineering skillset
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:
- Mock creation
- Behavior definition
- 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.