Assertions in Depth
Assertions in Depth
Assertions are the heart of every test. A test without a meaningful assertion is little more than a smoke screen — it can pass even when the code under test is completely wrong. JUnit 5 ships a rich set of static methods in org.junit.jupiter.api.Assertions that cover equality, exception behaviour, grouped checks, and much more. In this lesson you will move past the basics and learn to write assertions that are precise, self-documenting, and genuinely useful when they fail.
assertEquals — More Than Equality
assertEquals(expected, actual) is the most used assertion. The convention matters: expected goes first, actual goes second. This order is not arbitrary — JUnit uses it to produce the failure message "expected: <X> but was: <Y>". Reversing the order produces a confusing, backwards message that wastes debugging time.
The third argument here is the delta (tolerance) for floating-point comparisons — essential whenever your calculation involves double or float, where rounding errors accumulate. The fourth argument is the failure message. Always provide a descriptive message: when this test breaks at 2 a.m., the message should tell the reader what was expected without having to reverse-engineer the test name.
Supplier<String> instead of a plain String for expensive messages. JUnit only evaluates the supplier when the assertion actually fails, so there is no cost on the happy path:
assertEquals(expected, actual, () -> "Computed from " + expensiveOperation());
For object equality, assertEquals delegates to .equals(). This means you must ensure your domain objects implement equals correctly — a common source of false positives when comparing List, Map, or custom value types. For reference equality (same instance), use assertSame.
assertThrows — Testing Exceptional Paths
Production code that does not throw when it should is just as broken as code that returns the wrong value. assertThrows lets you assert both that an exception is thrown and that it is the correct type — it also returns the exception instance so you can inspect its message or cause.
The lambda passed to assertThrows is the executable — only the code inside it is expected to throw. This boundary matters: do not put setup code inside the lambda, or you may accidentally catch an exception from the wrong place.
assertThrows lambda and that code throws unexpectedly, your test passes for the wrong reason. Keep the lambda as narrow as possible — just the single call that should throw.
For the opposite scenario — asserting that code does not throw — use assertDoesNotThrow:
assertAll — Grouping Assertions
When you use a plain sequence of assertEquals calls, the first failure stops execution and hides all subsequent failures. This makes diagnosing multi-field objects painful — you fix one field, re-run, see the next failure, and so on. assertAll runs every executable in the group and reports all failures together.
The first argument is a heading that prefixes the failure report, making it immediately clear which group of assertions failed. This is invaluable when a class has multiple assertAll groups.
assertAll when the assertions are logically related properties of the same result — for example, every field of a mapped object. If the properties represent independent behaviours, prefer separate @Test methods so each has a clear, focused name and can fail independently.
Writing Clear Assertions
An assertion is also documentation. Future maintainers — and your future self — will read it to understand what the code is supposed to do. The following principles make assertions genuinely informative:
- One logical concept per test. Do not test five unrelated things in a single method. When the test name is shouldReturnCorrectResultWhenInputIsValidAndUserIsAuthenticatedAndCacheIsWarm, it is trying to do too much.
- Assert on outcomes, not implementations. Prefer asserting on the return value or state change rather than on which internal method was called (that belongs in Mockito verification, covered in a later lesson).
- Provide failure messages for non-obvious assertions.
assertTrue(result > 0)fails with "expected: true but was: false" — useless.assertTrue(result > 0, "balance must be positive after deposit")fails with a message anyone can act on. - Use the most specific assertion available.
assertNotNull(list)thenassertEquals(3, list.size())can be replaced with a singleassertIterableEqualsor, with AssertJ, a fluent chain. Specific assertions produce better failure messages.
Beyond the Standard Library — AssertJ
The standard Assertions class covers the common cases. For richer failure messages and more readable chains, many teams add AssertJ (assertj-core) alongside JUnit 5. It provides a fluent API:
AssertJ is not part of JUnit 5 but pairs with it naturally. Its error messages include the full state of the object under test, drastically reducing the time spent reading stack traces.
Summary
Effective assertions follow a clear structure: expected before actual, delta for floating-point, meaningful failure messages, and the most specific assertion type available. Use assertThrows to pin exceptional paths and keep the executable narrow. Use assertAll to surface all failures in a multi-field check at once. Every assertion you write is a contract — make it precise enough to catch exactly the bug it is meant to prevent, and descriptive enough that the failure message explains itself without a debugger.