skip to content

A team migrating tests writes assertEquals("user name should match", expectedName, actualName) with three String arguments using org.junit.jupiter.api.Assertions; it compiles but the failure output looks nonsensical. What changed about where the failure message goes compared with JUnit 4's org.junit.Assert?

level: juniorimportance: should knowfreq 44%

answer

  1. JUnit 4: message first
  2. JUnit 5: message last
  3. three Strings compile silently
  4. message compared as expected value
  5. assertAll heading is the exception — it leads

basics

~20 s

JUnit 4 put the message first; JUnit 5 puts it last. With three Strings the call still compiles, so JUnit 5 compares the message text against the expected name and treats the actual name as the message — hence the confusing output.

solid answer

~50 s

In JUnit 4's `org.junit.Assert`, the optional message was the **first** parameter: `assertEquals(message, expected, actual)`. In JUnit 5's `org.junit.jupiter.api.Assertions` it is the **last**: `assertEquals(expected, actual, message)`, with a `Supplier<String>` variant alongside. When all three arguments are Strings the old order still type-checks, so nothing warns you. JUnit 5 then compares `"user name should match"` with `expectedName` and prints `actualName` as the message — a failure that reads as if the assertion itself is broken. With non-String types the compiler catches it, which is why the bug hides in String-heavy tests. The rationale is that the primary subject of the assertion should come first and the optional detail last, consistent across the whole API — including `assertThrows` and the assumption methods. Fix it with an IDE or OpenRewrite JUnit 4-to-5 migration, which reorders arguments mechanically; hand-migrating String assertions is where teams get bitten.

code

java · 5 lines
java
// JUnit 4
org.junit.Assert.assertEquals("user name should match", "ada", actual);

// JUnit 5 - message trails
org.junit.jupiter.api.Assertions.assertEquals("ada", actual, "user name should match");

go deeper

for a junior

Recall the positions — JUnit 4 first, JUnit 5 last — and why an all-String call compiles anyway.

for a middle

Add the reasoning behind the change (optional trailing argument, room for the Supplier overload) and how to spot the silent case.

for a senior

Talk about migrating safely at repository scale: automated refactoring, banning the legacy Assert import, and the transition period where both conventions coexist.

for a principal

Use it as an example of API-evolution risk — a source-compatible signature change that silently changes meaning — and argue for tooling plus lint enforcement over reviewer vigilance.

## The two signatures JUnit 4, `org.junit.Assert`: ```java assertEquals(String message, Object expected, Object actual) assertTrue(String message, boolean condition) ``` JUnit 5, `org.junit.jupiter.api.Assertions`: ```java assertEquals(Object expected, Object actual, String message) assertEquals(Object expected, Object actual, Supplier<String> messageSupplier) assertTrue(boolean condition, String message) ``` The optional message moved from the front to the back, and the same convention holds throughout Jupiter: `assertNotNull(actual, message)`, `assertThrows(type, executable, message)`, `assumeTrue(condition, message)`. ## Why the mistake survives compilation Most mis-ordered calls fail to compile — `assertEquals("msg", 1, 1)` under JUnit 5 has no matching overload for `(String, int, int)` in the intended sense and will either not compile or resolve unexpectedly. The dangerous case is when expected and actual are themselves Strings: `(String, String, String)` matches `assertEquals(Object expected, Object actual, String message)` perfectly. The code compiles cleanly, and at runtime JUnit compares your human-readable message to the expected value and reports the actual value as if it were the message: ``` ada ==> expected: <user name should match> but was: <adam> ``` Read that carefully and it is obvious; read it while triaging a red build and it looks like a broken test. ## Why the change was made The API redesign put required, primary arguments first and the optional message last. That reads better (`assertEquals(expected, actual, ...)` mirrors how you say it), matches the varargs/optional-trailing-parameter convention common in modern Java APIs, and — the practical driver — makes room for the `Supplier<String>` overload as a trailing lambda, which is far more natural in the last position. It also keeps `assertAll`'s heading, which is genuinely a leading label for a group rather than a per-assertion message, visibly different from an ordinary message. ## Migrating safely - Use tooling: the IDE's "migrate to JUnit 5" refactoring or OpenRewrite recipes reorder arguments mechanically across the whole codebase. - Do not mix imports. Having `org.junit.Assert` and `org.junit.jupiter.api.Assertions` both statically imported in one file is how mis-ordered calls slip through; enforce a ban on the JUnit 4 `Assert` import once migration is done. - Grep for three-String assertion calls after a hand migration; they are the population where the compiler cannot help. - Watch mixed-runtime projects. Tests still running on the JUnit 4 API (via the Vintage engine) keep the old order, so the same repository can legitimately contain both conventions during a transition — one more reason to finish migrations rather than let them linger. ## Related positional details The message is last on assumptions too, and `fail` takes only the message (no expected/actual), so `fail("...")` is unaffected by the change. `assertAll`'s heading is the one place where a String legitimately comes first — it labels the group, not a single assertion. Knowing that exception keeps the rule easy to state: in Jupiter the message always trails, except that a group heading leads.

  • Why does the mistake compile only when the values under test are Strings?
    The JUnit 5 overload is assertEquals(Object expected, Object actual, String message), so a (String, String, String) call matches it exactly. With other types, for example (String, int, int), no overload fits the intended reading, so the compiler rejects it or the mismatch is obvious. Only String-valued assertions type-check under both orderings.

saying these in an interview costs you the question

  • Claiming JUnit 5 also takes the message first
  • Assuming the compiler always catches a mis-ordered message
  • Thinking JUnit detects a message-looking String and reorders it at runtime
  • Keeping both org.junit.Assert and Assertions statically imported after migration
  • Saying the assertAll heading proves messages come first in JUnit 5

context