skip to content

JUnit 5 assertions accept an optional failure message — what forms can it take, and why is the Supplier form preferred for expensive messages?

level: middleimportance: should knowfreq 40%

answer

  1. message = last argument in JUnit 5 (first in JUnit 4)
  2. String = eager (built every run)
  3. Supplier<String> = lazy (built only on failure)
  4. message never changes pass/fail
  5. expensive message -> use the () -> lambda

basics

~20 s

You can pass a String message as the last argument, shown only when the assertion fails. JUnit also accepts a Supplier<String> lambda; the lambda runs only on failure, so a costly message isn't built every time the test passes.

solid answer

~50 s

Most JUnit 5 assertions have an overload taking an optional failure message as the final parameter, and it comes in two forms: a plain String, or a Supplier<String> (a lambda like () -> "..."). The message is appended to the assertion failure and only matters when the test fails — it never changes pass/fail. The crucial difference: a String argument is evaluated eagerly, every time the assertion runs, even on the overwhelmingly common passing path; if building it is expensive (string concatenation in a loop, serializing a big object), you pay that cost on every green run. The Supplier form is lazy — JUnit invokes get() only when the assertion actually fails, so the expensive construction happens just for the failure you need to diagnose. Note the message is always the LAST argument in JUnit 5 (it moved from first in JUnit 4), which avoids colliding with the delta parameter for floating-point asserts.

code

java · 20 lines
java
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class MessageFormsTest {
    @Test
    void eagerVsLazyMessage() {
        int balance = 10;

        // Eager: this String is built on EVERY run (cheap here, fine).
        assertTrue(balance >= 0, "balance must be non-negative");

        // Lazy: the lambda runs ONLY if the assertion fails.
        assertTrue(balance >= 0, () -> "balance was " + expensiveDump(balance));
    }

    private String expensiveDump(int b) {
        // Imagine this serializes a large object graph.
        return "<dump:" + b + ">";
    }
}

go deeper

for a junior

Know you can add a custom String message that appears when the assertion fails.

for a middle

Distinguish the String (eager) from the Supplier<String> (lazy) form, explain that the message is the last arg in JUnit 5, and that it's diagnostic-only.

for a senior

Reason about suite performance: defer expensive message construction with suppliers, and watch for JUnit 4->5 message-position migration bugs.

for a principal

Promote good failure diagnostics as a maintainability investment, codify message conventions, and flag eager expensive-message anti-patterns in review/lint.

## What the message is for When an assertion fails, JUnit throws an error and prints a message. The built-in message ('expected: <X> but was: <Y>') is often enough, but you can attach your **own** message to explain the business meaning of the failure — e.g. 'balance after withdrawal should be non-negative'. This custom message is **purely diagnostic**: it is shown only on failure and never affects whether the test passes. ## The two forms Nearly every JUnit 5 assertion has an overload whose **last** parameter is the message, in one of two shapes: 1. **String** — a fixed or already-built message: ```java assertTrue(balance >= 0, "balance must stay non-negative"); ``` 2. **Supplier<String>** — a lambda that *produces* the message on demand: ```java assertTrue(balance >= 0, () -> "balance was " + describe(account)); ``` `Supplier<String>` is a functional interface with a single method `String get()`. ## Eager vs lazy — the key distinction A `String` argument is an ordinary expression, so Java evaluates it **eagerly**: the message is constructed **every time the assertion line runs**, regardless of pass or fail. Tests pass on almost every run, so if the message is expensive — concatenating in a loop, calling `toString()` on a large graph, serializing JSON — you pay that cost on **every green run**, slowing the suite for no benefit. The `Supplier<String>` form is **lazy**: you hand JUnit a recipe (the lambda) instead of the finished string. JUnit calls the lambda's `get()` **only when the assertion fails** and it actually needs the text. On the passing path the lambda is never invoked, so the expensive construction is skipped. Rule of thumb: a **constant or cheap** string → pass it directly; an **expensive-to-build** message → use the supplier lambda. ## Position matters: last in JUnit 5 In **JUnit 4** the message was the **first** argument (`assertEquals("msg", expected, actual)`). In **JUnit 5** it moved to the **last** position (`assertEquals(expected, actual, "msg")`). This avoids ambiguity with the floating-point **delta** overload `assertEquals(expected, actual, delta)` and is a frequent source of migration bugs. If you put the message first in JUnit 5, it will be treated as the *expected value*, comparing strings instead of your real values. ## Quick summary - Message = diagnostic only, shown on failure, never changes the result. - Two forms: eager `String`, lazy `Supplier<String>`. - Prefer the supplier when the message is costly to build. - It is the **last** parameter in JUnit 5 (was first in JUnit 4).

  • When is the Supplier<String> overload genuinely worth it over a plain String?
    When constructing the message is expensive — concatenating in a loop, dumping a large object, serializing — because the eager String pays that cost on every (mostly passing) run, while the supplier defers it to the failure path only.
  • What breaks if you write assertEquals("msg", expected, actual) in JUnit 5?
    The message moved to the last position in JUnit 5, so "msg" is taken as the EXPECTED value and your real expected becomes actual — you'd be comparing the wrong things, often a compile error or a confusing failure.

saying these in an interview costs you the question

  • Putting the message first (JUnit 4 style) in JUnit 5 — it becomes the expected value
  • Claiming the supplier message runs every time the test passes
  • Thinking the message can influence whether the assertion passes

context