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?
answer
- JUnit 4: message first
- JUnit 5: message last
- three Strings compile silently
- message compared as expected value
- assertAll heading is the exception — it leads
basics
~20 sJUnit 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 sIn 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// 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
Recall the positions — JUnit 4 first, JUnit 5 last — and why an all-String call compiles anyway.
Add the reasoning behind the change (optional trailing argument, room for the Supplier overload) and how to spot the silent case.
Talk about migrating safely at repository scale: automated refactoring, banning the legacy Assert import, and the transition period where both conventions coexist.
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