JVM Advent

The JVM Programming Advent Calendar

Assert with Grace: Custom Assertions for Cleaner Code

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.000
  • installments 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:

  1. Create the custom assertion class
  2. Add the required constructor
  3. Add the assertThat method
  4. 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 the SimulationAssert 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 Assertions 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:

Author: Elias Nogueira

Next Post

Previous Post

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2024 JVM Advent | Powered by steinhauer.software Logosteinhauer.software

Theme by Anders Norén