Unit testing has become a standard part of development. Many tools can be used for this in many different ways. This article shows some tips or let’s say best practices that work well for me.
In this article you will learn
Do not 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 NPEs in tests. Our goal is to verify the behavior of the tested component in a clean, readable and reliable way.
Bad practice
I have used many times in the past isNotNull
assertion even when it wasn’t needed, as in the example below:
@Test
public void getMessage()
assertThat(service).isNotNull();
assertThat(service.getMessage()).isEqualTo("Hello world!");
This test produces the following errors:
java.lang.AssertionError:
Expecting actual not to be null
at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)
Good exercise
Although additional isNotNull
the claim is not really harmful, it should be avoided for the following reasons:
- It does not add any additional value. It’s just more code to read and maintain.
- The test fails when anyway
service
isnull
and we see the real root cause of failure. The test still serves its purpose. - The error message produced is even better with AssertJ.
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: an example can be found in SimpleSpringTest.
Validate the values, not the result
Every now and then we write the correct test, but in a “bad” way. This means that the test works exactly as intended and checks our component, but the failure does not provide enough information. Therefore, our goal is to confirm the value, not the result of the comparison.
Bad practice
Let’s look at a few 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 above tests 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 exercise
The solution is very simple with AssertJ and its fluent API. All the above cases can be easily rewritten as:
// #1
assertThat(argument).contains("o");
// #2
assertThat(result).isInstanceOf(String.class);
// #3
assertThat("").isBlank();
// #4
assertThat(testMethod).isPresent();
The same mistakes as mentioned before now provide more value.
#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: an example can be found in SimpleParamTests.
Group claims together
Assertion chaining and associated code indentation help a lot with test clarity and readability.
Bad practice
As we write the test, we may end up with an accurate but less readable test. Let’s imagine a test where we want to find countries and perform these checks:
- Count the countries found.
- Confirm the first entry with several values.
Such tests may 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 exercise
Although the previous test is correct, we should greatly improve readability by grouping related statements (lines 9-11). The goal here is to assert result
write many chained statements once and as needed. Check out 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: an example can be found in CountryRepositoryOtherTests.
Prevent a false positive from a successful test
When any method of asserting s ThrowingConsumer
argument is used, then the argument must contain assertThat
and with consumers. Otherwise, the test would pass all the time – even when the comparison fails, meaning the test is wrong. The test fails only when the statement throws out 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 that we have several country codes and we want to verify that each code meets some condition. In our mock case, we want to assert that each country code contains the character “a”. As you can see, it’s nonsense: we have the codes in uppercase, but we don’t apply case-insensitivity to 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 exercise
As mentioned at the beginning of this section, our test can be easily corrected by additional assertThat
in the consumer (line 7). A 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)
Chained claims
The last tip is actually not a practice, but rather a recommendation. The AssertJ fluent API should be used to create more readable tests.
Assertions without chaining
Let’s consider listLogs
test, the purpose of which is to test the writing of the component. The goal here is to check:
- Confirmed number of logs collected
- Establish existence
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" );
);
Assertion chaining
With the aforementioned fluent API and chaining, we can change the test as follows:
@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: an example can be found in AppleTest.
Summary and source code
The AssertJ framework provides a lot of help with its fluid API. This article presents some tips and advice to produce clearer and more reliable tests. Keep in mind that most of these recommendations are subjective. It depends on personal preference and code style.
The source code used can be found in my repositories: