Stubbing dependencies

Let’s learn how to use Mockito and set up stubs with a practical example. Suppose we have the following requirement:

The program must return all the issued invoices with values smaller than 100. The collection of invoices can be found in our database. The class IssuedInvoices already contains a method that retrieves all the invoices.

The code in listing 6.1 is a possible implementation of this requirement. Note that IssuedInvoices is a class responsible for retrieving all the invoices from a real database (for example, MySQL). For now, suppose it has a method all() (not shown) that returns all the invoices in the database. The class sends SQL queries to the database and returns invoices. You can check the (naive) implementation.

Listing 6.1 InvoiceFilter class

import java.util.List;
import static java.util.stream.Collectors.toList;
 
public class InvoiceFilter {
 
  public List<Invoice> lowValueInvoices() {
 
    DatabaseConnection dbConnection = new DatabaseConnection();        ❶
    IssuedInvoices issuedInvoices = new IssuedInvoices(dbConnection);  ❶
 
    try {
      List<Invoice> all = issuedInvoices.all();                        ❷
 
      return all.stream()
              .filter(invoice -> invoice.getValue() < 100)
              .collect(toList());                                      ❸
    } finally {
      dbConnection.close();                                            ❹
    }
  }
}

❶ Instantiates the IssuedInvoices dependency. It needs a DatabaseConnection, so we also instantiate one of those.

❷ Gets all the invoices from the database

❸ Picks all the invoices with a value smaller than 100

❹ Closes the connection with the database. You would probably handle it better, but this is here to remind you of all the things you need to handle when dealing with databases.

Without stubbing the IssuedInvoices class, testing the InvoiceFilter class means having to set up a database. It also means having invoices in the database so the SQL query can return them. This is a lot of work, as you can see from the (simplified) test method in listing 6.2, which exercises InvoiceFilter together with the concrete IssuedInvoices class and the database. Because the tests need a populated database up and running, we first create a connection to the database and clean up any old data it may contain. Then, in the test method, we persist a set of invoices to the database. Finally, when the test is over, we close the connection with the database, as we do not want hanging connections.

Listing 6.2 Tests for InvoiceFilter

public class InvoiceFilterTest {
  private IssuedInvoices invoices;
  private DatabaseConnection dbConnection;
 
  @BeforeEach                                          ❶
  public void open() {
    dbConnection = new DatabaseConnection();
    invoices = new IssuedInvoices(dbConnection);
 
    dbConnection.resetDatabase();                      ❷
  }
 
  @AfterEach                                           ❸
  public void close() {
    if (dbConnection != null)
      dbConnection.close();                            ❹
  }
 
  @Test
  void filterInvoices() {
    Invoice mauricio = new Invoice("Mauricio", 20);    ❺
    Invoice steve = new Invoice("Steve", 99);          ❻
    Invoice frank = new Invoice("Frank", 100);         ❻
    invoices.save(mauricio);                           ❼
    invoices.save(steve);                              ❼
    invoices.save(frank);                              ❼
 
    InvoiceFilter filter = new InvoiceFilter();        ❽
 
    assertThat(filter.lowValueInvoices())
        .containsExactlyInAnyOrder(mauricio, steve);   ❾
  }
}

❶ BeforeEach methods are executed before every test method.

❷ Cleans up the tables to make sure old data in the database does not interfere with the test

❸ AfterEach methods are executed after every test method.

❹ Closes the database connection after every test

❺ Creates in-memory invoices as we have been doing so far

❻ 99 and 100, boundary testing!

❼ However, we must persist them in the database!

❽ Instantiates InvoiceFilter, knowing it will connect to the database

❾ Asserts that the method only returns the low-value invoices

NOTE Did you notice the assertThat...containsExactlyInAnyOrder assertion? This ensures that the list contains exactly the objects we pass, in any order. Such assertions do not come with JUnit 5. Without AssertJ, we would have to write a lot of code for that assertion to happen. You should get familiar with AssertJ’s assertions; they are handy.

This is a small example. Imagine a larger business class with a much more complex database structure. Imagine that instead of persisting a bunch of invoices, you need to persist invoices, customers, items, shopping carts, products, and so on. This can become tedious and expensive.

Let’s rewrite the test. This time we will stub the IssuedInvoices class and avoid the hassle with the database. First, we need a way to inject the InvoiceFilter stub into the class under test. Its current implementation creates an instance of IssuedInvoices internally (see the first lines in the lowValueInvoices method). This means there is no way for this class to use the stub during the test: whenever this method is invoked, it instantiates the concrete database-dependent class.

We must change our production code to make testing easier (get used to the idea of changing the production code to facilitate testing). The most direct way to do this is to have IssuedInvoices passed in as an explicit dependency through the class constructor, as shown in listing 6.3. The class no longer instantiates the DatabaseConnection and IssuedInvoices classes. Rather, it receives IssuedInvoices via constructor. Note that there is no need for the DatabaseConnection class to be injected, as InvoiceFilter does not need it. This is good: the less we need to do in our test code, the better. The new implementation works for both our tests (because we can inject an IssueInvoices stub) and production (because we can inject the concrete IssueInvoices, which will go to the database, as we expect in production).

