Make your tests more readable using AssertJ and BDD syntax

Make your tests more readable using AssertJ and BDD syntax

It must be green ok? — Ruby Rhod

I love writing tests! I get a dopamine boost every time I see those green tests in my reports. Every test failing is a tiny puzzle to solve and you get a green result as soon as you find the solution. Supergreen!

But I know that writing and reading tests can be a chore for some devs. I believe this can be improved by making the tests easier to write and turning those long and boring succession of assertEquals more meaningful and fluent to read.

Remember that a developers spends more time reading code than writing it!

With those tips, reading your tests will be a breeze and your teammates will thank you for it!

Use AssertJ instead of Junit assertions

If you use Junit assertions in your tests, chances are you are mainly using the following assertions :

assertEquals(expected, actual);
assertTrue(condition);
assertFalse(condition);
assertNull(actual);

Most of the time I see tests using assertEquals() for anything like assertEquals(true, someList.size() == 5); 😱

A common pitfall of using Junit assertEquals() and it's 2 parameters is that you can easily invert the expected value with the actual value if you are not paying attention. As long as the test is green you won't see the mistake, but if the test fail, the error message will be misleading!

Example : let's assume our method generateHash() is supposed to return a 35 chars string, but is failing :

    @Test
    void testHashMethod() {
        String actual = underTest.generateHash("some string");
        assertEquals(actual.length(), 35); // Wrong order!
    }

The error message will be misleading since the expected value is the result of the method:

org.opentest4j.AssertionFailedError: 
Expected :32
Actual   :35

If you are using intelliJ, you may have hints on which parameter is which :

Capture d’écran 2021-11-01 à 17.19.51.png

but you won't have this hints during a code review on github for example. 🙈

Enter AssertJ

AssertJ comes with a variety of assertions that can be chained together and are specific to the type of your "actual" variable.

I've compiled some of my favorites assertions for various types. Some of those tests are redundant just to showcase the possibilities :

// actual is a String
assertThat(actual).isEqualTo("MD55AC749FBEEC93607FC28D666BE85E73A");
assertThat(actual).hasSize(35)
    .contains("A") // You can chain assertions if you want
    .startsWith("MD5")
    .isUpperCase()
    .isNotBlank();

// actual and expected are collections of String
assertThat(actual).hasSize(2);
assertThat(actual).contains("first value");
assertThat(actual).first().isEqualTo("first value"); // Get the first element
assertThat(actual).containsExactly("first value", "second value");
assertThat(actual).containsExactlyInAnyOrder("second value", "first value");
assertThat(actual).containsExactlyInAnyOrderElementsOf(expected);
assertThat(actual).doesNotContain("forbidden value", "another value");
// Make an assertion on all elements of the collection :
assertThat(actual).allSatisfy(s -> assertThat(s).doesNotStartWith("foo"));

// actual is an int
assertThat(actual).isEqualTo(42);
assertThat(actual).isGreaterThan(40);
assertThat(actual).isPositive();
assertThat(actual).isIn(0, 2, 42, 1337);
assertThat(actual).isBetween(30, 50);

// actual is an Instant
assertThat(actual).isAfterOrEqualTo(Instant.now())
    .isBetween(pastInstant, futureInstant)
    .isCloseTo(Instant.now(), within(1, ChronoUnit.MINUTES));

// Test an exception
Throwable thrown = catchThrowable(() -> List.of(1, 2, 3).get(4));
assertThat(thrown).isInstanceOf(ArrayIndexOutOfBoundsException.class);

As you can see, the assertion always starts with assertThat(actual) which exists for any type. This as 3 advantages :

  1. You can't invert actual and expected values

  2. The assertion reads like a normal sentence in English, allowing you to make your test very explicit on what you intend to verify

  3. You can easily discover which assertion is available for your type of data with your IDE auto completion! For everything else, the documentation has plenty of examples.

One last note on AssertJ : error message are often more explicit than Junit assertions. For example if your list contain one more element than expected :

// assertJ (using assertThat(actual).containsExactly("first value", "second value"); )
java.lang.AssertionError: 
Expecting:
  ["first value", "second value", "third value"]
to contain exactly (and in same order):
  ["first value", "second value"]
but some elements were not expected:
  ["third value"]

// junit assertion (using assertLinesMatch(expected, actual); )
org.opentest4j.AssertionFailedError: more actual lines than expected: 1

If you are using Spring Boot, AssertJ is already bundled into spring-boot-starter-test along with Mockito and other testing libs. Good news!

