With this post, we will take a close look at assertions for Java unit tests. The Java testing ecosystem offers a wide range of assertion libraries. There are assertion libraries available both for simple assertions like comparing primitive values as well as more complex data structures like JSON or XML.
We’ll begin by exploring the most commonly used Java assertion libraries and then move on to best practices for writing clear and effective assertions. When written correctly, assertions serve as a form of documentation, helping others quickly understand the purpose of the code and identify which part of it failed during a test. Following a set of guidelines for assertions is crucial for minimising confusion and assisting developers who are unfamiliar with the codebase.
What is An Assertion in Java?
In the context of unit tests, an assertion in Java is a statement used to verify that a specific condition holds true in the code being tested. The conditions we’re verifying are usually feature requirements from our business domain. As an example, we can use assertions to check whether our method returns the correct VAT rate for our checkout.
Assertions are fundamental to unit testing because they help ensure that the code behaves as expected. If an assertion fails during a test, it indicates that there is a bug or an issue in the code.
Furthermore, assertions are critical in Test-Driven Development (TDD), where tests (including assertions) are written before the actual code. The assertions define the expected behaviour, guiding the implementation.
From a structural perspective and when following an arrange/act/assert test setup, we place assertions at the third and last part of our unit tests:
- Arrange: Initial setup to prepare the test execution, mock collaborators and initialise objects
- Act: Invoking our class/method under test
- Assert: Comparing the returned value with the expected value
Even when not following this structure, assertions are usually found towards the end of a unit test.
Assertion Library Overview in Java
The Java testing ecosystem is both vast and mature, offering a wide range of assertion libraries to choose from. Generally, there isn’t a single “best” assertion library, as they all effectively serve their purpose. The choice often comes down to personal preference or the specific need for advanced features, such as writing assertions for asynchronous code or specific data structures like XML or JSON.
For this article, we focus on the following common assertion libraries:
- JUnit
- AssertJ
- Hamcrest
- JSONAssert
- XMLUnit
- Awaitility
JUnit
As Java’s de-facto standard testing framework, JUnit also comes with a set of built-in assertions. It’s usually the first choice of developers for assertions as they’re anyway using JUnit as the test framework.
Let’s take a look at an example:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class JUnitExampleTest {
@Test
void testBasicAssertions() {
int result = 3 + 2;
String str = "Hello, World!";
Object obj = null;
// Basic Assertions
assertEquals(5, result);
assertEquals(5, result, "The result should be 5");
assertTrue(str.startsWith("Hello"), "String should start with 'Hello'");
assertNull(obj, "Object should be null");
assertNotNull(str, "String should not be null");
assertThrows(NullPointerException.class, () -> obj.toString());
}
}
JUnit’s standard assertion is assertEquals. This assertion takes the expected value as the first argument and the actual value as the second argument. Optionally, we can add a third argument that is logged to the console whenever the assertion fails.
Apart from assertEquals, JUnit offers assertions to compare boolean values (assertTrue/assertFalse), identify null/not null values (assertNull/assertNotNull) as well as thrown exceptions with assertThrows.
AssertJ
AssertJ is a fluent assertion library for Java that is known for its powerful and readable assertions. It allows chaining of assertions and provides a rich set of methods for various checks.
Let’s see AssertJ’s assertion in action by following the same test example as above:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
public class AssertJExampleTest {
@Test
public void testBasicAssertions() {
int result = 3 + 2;
String str = "Hello, World!";
Object obj = null;
// Fluent Assertions with AssertJ
assertThat(result).isEqualTo(5);
assertThat(str).startsWith("Hello")
.contains("World")
.endsWith("!");
assertThat(obj).isNull();
assertThat(str).isNotNull();
}
}
Compared to JUnit’s assertEquals method, where the expected and the actual value are passed as arguments – where the order might potentially be mixed up – with AssertJ, we fluently chain additional method(s) to verify the actual value.
This chaining allows for multiple checks on the actual value without adding additional boilerplate code.
AssertJ really shines when it comes to asserting more complex data structures and collections:
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import java.util.Arrays;
import java.util.List;
public class AssertJAdvancedExampleTest {
@Test
public void testAdvancedCollectionAssertions() {
User user1 = new User("Alice", 30);
User user2 = new User("Bob", 25);
User user3 = new User("Charlie", 35);
List<User> users = Arrays.asList(user1, user2, user3);
assertThat(users).hasSize(3);
assertThat(users)
.contains(user1, user3)
.doesNotContain(new User("David", 40));
assertThat(users).containsExactly(user1, user2, user3);
assertThat(users)
.extracting(User::getName)
.contains("Alice", "Bob")
.doesNotContain("David");
assertThat(users)
.allMatch(user -> user.getAge() > 20)
.anyMatch(user -> user.getName().equals("Bob"));
}
}
In the example above, we can see a mix of common assertions for a Java List. Apart from checking the size with hasSize, we can verify that the list contains specific objects. We can even verify individual attributes of the objects that our list contains by using the extracting method of AssertJ.
Hamcrest
Hamcrest is another matcher library for building complex and expressive assertions.
What sets Hamcrest apart from other libraries is its focus on creating human-readable assertions through matchers, which make tests more understandable and maintainable.
Hamcrest allows developers to define complex conditions in a declarative manner, which is especially useful when testing collections and object properties.
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import java.util.Arrays;
import java.util.List;
public class HamcrestAdvancedExampleTest {
@Test
public void testAdvancedCollectionAssertions() {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
assertThat(names, hasSize(3));
assertThat(names, hasItems("Alice", "Charlie"));
assertThat(names, contains("Alice", "Bob", "Charlie"));
assertThat(names.get(0), anyOf(is("Alice"), is("John")));
assertThat(names, not(hasItems("David")));
assertThat(names.get(1), allOf(startsWith("B"), equalTo("Bob")));
}
}
When seeing Hamcrest for the first time, it looks like a mix of JUnit and AssertJ’s assertion. The big difference compared to JUnit is that the actual value comes first, while the expected value is the second argument.
Hamcrest comes with the same feature-rich assertion for collections as AssertJ without the chaining capability. Furthermore, Hamcrest is extensible as it allows us to write our own domain-specific matchers.
JSONAssert
JSONAssert is a specialized library for asserting JSON content. It allows you to compare JSON strings, ignoring irrelevant differences like the order of fields.
This assertion library comes handy when checking responses from JSON REST APIs.
import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
public class JSONAssertExampleTest {
@Test
public void testJSONAssertions() throws Exception {
String expected = "{\"name\":\"John\", \"age\":30}";
String actual = "{\"age\":30, \"name\":\"John\"}";
// Strict comparison (order matters), this assertion fail
JSONAssert.assertEquals(expected, actual, JSONCompareMode.STRICT);
// Non-strict comparison (order doesn't matter)
JSONAssert.assertEquals(expected, actual, JSONCompareMode.LENIENT);
}
}
JSONAssert provides two verification modes: strict and lenient.
In strict mode, the comparison is highly precise. Both the attribute names and values must match exactly, the order of the attributes must be identical, and no additional attributes are permitted.
As a result, the assertion above using strict mode will fail as the order of attributes don’t match.
On the other hand, lenient mode is more flexible. It allows for additional attributes and does not enforce the order of attributes during comparison.
XMLUnit
XMLUnit is a library for testing XML payloads. It allows us to compare XML structures, validate against schemas, and check specific XML nodes.
Similar to JSONAssert, this assertion library is beneficial when dealing with REST or SOAP APIs that return XML payloads:
import org.junit.jupiter.api.Test;
import org.xmlunit.builder.DiffBuilder;
import org.xmlunit.diff.Diff;
import static org.xmlunit.assertj.XmlAssert.assertThat;
public class XMLUnitExampleTest {
@Test
public void testXMLAssertions() {
String expectedXml = "<user><name>John</name><age>30</age></user>";
String actualXml = "<user><age>30</age><name>John</name></user>";
// Compare XML
Diff diff = DiffBuilder.compare(actualXml)
.withTest(expectedXml)
.ignoreWhitespace()
.checkForSimilar()
.build();
assertThat(diff.hasDifferences()).isFalse();
}
}
The test above checks whether there are differences between the compared XML payloads.
Awaitility
While writing and debugging asynchronous code can be hard, without the help of Awaitlity, testing it would be another challenge.
Awaitility is a library used for testing asynchronous code. It allows us to wait for a condition to be met within a given time frame, making it useful for testing asynchronous processes.
import org.junit.jupiter.api.Test;
import static org.awaitility.Awaitility.await;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class AwaitilityExampleTest {
@Test
public void testAwaitilityAssertions() {
AtomicInteger counter = new AtomicInteger(0);
// Simulate asynchronous update
new Thread(() -> {
try {
Thread.sleep(500);
counter.incrementAndGet();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// Awaitility to wait until condition is met
await().atMost(3, TimeUnit.SECONDS)
.untilAtomic(counter, isEqualTo(3));
}
}
The test example above shows an assertion for an asynchronous counter, that increments a counter every 500 milliseconds.
With the fluent await method we define our waiting condition. In our example we let the test run at most 3 seconds until the counter must match 3.
Assertion Best Practices
As a follow-up to our existing article on How to write better unit test assertions, we want to provide further best practices for writing assertions.
Stick to One Central Assertion Library
For basic assertions (those not specific to special data structures like JSON or asynchronous operations), it’s best practice to use a single assertion library across the entire project. While personal preferences may vary, this approach simplifies test writing and enhances consistency.
Mixing different libraries, such as using both Hamcrest and AssertJ in the same tests, can create confusion. Both libraries use assertThat, which can lead to ambiguity and make the code harder to understand.
By sticking to a single assertion library, you also ensure a more uniform testing style across the project, reducing cognitive overhead for developers and improving maintainability.
Explicit Variable Naming
To improve the readability of tests and assertions, it’s essential to give variables meaningful and descriptive names. This helps make it immediately clear what the test is doing and what the assertions are verifying.
For example, when unit testing a logarithmic function, we can name the returned value actualResult and the expected value expectedResult. Using this naming convention consistently across your test suite makes it easier to understand what values are being compared, especially in larger test cases.
The following code example showcase this best practice:
@Test
void testLogarithmicFunction() {
double actualResult = calculateLog(100);
double expectedResult = 2.0;
assertEquals(expectedResult, actualResult, 0.01);
}
Avoid using ambiguous names like personOne or personTwo, as they don’t provide enough context.
By using clear and descriptive names, you not only make tests easier to read but also ensure they remain maintainable over time.
Add Assertion Failure Messages
When tests fail, the log output displays the expected and actual values, helping us quickly identify what went wrong. However, sometimes just seeing these values isn’t enough to understand the root cause, especially in larger or more complex tests.
To make failures easier to debug, it’s helpful to include failure messages in your assertions. These messages provide additional context, explaining why the test failed and helping you diagnose the issue faster.
Adding custom messages to assertions improves clarity and makes it immediately obvious what part of the code or logic needs attention. This becomes particularly important when refactoring or changing code, as test failures are common during such processes.
Without an failure message, the followings test provides very little information whenever it fails:
@Test
void testIsAdult() {
boolean actualResult = isAdult(17);
assertFalse(actualResult);
}
org.opentest4j.AssertionFailedError:
Expected :true
Actual :false
The log output tells us that we expected the opposite boolean value. Without additional information, we need additional time to invest what’s being tested and what the context is.
If, instead, we add a failure message, our teammates can quickly understand the test behavior and what was expected:
@Test
void testIsAdult() {
boolean actualResult = isAdult(17);
assertFalse(actualResult, "An underage person of age 17 should not be identified as an adult");
}
While this additional failure message requires some additional keystrokes when writing the test, it greatly helps all upcoming developers working with the test suite.
Assertions with Diffblue Cover
With Diffblue Cover, we can reliably generate Java unit regression tests at scale to accelerate our development workflow. With the help of reinforcement learning, Diffblue Cover autonomously analyses our production code and generates unit tests for all possible code paths, including correct assertions.
Let’s see this in action by generating tests for the following method from the OwnerController of the Spring PetClinic project:
@ModelAttribute("owner")
public Owner findOwner(@PathVariable("ownerId") int ownerId) {
Owner owner = this.owners.findById(ownerId);
if (owner == null) {
throw new IllegalArgumentException("Owner ID not found: " + ownerId);
}
return owner;
}
For this controller method, we have two execution paths to verify. One where we find an owner by its ID and one where we don’t.
Diffblue Cover automatically detects that and generates the following two tests for us:
@Test
void testFindOwner_givenOwnerIdIs1_thenReturnsOwner() {
// Arrange
Owner owner = new Owner();
// … setup Owner object
when(ownerRepository.findById(Mockito.<Integer>any())).thenReturn(owner);
// Act
Owner actualFindOwnerResult = petController.findOwner(1);
// Assert
verify(ownerRepository).findById(eq(1));
assertSame(owner, actualFindOwnerResult);
}
/**
* Method under test: {@link PetController#findOwner(int)}
*/
@Test
void testFindOwner_thenThrowsIllegalArgumentException() {
// Arrange when(ownerRepository.findById(Mockito.<Integer>any())).thenReturn(null);
// Act and Assert
assertThrows(IllegalArgumentException.class, () -> petController.findOwner(1));
verify(ownerRepository).findById(eq(1));
}
These two tests include the necessary setup methods as well as the relevant assertions for each test. No manual interaction was necessary to generate them.
Diffblue Cover also clearly separates the test’s Arrange/Act/Assert section to easily navigate and understand them.
In this post, we highlighted the essential role of assertions in Java unit tests, showing how they help verify that your code behaves as expected. Assertions are an essential part of every test, and by applying best practices, they help developers catch bugs early and prevent regressions.
We also explored the rich ecosystem of Java assertion libraries, showcasing popular options like JUnit, AssertJ, Hamcrest, and more, with practical examples.
If you want to find out more about how Diffblue Cover automatically generates robust unit test cases at scale using assertions, you can see some examples of tests created at Docs.diffblue.com