Listing 6.3 InvoiceFilter class receiving IssueInvoices via constructor

public class InvoiceFilter {
 
  private final IssuedInvoices issuedInvoices;            ❶
 
  public InvoiceFilter(IssuedInvoices issuedInvoices) {   ❷
    this.issuedInvoices = issuedInvoices;
  }
 
  public List<Invoice> lowValueInvoices() {
    List<Invoice> all = issuedInvoices.all();             ❸
 
    return all.stream()
        .filter(invoice -> invoice.getValue() < 100)
        .collect(toList());
  }
}

❶ Creates a field in the class to store the dependency

❷ IssuedInvoices is now passed in the constructor.

❸ We no longer instantiate the IssuedInvoices database class. We received it as a dependency, and we use it.

Let’s change our focus to the unit test of InvoiceFilter. The test is very similar to the one we wrote earlier, but now we do not handle the database. Instead, we configure the IssuedInvoices stub as shown in the next listing. Note how easy it is to write this test: full control over the stub enables us to try different cases (even exceptional ones) quickly.

Listing 6.4 Tests for InvoiceFilter, stubbing IssuedInvoices

public class InvoiceFilterTest {
 
  @Test
  void filterInvoices() {
    IssuedInvoices issuedInvoices = mock(IssuedInvoices.class);     ❶
 
    Invoice mauricio = new Invoice("Mauricio", 20);                 ❷
    Invoice steve = new Invoice("Steve", 99);                       ❷
    Invoice frank = new Invoice("Frank", 100);                      ❷
    List<Invoice> listOfInvoices = Arrays.asList(mauricio, steve, frank);
 
    when(issuedInvoices.all()).thenReturn(listOfInvoices);          ❸
 
    InvoiceFilter filter = new InvoiceFilter(issuedInvoices);       ❹
 
    assertThat(filter.lowValueInvoices())
        .containsExactlyInAnyOrder(mauricio, steve);                ❺
  }
}

❶ Instantiates a stub for the IssuedInvoices class, using Mockito’s mock method

❷ Creates invoices as we did before

❸ Makes the stub return the predefined list of invoices if all() is called

❹ Instantiates the class under test, and passes the stub as a dependency (instead of the concrete database class)

❺ Asserts that the behavior is as expected

NOTE This idea of classes not instantiating their dependencies by themselves but instead receiving them is a popular design technique. It allows us to inject mocks and also makes the production code more flexible. This idea is also known as dependency injection. If you want to dive into the topic, I suggest Dependency Injection: Principles, Practices, and Patterns by Steven van Deursen and Mark Seemann (2019).

Note how we set up the stub using Mockito’s when() method. In this example, we tell the stub to return a list containing mauriciofrank, and steve, the three invoices we instantiate as part of the test case. The test then invokes the method under test, filter.lowValueInvoices(). Consequently, the method under test invokes issuedInvoices.all(). However, at this point, issuedInvoices is a stub that returns the list with the three invoices. The method under test continues its execution and returns a new list with only the two invoices that are below 100, causing the assertion to pass.

Besides making the test easier to write, stubs also made the test class more cohesive and less prone to change if something other than InvoiceFilter changes. If IssuedInvoices changes—or, more specifically, if its contracts change—we may have to propagate it to the tests of InvoiceFilter, too. Our discussion of contracts also makes sense when talking about mocks. Now InvoiceFilterTest only tests the InvoiceFilter class. It does not test the IssuedInvoices class. IssuedInvoices deserves to be tested, but in another place, using an integration test.

A cohesive test also has fewer chances of failing for another reason. In the old version, the filterInvoices test could fail because of a bug in the InvoiceFilter class or a bug in the IssuedInvoices class (imagine a bug in the SQL query that retrieves the invoices from the database). The new tests can only fail because of a bug in InvoiceFilter, never because of IssuedInvoices. This is handy, as a developer will spend less time debugging if this test fails. Our new approach for testing InvoiceFilter is faster, easier to write, and more cohesive.

NOTE This part of the not focus on systematic testing. But that is what you should do, regardless of whether you are using mocks. Look at the filterInvoices test method. Its goal is to filter invoices that are below 100. In our (currently only) test case, we ensure that this works, and we even exercise the 100 boundary. You may want to exercise other cases, such as empty lists, or lists with a single element, or other test cases that emerge during specification-based and structural testing but you should remember all the techniques discussed.

In a real software system, the business rule implemented by InvoiceFilter would probably be best executed in the database. A simple SQL query would do the job with a much better performance. Try to abstract away from this simple example: whenever you have a dependency that is expensive to use during testing, stubs may come in handy.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *