skip to content

JUnit 5 assertion methods such as org.junit.jupiter.api.Assertions.assertEquals accept a failure message either as a String or as a Supplier<String>. When would you choose the Supplier form, and what difference does it make at runtime?

level: middleimportance: must knowfreq 50%

answer

  1. String = eager, built on every run
  2. Supplier = get() only on failure
  3. anything with + or a call → lambda
  4. helper method is still eager
  5. rich diagnostics become affordable

basics

~20 s

A String message is built every time the assertion runs, including when it passes. A Supplier<String> is only invoked when the assertion fails, so use it whenever building the message costs something — concatenation in a loop, serializing an object, formatting collections.

solid answer

~50 s

Every JUnit 5 assertion has overloads taking `String message` and `Supplier<String> messageSupplier`. Semantically they produce the same text; the difference is *when* the text is produced. With the `String` overload the argument is evaluated at the call site — before the assertion even runs — so the concatenation, `String.format`, or `toString()` of a big object happens on every execution, and on the passing path that work is thrown away. With the supplier, JUnit calls `get()` only on the failure path. ```java assertEquals(expected, actual, () -> "mismatch for order " + order.id() + ": " + order.dump()); ``` So: plain literals stay `String`; anything computed — string concatenation, formatting, dumping state — goes in a lambda. It matters most in loops, in generated groups of assertions, and in parameterized runs where the same assertion executes hundreds of times. It also keeps expensive diagnostics affordable: you can afford a rich message precisely because nobody pays for it until something breaks.

code

java · 5 lines
java
// eager: dump() runs on every pass
assertEquals(expected, actual, "mismatch: " + order.dump());

// lazy: dump() runs only when the assertion fails
assertEquals(expected, actual, () -> "mismatch: " + order.dump());

go deeper

for a junior

State the core fact: the String is built every time, the supplier only when the assertion fails.

for a middle

Explain Java's eager argument evaluation, name the situations where it costs (loops, generated groups, expensive toString) and where a literal is fine.

for a senior

Argue the diagnostics angle — laziness is what makes rich, context-carrying messages affordable — and warn about suppliers that can throw and mask the failure.

for a principal

Position it as an API-design pattern (deferred computation for cold paths) applied consistently across assertions, assumptions and logging, and set it as a team convention rather than a micro-optimization.

## The overload pairs JUnit 5's `Assertions` methods are overloaded three ways: no message, `String message`, and `Supplier<String> messageSupplier`. `assertEquals(expected, actual)`, `assertEquals(expected, actual, "...")`, `assertEquals(expected, actual, () -> "...")`. The same pattern appears on `assertTrue`, `assertNotNull`, `assertThrows`, `fail`, and the assumption methods. ## Eager versus lazy evaluation Java evaluates arguments before the call. Writing ```java assertEquals(expected, actual, "mismatch for " + order + " in " + context.describe()); ``` means `order.toString()`, `context.describe()` and the concatenation all execute *every* time the line is reached — overwhelmingly the passing path, where the string is discarded immediately. The lambda form defers that work: ```java assertEquals(expected, actual, () -> "mismatch for " + order + " in " + context.describe()); ``` JUnit stores the supplier and calls `get()` only when it is building a failure. On the happy path nothing but a small lambda object is created — and even that is usually stack-allocatable or a cached constant when it captures nothing. ## When the difference is real One assertion executed once: irrelevant, and a literal message is clearer. The supplier earns its keep when: - the message formats a collection, map, or entity graph (`toString()` on a large object can be surprisingly slow); - the message needs a call — a database read, a re-serialization, a diff computation — that you would never want on the passing path; - the assertion runs many times: inside a loop, inside a generated group of assertions, or in a parameterized test with a large data set; - the message would throw or misbehave if built when the value under test is fine — for example dereferencing something only meaningful in the failure case. That last point is the subtle one: laziness also changes *whether* the code runs at all, not just when. A supplier that would throw a `NullPointerException` never runs on the passing path — but be careful, because if it throws on the failure path you get that exception instead of your assertion failure, masking the real defect. Keep suppliers total: no assertions inside them, no calls that can blow up. ## Do not fake it with a helper A common wrong answer is "I extract a `message(...)` method and pass its result" — that is still eager; the method executes before the assertion. Only the lambda (or an already-built constant) defers. ## Style guidance - Literal, constant text → `String` overload. `() -> "user should be active"` adds noise for no benefit. - Anything with a `+`, a `String.format`, or a method call → `Supplier<String>`. - Kotlin users get the same thing as a trailing lambda parameter, and Kotlin's string templates make the eager form especially tempting, so the rule matters there too. ## Relationship to the default output The custom message does not replace JUnit's own report — for `assertEquals` the failure still shows `expected: <X> but was: <Y>`; your message is prefixed with `==> ` separating it from the comparison. So a message should add context the comparison cannot supply (which iteration, which input, which entity), not restate that two values differ. ## Performance in perspective On a modern JIT the cost of one discarded concatenation is small; the honest framing is that suppliers are a cheap habit that removes a whole class of waste and enables richer diagnostics, not that eager messages will visibly slow your build. Interviewers want the reasoning — evaluation timing and why the API exposes both — more than a benchmark.

  • Does the custom message replace JUnit's expected/actual output?
    No. For value assertions the framework still renders its own comparison, and your text is prefixed to it, separated by '==>'. That is why a good message adds context the comparison cannot — which input, which iteration, which entity — rather than restating that the values differ.
  • What can go wrong inside a message supplier?
    If the supplier itself throws, you see that exception instead of the assertion failure, and the real defect is masked. Keep suppliers side-effect-free and total: no assertions, no calls that can throw, no mutation of test state, since the supplier runs at reporting time.

Eager message = printing the incident report before checking whether there was an incident; supplier = printing it only when the alarm actually goes off.

saying these in an interview costs you the question

  • Claiming both overloads behave identically at runtime
  • Thinking the String overload is somehow evaluated lazily by the compiler
  • Extracting message building into a helper method and calling it a lazy message
  • Using a supplier for a constant literal and calling it an optimization
  • Putting assertions or state mutation inside the message supplier

context