Hints for Unit Testing With AssertJ
Discover a couple of tips and tricks for writing better unit tests with the AssertJ framework. Every recommendation includes a practical demonstration.
Join the DZone community and get the full member experience.
Join For FreeUnit testing has become a standard part of development. Many tools can be utilized for it in many different ways. This article demonstrates a couple of hints or, let's say, best practices working well for me.
In This Article, You Will Learn
- How to write clean and readable unit tests with JUnit and Assert frameworks
- How to avoid false positive tests in some cases
- What to avoid when writing unit tests
Don't Overuse NPE Checks
We all tend to avoid NullPointerException
as much as possible in the main code because it can lead to ugly consequences. I believe our main concern is not to avoid NPE in tests. Our goal is to verify the behavior of a tested component in a clean, readable, and reliable way.
Bad Practice
Many times in the past, I've used isNotNull
assertion even when it wasn't needed, like in the example below:
@Test
public void getMessage() {
assertThat(service).isNotNull();
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
This test produces errors like this:
java.lang.AssertionError:
Expecting actual not to be null
at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)
Good Practice
Even though the additional isNotNull
assertion is not really harmful, it should be avoided due to the following reasons:
- It doesn't add any additional value. It's just more code to read and maintain.
- The test fails anyway when
service
isnull
and we see the real root cause of the failure. The test still fulfills its purpose. - The produced error message is even better with the AssertJ assertion.
See the modified test assertion below.
@Test
public void getMessage() {
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
The modified test produces an error like this:
java.lang.NullPointerException: Cannot invoke "com.github.aha.poc.junit.spring.HelloService.getMessage()" because "this.service" is null
at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)
Note: The example can be found in SimpleSpringTest.
Assert Values and Not the Result
From time to time, we write a correct test, but in a "bad" way. It means the test works exactly as intended and verifies our component, but the failure isn't providing enough information. Therefore, our goal is to assert the value and not the comparison result.
Bad Practice
Let's see a couple of such bad tests:
// #1
assertThat(argument.contains("o")).isTrue();
// #2
var result = "Welcome to JDK 10";
assertThat(result instanceof String).isTrue();
// #3
assertThat("".isBlank()).isTrue();
// #4
Optional<Method> testMethod = testInfo.getTestMethod();
assertThat(testMethod.isPresent()).isTrue();
Some errors from the tests above are shown below.
#1
Expecting value to be true but was false
at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
#3
Expecting value to be true but was false
at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)
Good Practice
The solution is quite easy with AssertJ and its fluent API. All the cases mentioned above can be easily rewritten as:
// #1
assertThat(argument).contains("o");
// #2
assertThat(result).isInstanceOf(String.class);
// #3
assertThat("").isBlank();
// #4
assertThat(testMethod).isPresent();
The very same errors as mentioned before provide more value now.
#1
Expecting actual:
"Hello"
to contain:
"f"
at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
#3
Expecting blank but was: "a"
at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)
Note: The example can be found in SimpleParamTests.
Group-Related Assertions Together
The assertion chaining and a related code indentation help a lot in the test clarity and readability.
Bad Practice
As we write a test, we can end up with the correct, but less readable test. Let's imagine a test where we want to find countries and do these checks:
- Count the found countries.
- Assert the first entry with several values.
Such tests can look like this example:
@Test
void listCountries() {
List<Country> result = ...;
assertThat(result).hasSize(5);
var country = result.get(0);
assertThat(country.getName()).isEqualTo("Spain");
assertThat(country.getCities().stream().map(City::getName)).contains("Barcelona");
}
Good Practice
Even though the previous test is correct, we should improve the readability a lot by grouping the related assertions together (lines 9-11). The goal here is to assert result
once and write many chained assertions as needed. See the modified version below.
@Test
void listCountries() {
List<Country> result = ...;
assertThat(result)
.hasSize(5)
.singleElement()
.satisfies(c -> {
assertThat(c.getName()).isEqualTo("Spain");
assertThat(c.getCities().stream().map(City::getName)).contains("Barcelona");
});
}
Note: The example can be found in CountryRepositoryOtherTests.
Prevent False Positive Successful Test
When any assertion method with the ThrowingConsumer
argument is used, then the argument has to contain assertThat
in the consumer as well. Otherwise, the test would pass all the time - even when the comparison fails, which means the wrong test. The test fails only when an assertion throws a RuntimeException
or AssertionError
exception. I guess it's clear, but it's easy to forget about it and write the wrong test. It happens to me from time to time.
Bad Practice
Let's imagine we have a couple of country codes and we want to verify that every code satisfies some condition. In our dummy case, we want to assert that every country code contains "a" character. As you can see, it's nonsense: we have codes in uppercase, but we aren't applying case insensitivity in the assertion.
@Test
void assertValues() throws Exception {
var countryCodes = List.of("CZ", "AT", "CA");
assertThat( countryCodes )
.hasSize(3)
.allSatisfy(countryCode -> countryCode.contains("a"));
}
Surprisingly, our test passed successfully.
Good Practice
As mentioned at the beginning of this section, our test can be corrected easily with additional assertThat
in the consumer (line 7). The correct test should be like this:
@Test
void assertValues() throws Exception {
var countryCodes = List.of("CZ", "AT", "CA");
assertThat( countryCodes )
.hasSize(3)
.allSatisfy(countryCode -> assertThat( countryCode ).containsIgnoringCase("a"));
}
Now the test fails as expected with the correct error message.
java.lang.AssertionError:
Expecting all elements of:
["CZ", "AT", "CA"]
to satisfy given requirements, but these elements did not:
"CZ"
error:
Expecting actual:
"CZ"
to contain:
"a"
(ignoring case)
at com.github.aha.sat.core.clr.AppleTest.assertValues(AppleTest.java:45)
Chain Assertions
The last hint is not really the practice, but rather the recommendation. The AssertJ fluent API should be utilized in order to create more readable tests.
Non-Chaining Assertions
Let's consider listLogs
test, whose purpose is to test the logging of a component. The goal here is to check:
- Asserted number of collected logs
- Assert existence of
DEBUG
andINFO
log message
@Test
void listLogs() throws Exception {
ListAppender<ILoggingEvent> logAppender = ...;
assertThat( logAppender.list ).hasSize(2);
assertThat( logAppender.list ).anySatisfy(logEntry -> {
assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
});
assertThat( logAppender.list ).anySatisfy(logEntry -> {
assertThat( logEntry.getLevel() ).isEqualTo(INFO);
assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
});
}
Chaining Assertions
With the mentioned fluent API and the chaining, we can change the test this way:
@Test
void listLogs() throws Exception {
ListAppender<ILoggingEvent> logAppender = ...;
assertThat( logAppender.list )
.hasSize(2)
.anySatisfy(logEntry -> {
assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
})
.anySatisfy(logEntry -> {
assertThat( logEntry.getLevel() ).isEqualTo(INFO);
assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
});
}
Note: the example can be found in AppleTest.
Summary and Source Code
The AssertJ framework provides a lot of help with their fluent API. In this article, several tips and hints were presented in order to produce clearer and more reliable tests. Please be aware that most of these recommendations are subjective. It depends on personal preferences and code style.
The used source code can be found in my repositories:
Opinions expressed by DZone contributors are their own.
Comments