Use the same pattern for all your tests

Define a pattern for you tests and stick to it in every test class. This makes your tests more predictable for anyone of your team reading it.

I tend to use the following pattern, suggested by Robert C. Martin in Clean Code :

    @Test
    void someTest() {
        // Given
            // prepare your expected values, program mocks
        // When
            // call the method under test
        // Then
            // put all you assertions here, verify your mocks
    }

Which leads me nicely to my next point....

Use BDD syntax

Did you know that both Mockito and AssertJ offers an alternative syntax ?

For AssertJ you can use BDDAssertions.then to replace your assertThat():

import static org.assertj.core.api.BDDAssertions.then;
(...)
then(actual).isEqualTo(expected);// Everything else is the same

For Mockito you can use BDDMockito.given instead of when() :

import static org.mockito.BDDMockito.given;
(...)
// instead of :
when(myMock.someMethod(any())).thenReturn("Some mocked value");
// use this :
given(myMock.someMethod(any())).willReturn("Some mocked value");

This will turn something like this :

@Test
    void someTest() {
        when(myMock.someMethod(any())).thenReturn("some string");
        var expected = "MD55AC749FBEEC93607FC28D666BE85E73A";

        var actual = underTest.generateHash(10);

        assertThat(actual).isEqualTo(expected);
        verify(myMock).someMethod(10);
    }

into this :

@Test
    void someTest() {
        // Given
        given(myMock.someMethod(any())).willReturn("some string");
        var expected = "MD55AC749FBEEC93607FC28D666BE85E73A";

        // When
        var actual = underTest.generateHash(10);

        // Then
        then(actual).isEqualTo(expected);
        verify(myMock).someMethod(10);
    }

Beware that BDDMockito also have a then() method, I don't recommend using both in the same class.

I found this methods fits nicely inside the "Given, When, Then" pattern with no downsides since it's a simple alias of the original methods.

Use soft assertions

If your test method contains multiples assertions but one of them fails, the test stops at the first failure but don't run the other assertions. If you have 2 assertions failing, you won't know it until you have fixed the first assertion, run the test again and get the second failure. Such a waste of time!

Some devs will argue that a test method should only have a single assertion. I prefer the concept of "one test should only test one concept". For example if the method under test returns a list I could have assertions on the size of the list, the values inside the list and their orders, test the absence of some values etc. I write all those assertions in the same test method.

Soft assertions allows you to run multiples assertions without stopping at the first failure. If anything fails you will have a report on which assertions failed.

    @Test
    void testList() {
        var actual = List.of("first value", "second value", "third value");
        var expected = List.of("first value", "second value");

        var softly = new BDDSoftAssertions();
        softly.then(actual).contains("first value");
        softly.then(actual).first().isEqualTo("first value");
        softly.then(actual).containsExactly("first value", "second value"); // fails
        softly.then(actual).containsExactlyInAnyOrderElementsOf(expected); // fails
        softly.then(actual).doesNotContain("forbidden value", "another value");
        softly.then(actual).allSatisfy(s -> assertThat(s).doesNotStartWith("foo"));
        softly.assertAll();
    }

Here is the result :

org.assertj.core.error.AssertJMultipleFailuresError: 
Multiple Failures (2 failures)
-- failure 1 --
Expecting:
  ["first value", "second value", "third value"]
to contain exactly (and in same order):
  ["first value", "second value"]
but some elements were not expected:
  ["third value"]

at SimpleTest.testList(SimpleTest.java:49)
-- failure 2 --
Expecting:
  ["first value", "second value", "third value"]
to contain exactly in any order:
  ["first value", "second value"]
but the following elements were unexpected:
  ["third value"]

at SimpleTest.testList(SimpleTest.java:50)

⚠️ Warning : do not forget the line with assertAll() otherwise your test will be green but no assertions are actually run!

There are other ways to use the Soft assertions in the documentation that allows you to omit the assertAll() if you want.

Here is my favorite way:

@ExtendWith(SoftAssertionsExtension.class) // <- use the extension
class YourTestClass {

    @Test
    void someTest(BDDSoftAssertions softly) { // <- insert a soft Assertion (here in BDD form) as a parameter
        // Content of your test goes here
        softly.then(actual).isEqualTo(expected);
        // No need for assertAll() now, it's done at the end of the test
    }
}

That's it for today! I still have plenty of tips regarding tests but those will be for another post.

Did you learn something new today? Do you have suggestions? Feel free to leave a comment below!