Example 2: Observing the behavior of void methods

When a method returns an object, it is natural to think that assertions will check whether the returned object is as expected. However, this does not happen naturally in void methods. If your method does not return anything, what will you assert? It is even more complicated if what you need to assert stays within the method. As an example, the following method creates a set of Installments based on a ShoppingCart.

Listing 7.9 InstallmentGenerator

public class InstallmentGenerator {
 
  private InstallmentRepository repository;
 
  public InstallmentGenerator(InstallmentRepository repository) {          ❶
    this.repository = repository;
  }
 
  public void generateInstallments(ShoppingCart cart,
    ➥ int numberOfInstallments) {
    LocalDate nextInstallmentDueDate = LocalDate.now();                    ❷
 
    double amountPerInstallment = cart.getValue() / numberOfInstallments;  ❸
 
    for(int i = 1; i <= numberOfInstallments; i++) {                       ❹
      nextInstallmentDueDate =
        nextInstallmentDueDate.plusMonths(1);                              ❺
 
      Installment newInstallment =
        new Installment(nextInstallmentDueDate, amountPerInstallment);
      repository.persist(newInstallment);                                   ❻
    }
  }
}

❶ We can inject a stub of InstallmentRepository.

❷ Creates a variable to store the last installment date

❸ Calculates the amount per installment

❹ Creates a sequence of installments, one month apart

❺ Adds 1 to the month

❻ Creates and persists the installment

To test this method, we need to check whether the newly created Installments are set with the right value and date. The question is, how can we get the Installments easily? The Installment classes are instantiated within the method and sent to the repository for persistence, and that is it. If you know Mockito well, you know there is a way to get all the instances passed to a mock: an ArgumentCaptor. The overall idea is that we ask the mock, “Can you give me all the instances passed to you during the test?” We then make assertions about them. In this case, we can ask the repository mock whether all the Installments were passed to the persist method.

The test in listing 7.10 creates a shopping cart with value 100 and asks the generator for 10 installments. Therefore, it should create 10 installments of 10.0 each. That is what we want to assert. After the method under test is executed, we collect all the installments using an ArgumentCaptor. See the calls for capture() and getAllValues(). With the list available, we use traditional AssertJ assertions.

Listing 7.10 Tests for InstallmentGenerator using ArgumentCaptor

public class InstallmentGeneratorTest {
 
    @Mock private InstallmentRepository repository;                 ❶
 
    @Test
    void checkInstallments() {
 
      InstallmentGenerator generator =
        new InstallmentGenerator(repository);                       ❷
 
      ShoppingCart cart = new ShoppingCart(100.0);
      generator.generateInstallments(cart, 10);                     ❸
 
      ArgumentCaptor<Installment> captor =
        ArgumentCaptor.forClass(Installment.class);                 ❹
 
 
      verify(repository,times(10)).persist(captor.capture());       ❺
      List<Installment> allInstallments = captor.getAllValues();    ❺
 
      assertThat(allInstallments)
          .hasSize(10)
          .allMatch(i -> i.getValue() == 10);                       ❻
 
      for(int month = 1; month <= 10; month++) {                    ❼
        final LocalDate dueDate = LocalDate.now().plusMonths(month);
        assertThat(allInstallments)
            .anyMatch(i -> i.getDate().equals(dueDate));
      }
    }
}

❶ Creates a mock of the repository

❷ Instantiates the class under test, passing the mock as a dependency

❸ Calls the method under test. Note that the method returns void, so we need something smarter to assert its behavior.

❹ Creates an ArgumentCaptor

❺ Using the captor, we get all the installments passed to the repository.

❻ Asserts that the installments are correct. All of them should have a value of 10.0.

❼ Also asserts that the installments should be one month apart

The ArgumentCaptor makes writing the test possible. ArgumentCaptors are handy whenever we test methods that return void.

If we apply the idea of simplicity, you may wonder if there is a way to avoid the ArgumentCaptor. It would be much simpler if there were a “get all generated installments” method. If we make the generateInstallments method return the list of all newly generated Installments, the test becomes even simpler. The change required in InstallmentGenerator is small: as all we need to do is keep track of the installments in a list. The following listing shows the new implementation.

Listing 7.11 InstallmentGenerator returning the list of installments

public List<Installment> generateInstallments(ShoppingCart cart,
  ➥ int numberOfInstallments) {
 
  List<Installment> generatedInstallments = new ArrayList<Installment>(); ❶
 
  LocalDate nextInstallmentDueDate = LocalDate.now();
 
  double amountPerInstallment = cart.getValue() / numberOfInstallments;
 
  for(int i = 1; i <= numberOfInstallments; i++) {
    nextInstallmentDueDate = nextInstallmentDueDate.plusMonths(1);
 
    Installment newInstallment =
      new Installment(nextInstallmentDueDate, amountPerInstallment);
    repository.persist(newInstallment);
 
    generatedInstallments.add(newInstallment);                            ❷
 
  }
 
  return generatedInstallments;                                           ❸
}

❶ Creates a list that will keep track of all the generated installments

❷ Stores each of the generated installments

❸ Returns the list of installments

Now we can avoid the ArgumentCaptor completely in the test code.

Listing 7.12 InstallmentGeneratorTest without the ArgumentCaptor

public class InstallmentGeneratorTest {
 
    @Mock
    private InstallmentRepository repository;
 
    @Test
    void checkInstallments() {
 
      ShoppingCart cart = new ShoppingCart(100.0);
      InstallmentGenerator generator =
        new InstallmentGenerator(repository);
 
      List<Installment> allInstallments =
        generator.generateInstallments(cart, 10);   ❶
 
      assertThat(allInstallments)
          .hasSize(10)
          .allMatch(i -> i.getValue() == 10);       ❷
 
      for(int month = 1; month <= 10; month++) {
        final LocalDate dueDate = LocalDate.now().plusMonths(month);
        assertThat(allInstallments)
            .anyMatch(i -> i.getDate().equals(dueDate));
      }
    }
}

❶ The method under test returns the list of installments. No need for ArgumentCaptor.

❷ Same assertions as before

Again, do not take this example literally. Just remember that small design changes that improve your testability are fine. Sometimes it can be hard to tell whether a change will make the code design bad. Try it, and if you don’t like it, discard it. Pragmatism is key.


Comments

Leave a Reply

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