1. Overview
We, as programmers, often write tests to make sure that our code works as intended. One of the standard practices in testing is to use assertions.
When we want to verify multiple properties of an object, we could write a bunch of assertions to get the work done.
However, in this tutorial, weβll explore how to verify multiple properties in one single assert call.
2. Introduction to the Problem
In many cases, we need to check multiple properties of an object. Traditionally, this means writing separate assert statements for each property, which can make the code verbose and difficult to read.
However, a better way to do this is to use a single assert call for multiple properties. So next, letβs see how itβs done.
For a more straightforward demonstration, first, letβs look at a POJO class as an example:
class Product {
private Long id;
private String name;
private String description;
private boolean onSale;
private BigDecimal price;
private int stockQuantity;
// constructor with all properties is omitted
// getters and setters are omitted
}
The Product class has six properties. Letβs say weβve implemented a program to produce a Product instance. Usually, we compare the generated Product instance to an expected object to assert if the program works, such as assertEquals(EXPECTED_PRODUCT, myProgram.createProduct()).
However, in our program, the id and description are not predictable. In other words, we consider the program does the job correctly if we can verify that the rest four fields (name, onSale, price, and stockQuantity) hold the expected values.
Next, letβs create a Product object as the expected result:
Product EXPECTED = new Product(42L, "LG Monitor", "32 inches, 4K Resolution, Ideal for programmers", true, new BigDecimal("429.99"), 77);
For simplicity, we wonβt really implement a method to create a Product object. Instead, letβs simply create a Product instance to hold the required values, as our focus is on how to assert the four properties in one single statement:
Product TO_BE_TESTED = new Product(-1L, "LG Monitor", "dummy value: whatever", true, new BigDecimal("429.99"), 77);
So next, letβs see how to organize the assertions.
3. Using JUnit5βs assertAll()
JUnit is one of the most popular unit test frameworks. The latest version, JUnit 5, has brought many new features. For example, assertAll() is one of them.
JUnit 5βs assertAll() method takes a list of assertions, and all of them will be executed in a single call. Further, if any of the assertions fail, the test will fail, and all the failures will be reported.
Next, letβs group property assertions in one single assertAll() call:
assertAll("Verify Product properties",
() -> assertEquals(EXPECTED.getName(), TO_BE_TESTED.getName()),
() -> assertEquals(EXPECTED.isOnSale(), TO_BE_TESTED.isOnSale()),
() -> assertEquals(EXPECTED.getStockQuantity(), TO_BE_TESTED.getStockQuantity()),
() -> assertEquals(EXPECTED.getPrice(), TO_BE_TESTED.getPrice()));
As we can see, the assertAll() method groups four assertions in one call. Itβs worth mentioning that the price field is of type BigDecimal. We use assertEquals() to verify the BigDecimal objectβs value and scale.
Weβve achieved our goal. However, if we look at the code carefully, inside the assertAll() body, we still have four assertions, even though they are in the lambda expression format. Therefore, the code is still a bit verbose.
Next, letβs see other approaches to asserting these four properties in one call.
4. Using AssertJβs extracting() and containsExactly()
AssertJ is a powerful Java library that provides a fluent and intuitive API for writing assertions in tests. It provides the extracting() method that allows us only to extract our required propertiesβ values from an object. The extracted values are stored in a list. Then, AssertJ offers other methods to verify the list. For example, we can use containsExactly() to verify that the actual group contains exactly the given values in order and nothing else.
Next, letβ assemble extracting() and containsExactly():
assertThat(TO_BE_TESTED)
.extracting("name", "onSale", "stockQuantity", "price")
.containsExactly(EXPECTED.getName(), EXPECTED.isOnSale(), EXPECTED.getStockQuantity(), EXPECTED.getPrice());
As weβve seen, AssertJβs extracting() and containsExactly() allow us to write more concise and expressive assertions.
As the code above shows, passing property names as strings to the extracting() method is pretty straightforward. However, as the names are plain strings, they could contain typos. Moreover, if we renamed the properties, the test method still compiles without a problem. We wonβt see the problem until we run the test. Also, it might take some time to find the naming problem finally.
Therefore, AssertJ supports passing the getter method references instead of property names to extracting():
assertThat(TO_BE_TESTED)
.extracting(Product::getName, Product::isOnSale, Product::getStockQuantity,Product::getPrice)
.containsExactly(EXPECTED.getName(), EXPECTED.isOnSale(), EXPECTED.getStockQuantity(), EXPECTED.getPrice());
5. Using AssertJβs returns() and from()
AssertJ provides a rich set of assertions for various requirements. Weβve learned to use extracting() and containsExactly() to verify multiple properties in one assert call. In our example, weβd check four properties. However, we may want to verify ten properties in the real world. As the number of to-be-checked properties grows, the assertion line becomes difficult to read. Also, writing such a long assert line is error-prone.
Next, letβs see an alternative approach using AssertJβs returns() and from() methods. The usage is pretty straightforward: assertThat(ToBeTestedObject).returns(Expected, from(FunctionToGetTheValue)).
So, the returns() method verifies the object under test returns the Expected value from the given function FunctionToGetTheValue.
Next, letβs apply this approach to verify the Product object:
assertThat(TO_BE_TESTED)
.returns(EXPECTED.getName(), from(Product::getName))
.returns(EXPECTED.isOnSale(), from(Product::isOnSale))
.returns(EXPECTED.getStockQuantity(), from(Product::getStockQuantity))
.returns(EXPECTED.getPrice(), from(Product::getPrice));
As we can see, the code is fluent and easy to read. Further, we wonβt get lost even if we need to verify many properties.
Itβs worth mentioning that AssertJ offers the doesNotReturn() method to verify the from() result doesnβt match the expected value. Furthermore, we can use doesNotReturn() and returns() in the same assertion.
Finally, letβs write a one-line assertion that mixes the returns() and doesNotReturn() methods:
assertThat(TO_BE_TESTED)
.returns(EXPECTED.getName(), from(Product::getName))
.returns(EXPECTED.isOnSale(), from(Product::isOnSale))
.returns(EXPECTED.getStockQuantity(), from(Product::getStockQuantity))
.returns(EXPECTED.getPrice(), from(Product::getPrice))
.doesNotReturn(EXPECTED.getId(), from(Product::getId))
.doesNotReturn(EXPECTED.getDescription(), from(Product::getDescription));
6. Conclusion
Using a single assert call to test multiple properties provides many benefits, such as improved readability, less error-prone, better maintainability, and so on.
In this article, weβve learned three approaches to verifying multiple properties in one assert call through examples:
- JUnit5 β assertAll()
- AssertJ β extracting() and containsExactly()
- AssertJ β returns(), doesNotReturn(), and from()
