Mocks and stubs are useful tools for simplifying the process of writing unit tests. However, mocking too much might also be a problem. A test that uses the real dependencies is more real than a test that uses doubles and, consequently, is more prone to find real bugs. Therefore, we do not want to mock a dependency that should not be mocked. Imagine you are testing class A
, which depends on class B
. How do we know whether we should mock or stub B
or whether it is better to use the real, concrete implementation?
Pragmatically, developers often mock or stub the following types of dependencies:
- Dependencies that are too slow —If the dependency is too slow for any reason, it might be a good idea to simulate it. We do not want slow test suites. Therefore, I mock classes that deal with databases or web services. Note that I still do integration tests to ensure that these classes work properly, but I use mocks for all the other classes that depend on these slow classes.
- Dependencies that communicate with external infrastructure —If the dependency talks to (external) infrastructure, it may be too slow or too complex to set up the required infrastructure. So, I apply the same principle: whenever testing a class that depends on a class that handles external infrastructure, I mock the dependency (as we mocked the
IssuedInvoices
class when testing theInvoiceFilter
class). I then write integration tests for these classes. - Cases that are hard to simulate —If we want to force the dependency to behave in a hard-to-simulate way, mocks or stubs can help. A common example is when we would like the dependency to throw an exception. Forcing an exception might be tricky when using the real dependency but is easy to do with a stub.
On the other hand, developers tend not to mock or stub the following dependencies:
- Entities —Entities are classes that represent business concepts. They consist primarily of data and methods that manipulate this data. Think of the
Invoice
class or theShoppingCart
class. In business systems, entities commonly depend on other entities. This means, whenever testing an entity, we need to instantiate other entities.For example, to test aShoppingCart
, we may need to instantiateProduct
s andItem
s. One possibility would be to mock theProduct
class when the focus is to test theShoppingCart
. However, this is not something I recommend. Entities are classes that are simple to manipulate. Mocking them may require more work. Therefore, I prefer to never mock them. If my test needs three entities, I instantiate them.I make exceptions for heavy entities. Some entities require dozens of other entities. Think of a complexInvoice
class that depends on 10 other entities:Customer
,Product
, and so on. Mocking this complexInvoice
class may be easier. - Native libraries and utility methods —It is also not common to mock or stub libraries that come with our programming language and utility methods. For example, why would we mock
ArrayList
or a call toString.format
? Unless you have a very good reason, avoid mocking them. - Things that are simple enough —Simple classes may not be worth mocking. If you feel a class is too simple to be mocked, it probably is.
Interestingly, I always followed those rules, because they made sense to me. In 2018–2019, Spadini, myself, and colleagues performed a study to see how developers mock in the wild. Our findings were surprisingly similar to this list.
Let me illustrate with a code example. Consider a BookStore
class with the following requirement:
Given a list of books and their respective quantities, the program should return the total price of the cart.
If the bookstore does not have all the requested copies of the book, it includes all the copies it has in stock in the final cart and lets the user know about the missing ones.
The implementation (listing 6.16) uses a BookRepository
class to check whether the book is available in the store. If not enough copies are available, it keeps track of the unavailable ones in the Overview
class. For the available books, the store notifies BuyBookProcess
. In the end, it returns the Overview
class containing the total amount to be paid and the list of unavailable copies.
Listing 6.16 Implementation of BookStore
class BookStore {
private BookRepository bookRepository;
private BuyBookProcess process;
public BookStore(BookRepository bookRepository, BuyBookProcess process) ❶
{
this.bookRepository = bookRepository;
this.process = process;
}
private void retrieveBook(String ISBN, int amount, Overview overview) {
Book book = bookRepository.findByISBN(ISBN); ❷
if (book.getAmount() < amount) { ❸
overview.addUnavailable(book, amount - book.getAmount());
amount = book.getAmount();
}
overview.addToTotalPrice(amount * book.getPrice()); ❹
process.buyBook(book, amount); ❺
}
public Overview getPriceForCart(Map<String, Integer> order) {
if(order==null)
return null;
Overview overview = new Overview();
for (String ISBN : order.keySet()) { ❻
retrieveBook(ISBN, order.get(ISBN), overview);
}
return overview;
}
}
❶ We know we must mock and stub things, so we inject the dependencies.
❷ Searches for the book using its ISBN
❸ If the number of copies in stock is less than the number of copies the user wants, we keep track of the missing ones.
❹ Adds the available copies to the final price
❺ Notifies the buy book process
❻ Processes each book in the order
Let’s discuss the main dependencies of the BookStore
class:
- The
BookRepository
class is responsible for, among other things, searching for books in the database. This means the concrete implementation of this class sends SQL queries to a database, parses the result, and transforms it intoBook
classes. Using the concreteBookRepository
implementation in the test might be too painful: we would need to set up the database, ensure that it had the books we wanted persisted, clean the database afterward, and so on. This is a good dependency to mock. - The
BuyBookProcess
class is responsible for the process of someone buying a book. We do not know exactly what it does, but it sounds complex.BuyBookProcess
deserves its own test suite, and we do not want to mix that with theBookStore
tests. This is another good dependency to mock. - The
Book
class represents a book. The implementation ofBookStore
gets the books that are returned byBookRepository
and uses that information to know the book’s price and how many copies the bookstore has in stock. This is a simple class, and there is no need to mock it since it is easy to instantiate a concreteBook
. - The
Overview
class is also a simple, plain old Java object that stores the total price of the cart and the list of unavailable books. Again, there is no need to mock it. - The
Map<String,
Integer>
that thegetPriceForCart
receives as an input is aMap
object.Map
and its concrete implementationHashMap
are part of the Java language. They are simple data structures that also do not need to be mocked.
Now that we have decided what should be mocked and what should not be mocked, we write the tests. The following test exercises the behavior of the program with a more complex order.
Listing 6.17 Test for BookStore
, only mocking what needs to be mocked
@Test
void moreComplexOrder() {
BookRepository bookRepo = mock(BookRepository.class); ❶
BuyBookProcess process = mock(BuyBookProcess.class); ❶
Map<String, Integer> orderMap = new HashMap<>(); ❷
orderMap.put("PRODUCT-ENOUGH-QTY", 5); ❸
orderMap.put("PRODUCT-PRECISE-QTY", 10);
orderMap.put("PRODUCT-NOT-ENOUGH", 22);
Book book1 = new Book("PRODUCT-ENOUGH-QTY", 20, 11); // 11 > 5
when(bookRepo.findByISBN("PRODUCT-ENOUGH-QTY"))
.thenReturn(book1); ❹
Book book2 = new Book("PRODUCT-PRECISE-QTY", 25, 10); // 10 == 10
when(bookRepo.findByISBN("PRODUCT-PRECISE-QTY"))
.thenReturn(book2); ❹
Book book3 = new Book("PRODUCT-NOT-ENOUGH", 37, 21); // 21 < 22
when(bookRepo.findByISBN("PRODUCT-NOT-ENOUGH"))
.thenReturn(book3); ❹
BookStore bookStore = new BookStore(bookRepo, process); ❺
Overview overview = bookStore.getPriceForCart(orderMap);
int expectedPrice = ❻
5*20 + // from the first product
10*25 + // from the second product
21*37; // from the third product
assertThat(overview.getTotalPrice()).isEqualTo(expectedPrice);
verify(process).buyBook(book1, 5); ❼
verify(process).buyBook(book2, 10); ❼
verify(process).buyBook(book3, 21); ❼
assertThat(overview.getUnavailable())
.containsExactly(entry(book3, 1)); ❽
}
❶ As agreed, BookRepository and BuyBookProcess should be mocked.
❸ The order has three books: one where there is enough quantity, one where the available quantity is precisely what is requested in the order, and one where there is not enough quantity.
❹ Stubs the BookRepository to return the three books
❺ Injects the mocks and stubs into BookStore
❻ Ensures that the total price is correct
❼ Ensures that BuyBookProcess was called for three books with the right amounts
❽ Ensures that the list of unavailable books contains the one missing book
Could we mock everything? Yes, we could—but doing so would not make sense. You should only stub and mock what is needed. But whenever you mock, you reduce the reality of the test. It is up to you to understand this trade-off.
Leave a Reply