DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Mastering Unit Testing and Test-Driven Development in Java
  • Testing Asynchronous Operations in Spring With JUnit 5 and Byteman
  • Testing Asynchronous Operations in Spring With JUnit and Byteman
  • Comprehensive Guide to Unit Testing Spring AOP Aspects

Trending

  • Beyond ChatGPT, AI Reasoning 2.0: Engineering AI Models With Human-Like Reasoning
  • Unlocking the Potential of Apache Iceberg: A Comprehensive Analysis
  • Create Your Own AI-Powered Virtual Tutor: An Easy Tutorial
  • How the Go Runtime Preempts Goroutines for Efficient Concurrency
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. Hints for Unit Testing With AssertJ

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.

By 
Arnošt Havelka user avatar
Arnošt Havelka
DZone Core CORE ·
Feb. 26, 24 · Tutorial
Likes (8)
Comment
Save
Tweet
Share
5.5K Views

Join the DZone community and get the full member experience.

Join For Free

Unit 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:

Java
 
@Test
public void getMessage() {
	assertThat(service).isNotNull();
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}


This test produces errors like this:

Plain Text
 
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 is null 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.

Java
 
@Test
public void getMessage() {
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}


The modified test produces an error like this:

Java
 
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:

Java
 
// #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.

Plain Text
 
#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:

Java
 
// #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.

Plain Text
 
#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:

  1. Count the found countries. 
  2. Assert the first entry with several values.

Such tests can look like this example:

Java
 
@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.

Java
 
@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.

Java
 
@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.

A false positive correct test result

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:

Java
 
@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.

Plain Text
 
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 and INFO log message
Java
 
@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:

Java
 
@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:

  • spring-advanced-training
  • junit-poc
Java (programming language) Coding best practices JUnit unit test

Opinions expressed by DZone contributors are their own.

Related

  • Mastering Unit Testing and Test-Driven Development in Java
  • Testing Asynchronous Operations in Spring With JUnit 5 and Byteman
  • Testing Asynchronous Operations in Spring With JUnit and Byteman
  • Comprehensive Guide to Unit Testing Spring AOP Aspects

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!