Write your own mock
We all know mocking libraries like Mockito or Mockk to mock classes in our unit tests. They can be convenient to mock I/O with external systems by replacing the boundary classes (aka. DAO = Data Access Objects) with mock objects. That way we do not require a full-blown simulator of that external system. However, mocking using these libraries also has some drawbacks. One way to avoid these drawbacks is to write your own mocks.
Write your own Mocks
Using mock libraries has some drawbacks. To name a few:
-
Encapsulation breaking: when declaring the method call on the mock we need to know which methods are invoked by the class we are testing. This breaks encapsulation. We would prefer just to describe some pre- and postconditions and test against the API of the class.
-
Checking pre- and postconditions usually requires state, which is often not possible because that is hidden behind the class that we mock, and mocks do not usually have state.
-
None of the code of the class being mocked is tested.
-
Mock code can be hard to read. That makes it a good practice to mock as little as possible, to keep the test code readable and cover as much as possible of the main code. So usually mock only the boundary classes.
However, when writing your own mock classes most, if not all, of these drawbacks can be avoided. The code examples below illustrate this.
Main code
public class Order {
private final UUID id;
private String description;
public Order(UUID id) {
this.id = id;
}
public UUID getId() {
return id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
public class OrderDao {
public Order findOrder(UUID id) {
return null; // Normally some code that queries the database, now not relevant so return null.
}
public void persist(Order order) {
validate(order);
// Normally here some code that actually persist the object in the database, now not relevant.
}
private void validate(Order order) {
if (order.getId() == null) {
throw new IllegalArgumentException("Order.id must not be null.");
}
}
}
public class OrderService {
private final OrderDao dao;
public OrderService(OrderDao dao) {
this.dao = dao;
}
@Transactional
public Order findOrder(UUID orderId) {
return dao.findOrder(orderId);
}
@Transactional
public Order findOrCreateOrder(UUID orderId) {
var order = dao.findOrder(orderId);
if (order == null) {
order = new Order(orderId);
dao.persist(order);
}
return order;
}
}
Test code with Mockito
public class OrderServiceTest {
@Test
void findOrCreateNewOrder_withMockito() {
var dao = Mockito.mock(OrderDao.class);
// Encapsulation breaking: we need to know which methods the service method will invoke.
Mockito.when(dao.findOrder(any())).thenReturn(null);
Mockito.doNothing().when(dao).persist(any());
var service = new OrderService(dao);
var id = UUID.fromString("f81d4fae-7dec-11d0-a765-00a0c91e6bf6");
// Assert pre-condition, but is not really testing anything.
assertNull(service.findOrder(id));
var order = service.findOrCreateOrder(id); // OrderDao.validate(Order) method is now NOT tested
assertEquals(id, order.getId());
// Assert post-condition: not possible to assert state, ie. whether object has really been persisted.
}
}
Test code with own mock
public class OrderServiceTest {
@Test
void findOrCreateNewOrder_withOwnMock() {
var dao = new OrderDaoMock();
var service = new OrderService(dao);
var id = UUID.fromString("f81d4fae-7dec-11d0-a765-00a0c91e6bf6");
// Assert precondition.
assertNull(service.findOrder(id));
var order = service.findOrCreateOrder(id); // OrderDao.validate(Order) method is now ALSO tested
assertEquals(id, order.getId());
// Assert postcondition: assert state, ie. object has actually been persisted.
assertEquals(id, service.findOrder(id).getId());
}
}
public class OrderDaoMock extends OrderDao {
public Map<UUID, Order> ordersById = new HashMap<>();
@Override
public Order findOrder(UUID id) {
return ordersById.get(id);
}
@Override
public void persist(Order order) {
ordersById.put(order.getId(), order);
}
}
Conclusion
As you can see the test code with an own mock is more readable, does not break encapsulation, does have higher test coverage (OrderDao.validation(Order)
is also covered), and can assert pre- and postconditions.
The only penalty we have is that we need to write our own (very simple) mock class.