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 Installment
s 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
❻ Creates and persists the installment
To test this method, we need to check whether the newly created Installment
s are set with the right value and date. The question is, how can we get the Installment
s 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 Installment
s 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.
❺ 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. ArgumentCaptor
s 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 Installment
s, 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.
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.
Leave a Reply