What’s an assertion?
It’s a way to test an assumption in the code normally associated with an expected result, where we will compare it to the current outcome.
We all know a lot of different assertions: if it is null, equal, true and all its variations are negations: not null, not equal, false, and so on.
Adding assertions in the tests is that makes the test a test!
Assertion libraries for the win!
The unit test libraries support different assertions but are limited in the way few variations exist. Taking JUnit 5 as an example, it has the class Assertion with 8 main assertion targets, excluding its variations as the negation (not and false) and parameter types:
- array
- exceptions
- equals
- instanceOf
- iterable
- null
- timeout
- true
Because of this, new libraries that provide only assertion methods emerged to solve this gap. Tools like Truth, Hamcrest, and AssertJ provide extensive ways to assert different aspects providing different features.
This article will use AssertJ given the extensive assertion methods, constant development, and ability to extend its features.
Custom Assertion with AssertJ
At this point, I assume that you know about assertions and how to use them.
The case
Let’s imagine you have an entity within the following restrictions:
amount
where the minimum acceptable is 1.000 and the maximum is 40.000installments
where the minimum acceptable is 2 and the maximum is 48
The Simulation
class expresses these constraints:
public class Simulation {
@NotNull(message = "Amount cannot be empty")
@Min(value = 1000, message = "Amount must be equal or greater than $ 1.000")
@Max(value = 40000, message = "Amount must be equal or less than than $ 40.000")
private BigDecimal amount;
@NotNull(message = "Installments cannot be empty")
@Min(value = 2, message = "Installments must be equal or greater than 2")
@Max(value = 48, message = "Installments must be equal or less than 48")
private Integer installments;
}
The classical assertion
We can easily test the object data using the SoftAssertions example and the isBetween() assertion to check the amount
and installments
min and max constraints. PS: see how AssertJ is awesome in providing assertions like this?
There’s nothing wrong with a classic assertion like this, but when you need to validate the amount and installments, your code will have the constraint value, creating more efforts to change it later.
class SimulationTest {
@Test
void basicCheck() {
Simulation simulation = Simulation.builder().name("Elias").cpf("123456").email("elias@elias.com")
.amount(new BigDecimal(1000)).installments(48).insurance(false).build();
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(simulation.getName()).isEqualTo("Elias");
softly.assertThat(simulation.getCpf()).isNotEmpty();
softly.assertThat(simulation.getEmail()).isEqualTo("elias@elias.com");
softly.assertThat(simulation.getAmount()).usingComparator(BigDecimal::compareTo).isBetween(new BigDecimal(1000), new BigDecimal(40000));
softly.assertThat(simulation.getInstallments()).isBetween(2, 48);
softly.assertThat(simulation.getInsurance()).isFalse();
});
}
}
The custom assertion
It seems hard but it’s not! AssertJ provides the AbstractAsser class to create your assertion, with all the other ones handy.
The steps are simple, as we need to:
- Create the custom assertion class
- Add the required constructor
- Add the
assertThat
method - Implement the custom assertion methods
1. Create the custom assertion class
The first point is to create the custom assertion class, which we will call SimulationAssert
.
The class must extend the AbstractAssert
specifying two arguments: the class itself and to be able to chain the custom methods and the class under test, which is the Simulation
class.
public class SimulationAssert extends AbstractAssert<SimulationAssert, Simulation> {
}
2. Add the required constructor
An assertion always has the actual and expected results. In a custom assertion, we need to express the actual as the class under test in a constructor, calling the parent AbstractAssert
constructor.
public class SimulationAssert extends AbstractAssert<SimulationAssert, Simulation> {
protected SimulationAssert(Simulation actual) {
super(actual, SimulationAssert.class);
}
}
3. Add the assertThat method
The creation of the assertThat
method is necessary and the first point to assert the class under test. The method must be static having a parameter which is the class under test, and return its new class instance
// previous code ignored
public static SimulationAssert assertThat(Simulation actual) {
return new SimulationAssert(actual);
}
This is the assertThat
that will make available the custom method without losing the basic ones.
4. Implement the custom assertion methods
The custom methods have a pattern:
- will return, all the time, it’s class as we do in a Fluent Builder interface
- [optional] have a method parameter to specify any extra information
- a check of the class under test
isNotNull()
- the usage of the
failWithMessage()
method when the assertion doesn’t satisfy the requirements
Let’s create one custom assertion to validate the valid installments
. If you remember from the Simulation
entity, the allowed values are a minimum of 2 and a maximum of 48. The method will have an if statement where we will check the minimum and maximum values:
// previous code ignored
public SimulationAssert hasValidInstallments() {
isNotNull();
if (actual.getInstallments() < 2 || actual.getInstallments() > 48) {
failWithMessage("Installments must be must be equal or greater than 2 and equal or less than 48");
}
return this;
}
- line 3 shows that the method
hasValidInstallments()
return theSimulationAssert
class - line 4 has the
isNotNull()
check for the class under test - line 6 has the check for the min and max values
- line 7 will fail the test where we can set a custom message using the
failWithMessage(
) method - line 10 returns its class
You can do the same for the amount constraint in the entity class:
// previous code ignored
public SimulationAssert hasValidAmount() {
isNotNull();
var minimum = new BigDecimal("1.000");
var maximum = new BigDecimal("40.000");
if (actual.getAmount().compareTo(minimum) < 0 || actual.getAmount().compareTo(maximum) > 0) {
failWithMessage("Amount must be equal or greater than $ 1.000 or equal or less than than $ 40.000");
}
return this;
}
The usage in the test
Now comes the easiest part: the test creation!
class SimulationsCustomAssertionTest {
@Test
void simulationErrorAssertion() {
var simulation = Simulation.builder().name("John").cpf("9582728395").email("john@gmail.com")
.amount(new BigDecimal("1.500")).installments(5).insurance(false).build();
SimulationAssert.assertThat(simulation).hasValidInstallments();
SimulationAssert.assertThat(simulation).hasValidAmount();
}
}
- line 1 is the test class 🙂
- line 4 is the test method
- lines 5 and 6 have the Simulation object with some valid data
To use the custom assertion, instead of using the Assertion
s class from AssertJ, we need to use the custom assertion class SimulationAssert
. You will have access to all the custom-created assertions plus all the methods from the AbstractAssert.
So, lines 8 and 9 use the SimulationAssert
methods to assert the class under test (Simulation
) complies with the custom validations.
Of course, when the value of the attributes does not meet the validation we created the failWithMessage()
method will take place. Try it out!
How about the other fields to check?
If you need to validate the other fields of the Simulation object you can either mix the custom assertion with the AssertJ one. In the below example add an extra assert to check if the name
is the expected one, as you can see in line 8:
@Test
void simulationValidationAssertion() {
var simulation = Simulation.builder().name("John").cpf("9582728395").email("john@gmail.com")
.amount(new BigDecimal("1.500")).installments(5).insurance(false).build();
SimulationAssert.assertThat(simulation).hasValidInstallments();
SimulationAssert.assertThat(simulation).hasValidAmount();
Assertions.assertThat(simulation.getName()).isEqualTo("John");
}
You can also create another custom assertion method like hasName()
. If you would like to do so, the same pattern we learned will apply by adding the optional step: the parameter to check. This would be a possible implementation:
public SimulationAssert hasNameEqualsTo(String name) {
isNotNull();
if (!Objects.equals(actual.getName(), name)) {
failWithMessage("Expect the Simulation to have the name equals to %s", name);
}
return this;
}
Line 4 verifies if the name value of the Simulation
actual result is equal to the expected one. If not the failWithMessage()
will fail the test with the corresponding defined message.
You can now use the new custom method to check the name value without losing the class context.
@Test
void simulationValidationAssertion() {
var simulation = Simulation.builder().name("John").cpf("9582728395").email("john@gmail.com")
.amount(new BigDecimal("1.500")).installments(5).insurance(false).build();
SimulationAssert.assertThat(simulation).hasValidInstallments();
SimulationAssert.assertThat(simulation).hasValidAmount();
SimulationAssert.assertThat(simulation).hasNameEqualsTo("John");
}
The end
That’s all folks!
You can find a fully implemented and working example in the manage-data branch of the credit-api project, where you can see the:
- SimulationAssert class
- Test usage in the SimulationsCustomAssertionTest class