AssertionsJUnit 5
In JUnit 5, assertions are the heart of your test cases. They are the mechanism used to verify that the actual behavior of your code aligns with your expectations. If an assertion fails, it throws an AssertionFailedError, which immediately terminates the current test and marks it as failed.
All standard assertions are centralized in the org.junit.jupiter.api.Assertions class as static methods. To keep your test code clean and readable, it is highly recommended to use a static import:
import static org.junit.jupiter.api.Assertions.*;
The most frequently used assertions handle common scenarios such as checking for equality, boolean truth, and nullity. These form the foundation of most unit tests.
- Equality and Inequality:
assertEquals(expected, actual)andassertNotEquals(unexpected, actual)verify if two values are equal. - Boolean Checks:
assertTrue(condition)andassertFalse(condition)validate whether a boolean expression evaluates to true or false. - Nullity:
assertNull(object)andassertNotNull(object)check for the presence or absence of a reference. - Identity:
assertSame(expected, actual)andassertNotSame(unexpected, actual)check if two references point to the exact same object in memory.
Example usage:
@Test
void basicAssertions() {
assertEquals(4, 2 + 2);
assertNotEquals(5, 2 + 2);
assertTrue(3 > 2);
assertFalse(3 < 2);
String name = null;
assertNull(name);
assertNotNull("JUnit 5");
Object a = new Object();
Object b = a;
assertSame(a, b);
assertNotSame(a, new Object());
}
Each of these methods accepts an optional message parameter. This message is only displayed if the assertion fails, providing critical context for debugging.
assertEquals(10, result, "The calculation result should be 10");
Array and Collection Assertions
When dealing with arrays or collections, comparing references (using assertEquals on the objects themselves) is often insufficient because you typically care about the contents of the collection rather than the object identity.
assertArrayEquals: Specifically designed for arrays, ensuring that both arrays have the same length and identical elements in the same order.assertIterableEquals: Used forIterableobjects (likeList), verifying that the elements are the same and in the same sequence.assertLinesMatch: A powerful tool for verifying text content. It compares two lists of strings and supports regular expressions, making it ideal for validating log outputs or multi-line text files.
@Test
void collectionAssertions() {
int[] expected = {1, 2, 3};
int[] actual = {1, 2, 3};
assertArrayEquals(expected, actual);
List<String> list1 = List.of("a", "b");
List<String> list2 = List.of("a", "b");
assertIterableEquals(list1, list2);
assertLinesMatch(
List.of("hello", "\\d+"),
List.of("hello", "123")
);
}
Exception Assertions
Testing for "sad paths"—where your code is expected to fail—is just as important as testing the "happy path." JUnit 5 provides a clean way to verify that a specific exception is thrown without needing cumbersome try-catch blocks.
The assertThrows method takes the expected exception class and a lambda expression containing the code that should trigger the exception. Crucially, it returns the thrown exception object, allowing you to perform further assertions on the exception's message or state.
@Test
void exceptionAssertions() {
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> {
throw new IllegalArgumentException("Invalid parameter");
}
);
assertEquals("Invalid parameter", ex.getMessage());
// Verify that a block of code does NOT throw any exception
assertDoesNotThrow(() -> Integer.parseInt("123"));
}
Timeout Assertions
To ensure your application remains responsive and doesn't hang indefinitely, JUnit 5 offers timeout assertions. There are two distinct approaches:
assertTimeout: This method executes the provided code and checks if it finished within the specified duration. However, it waits for the code to complete before failing the test.assertTimeoutPreemptively: This method is more aggressive. If the code exceeds the timeout, it interrupts the executing thread immediately.
Caution: Because assertTimeoutPreemptively runs the code in a different thread, it can lead to issues with ThreadLocal variables or other thread-dependent states. Use it only when you truly need to kill a hanging process.
@Test
void timeoutAssertions() {
assertTimeout(Duration.ofMillis(500), () -> {
Thread.sleep(100);
});
assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
Thread.sleep(200);
});
}
Grouped Assertions with assertAll
By default, a test stops as soon as the first assertion fails. This "fail-fast" behavior can be frustrating when you have multiple related assertions; you fix the first one, only to find the second one fails immediately after.
assertAll solves this by grouping multiple assertions together. It executes all of them, regardless of whether some fail, and then reports all failures in a single summary.
@Test
void groupedAssertions() {
Person person = new Person("Alice", 30);
assertAll("Person validation",
() -> assertEquals("Alice", person.getName()),
() -> assertEquals(30, person.getAge()),
() -> assertNotNull(person.getEmail())
);
}
Lazy Message Evaluation
Constructing complex error messages (e.g., concatenating strings or calling a helper method) can be computationally expensive. If the assertion passes, that string construction was a waste of resources.
JUnit 5 allows you to pass a Supplier<String> (via a Lambda) as the message. The message is only evaluated if the assertion actually fails.
assertEquals(2, 1 + 1, () -> "Calculation failed: " + expensiveOperation());
Integrating Third-Party Libraries
While the built-in assertions are sufficient for most cases, many developers prefer libraries like AssertJ or Hamcrest for their "fluent" API, which reads more like natural English and provides richer assertions for complex objects.
For example, using AssertJ:
assertThat(list)
.hasSize(3)
.contains("a", "b")
.doesNotContain("z");
You can mix these libraries with JUnit 5, but for the sake of maintainability, it is best to stick to one consistent style across your project.
Best Practices Summary
To write maintainable and effective tests, keep these guidelines in mind:
- Use Static Imports: Always import
Assertions.*statically to reduce boilerplate. - Be Descriptive: Add meaningful failure messages to critical assertions to speed up debugging.
- Group Related Checks: Use
assertAllwhen verifying multiple properties of a single object. - Verify Exception Details: Don't just check that an exception was thrown; use the return value of
assertThrowsto verify the error message. - Choose Timeouts Wisely: Prefer
assertTimeoutunless you specifically need to preemptively interrupt a thread. - Keep Assertions Pure: Avoid performing operations with side effects inside an assertion.