In JUnit 5, when does adding an explicit failure message to an assertion genuinely improve diagnosis, and when does it make a failing build harder to understand?
answer
- assertTrue/assertNotNull default output names nothing
- loops and data-driven runs → message carries identity
- supplier form for anything computed
- restated or stale messages are noise
- strengthen the assertion before explaining it
basics
~20 sMessages earn their place when the default output is uninformative — assertTrue, assertNotNull — or when the same assertion runs many times and you need to know which input failed. They hurt when they restate the assertion, or when they go stale and describe intent the code no longer has.
solid answer
~50 sAsk what the default report already tells you. `assertEquals` prints `expected: <X> but was: <Y>`, which is often the whole story; a message saying "values should be equal" adds nothing. But `assertTrue(condition)` prints only `expected: <true> but was: <false>` — no clue what the condition was — so boolean and null assertions almost always deserve a message, ideally one naming the subject and the observed state. The second case is identity: when an assertion runs inside a loop, a generated group, or a data-driven test, the message is what tells you *which* input failed. Use a `Supplier<String>` so that context costs nothing on the passing path. Messages hurt when they restate the assertion, when they encode stale intent after a refactor — a message that lies is worse than none — and when a rich message papers over a weak assertion. Prefer strengthening the assertion (assert the value, not a boolean) over explaining a vague one.
go deeper
Know that assertTrue and assertNotNull failures say almost nothing on their own, so those assertions get a message.
Add the repetition case — loops and data-driven runs need the input identity in the message — and use the supplier overload when the text is computed.
Reason from the triage perspective: what a CI log alone must convey, when strengthening the assertion beats explaining it, and how stale messages actively mislead.
Turn it into a suite-wide convention and review rule so failure output stays trustworthy as the suite grows, and treat consistently uninformative failures as a maintenance defect.
## Start from the default output JUnit 5 renders a failure from the assertion itself before your message is considered: - `assertEquals(2, count)` → `expected: <2> but was: <1>`. Informative. - `assertTrue(order.isPaid())` → `expected: <true> but was: <false>`. Useless on its own: it does not say what was checked or on which object. - `assertNotNull(user)` → `expected: not <null>`. Same problem. So the first rule is mechanical: assertions whose default output does not name the subject — `assertTrue`, `assertFalse`, `assertNotNull`, `assertNull` — should carry a message. Assertions with a real comparison often do not need one. ## The second rule: identity under repetition A single assertion in a small test is located by its stack trace line number. That breaks down when the same line executes many times: a loop over rows, a generated group of assertions built from a collection, a data-driven run over many inputs. The stack trace points at the line but not at the iteration. The message must therefore carry the discriminator — the id, the index, the input value: ```java assertEquals(expected, row.total(), () -> "wrong total for order " + row.id()); ``` Use the `Supplier<String>` overload here: the context is computed only for the failure, so you can afford to include a lot of it. ## The third rule: prefer a stronger assertion over an explanatory message A long message attached to `assertTrue(list.size() > 0 && list.get(0).isActive())` is a smell. Splitting it into value assertions gives you better default output for free, with less prose to maintain. Explaining a weak assertion is a workaround; strengthening it is the fix. Similarly, comparing whole value objects instead of field-by-field lets the framework's comparison do the explaining. ## When messages actively hurt 1. **Restatement.** "should be equal", "must not be null", "assertion failed" — these consume a line and add zero information. 2. **Staleness.** A message describing a business rule that has since changed misleads whoever reads the failure at 2am. Unlike code, messages are not type-checked, so nothing forces them to keep up with a refactor. If a message states intent, keep it short and general enough to survive. 3. **Overlong text.** Dumping an entire object graph inline can bury the actual comparison in CI logs, especially where reporters truncate. Include the discriminator and the interesting field, not everything. 4. **Messages as documentation.** Explaining *why* the expectation exists usually belongs in the test's name or a comment, not in every assertion. A well-named test method already states intent; the message should state observation. ## Practical conventions worth stating in an interview - Boolean and null assertions: message required. - Comparisons in loops or data-driven runs: message required, and it names the input. - Comparisons in straight-line tests: message optional; add one only when the failure would be ambiguous. - Always use the supplier form when the message concatenates or calls anything. - Write the message so it reads as an observation about the system ("order 42 was not paid after settlement"), not as a restatement of the assertion. ## The reader you are writing for The audience is someone triaging a red pipeline who did not write the test, cannot reproduce locally, and has the CI log and nothing else. That framing settles most arguments: if the log line alone would not tell that person which behaviour broke and on which data, the message is doing too little; if it makes them scroll past three screens of dumped state to find the comparison, it is doing too much.
- Why do boolean assertions need messages more than equality assertions?assertEquals renders both operands, so the failure already shows what was expected and what was observed. assertTrue collapses everything into a boolean before the framework sees it, so the report can only say expected true but was false — the subject, the operands and the reason are all lost, and only the message can restore them.
- How do you keep failure messages from going stale?Keep them observational rather than normative: describe what was seen and on which input instead of restating a business rule that may change. Keep intent in the test name, review messages in the same diff as behaviour changes, and prefer stronger assertions whose default output stays correct automatically.
saying these in an interview costs you the question
- Adding "should be equal" style messages to every assertEquals
- Leaving assertTrue with no message and calling the stack trace enough
- Using a message to justify a weak or compound boolean assertion instead of splitting it
- Dumping huge object graphs into an eagerly built message string
- Treating failure messages as documentation of business rules