skip to content

How do JUnit 5's assertEquals, assertSame and assertArrayEquals behave when one or both arguments are null, and how should that shape the way you write null assertions?

level: middleimportance: should knowfreq 34%

answer

  1. both null -> passes everywhere; never NPE
  2. assertEquals: expected==null ? actual==null : expected.equals(actual)
  3. assertArrayEquals prints 'expected array was <null>'
  4. prefer assertNull/assertNotNull for intent + overloads
  5. assertEquals(1, aLong) fails: Integer.equals(Long) is false

basics

~20 s

All of them are null-safe: both arguments null passes, one null fails with a message naming which side was null — no NullPointerException. Still prefer assertNull and assertNotNull for null checks, because they state intent and avoid overload-resolution problems with a bare null literal.

solid answer

~40 s

`assertEquals` does a null-safe check: if the expected value is null it passes only when the actual value is null, otherwise it calls `expected.equals(actual)` — so a null actual never throws, it just fails. `assertSame(null, null)` passes because `null == null`. `assertArrayEquals(null, null)` passes; one null side fails with `expected array was <null>` rather than an NPE. Null *elements* inside object arrays and iterables are compared null-safely too. Even so, write `assertNull(result)` and `assertNotNull(result)` rather than `assertEquals(null, result)`. Three reasons: the intent is explicit in the assertion name; the failure message is clearer; and a bare `null` literal can hit overload ambiguity or silently select an unintended overload. A related trap is boxed-type mismatch: `assertEquals(1, someLong)` fails because `Integer.equals(Long)` is false regardless of the numeric value.

code

java · 14 lines
java
@Test
void nullAndBoxing() {
    assertEquals(null, null);            // passes, no NPE
    assertSame(null, null);              // passes
    assertArrayEquals(null, null);       // passes
    // assertArrayEquals(new int[]{1}, null); // fails: actual array was <null>

    assertNull(repository.findById("missing"));   // states intent
    assertNotNull(repository.findById("known"));

    Long count = service.count();        // boxed Long
    // assertEquals(1, count);           // FAILS: Integer.equals(Long) is false
    assertEquals(1L, count);             // passes
}

go deeper

for a junior

Know that null arguments are handled safely — both null passes, one null fails — and that assertNull/assertNotNull are the idiomatic checks.

for a middle

Explain the exact comparison rule, the array and iterable behaviour, and the overload/boxing hazards that make a bare null or an int literal misbehave.

for a senior

Point at the false-positive risk: assertions that pass because both sides are null prove nothing, so tests must observe at least one non-null value, and Optional-returning APIs should be asserted as Optionals.

for a principal

Discuss nullability as a design decision — Optional or non-null-by-default annotations at module boundaries — so that tests rarely need null assertions at all.

## Null is a value, not a crash JUnit 5's assertions in `org.junit.jupiter.api.Assertions` are deliberately null-tolerant, because a test asserting on a value that unexpectedly turns out to be null should report a readable failure, not blow up with a `NullPointerException` inside the assertion library. **`assertEquals(Object, Object)`** uses a null-safe comparison: if the expected reference is null, the assertion passes exactly when the actual reference is also null; otherwise it calls `expected.equals(actual)`, and a well-behaved `equals` returns false for a null argument (that is required by the `Object.equals` contract). So: - `assertEquals(null, null)` → passes - `assertEquals("a", null)` → fails, `expected: <a> but was: <null>` - `assertEquals(null, "a")` → fails, `expected: <null> but was: <a>` Note the asymmetry in which object's `equals` runs — the *expected* one. A class with a sloppy `equals` that returns true for null would make `assertEquals(badObject, null)` pass; that is a defect in the class, not in JUnit. **`assertSame` / `assertNotSame`** compare with `==`, so `assertSame(null, null)` passes and there is nothing to dereference — inherently null-safe. **`assertArrayEquals`** treats null arrays as a comparable state: both null passes, one null fails with a message that names which side was null (`expected array was <null>` / `actual array was <null>`). Inside `Object[]`, null elements are compared null-safely, and the failure message reports the index. **`assertIterableEquals`** behaves the same way for null iterables and null elements. ## Why assertNull and assertNotNull still exist Given that `assertEquals(null, x)` works, why prefer `assertNull(x)`? 1. **Intent.** `assertNull(result)` reads as "this must be absent". `assertEquals(null, result)` reads as a value comparison that happens to use null, and a reviewer must pause on it. 2. **Message quality.** `assertNull` produces `expected: <null> but was: <Order[id=7]>` with the assertion name in the stack trace, so the report says *what kind* of check failed. 3. **Overload hazards.** A bare `null` literal is typed as the null type and is applicable to several reference overloads. In JUnit 5 the object overload is normally selected without complaint, but in code that mixes generics, casts or its own overloaded helpers, a bare null can produce an ambiguous-method compile error or select an overload you did not intend, forcing casts like `assertEquals((Object) null, result)`. Using `assertNull` removes the question. 4. **Static analysis.** Nullability checkers and IDE inspections understand `assertNull`/`assertNotNull` and can narrow types afterwards; `assertEquals(null, x)` teaches them nothing. A practical corollary: `assertNotNull` is often written as a defensive prelude to further assertions (`assertNotNull(order); assertEquals(7, order.id());`). That is reasonable but often redundant — the following assertion would throw an NPE anyway. Prefer `assertNotNull` when null is a *meaningful* outcome to rule out, or group the checks with `assertAll` so a null does not stop the rest of the test from reporting. ## The neighbouring trap: boxed types While on the subject of arguments that silently do the wrong thing, the most common non-null equality surprise is numeric boxing. `assertEquals(1, service.count())` where `count()` returns `long` may resolve to the `(long, long)` overload and pass — but `assertEquals(1, someLongObject)` where the actual is a `Long` object resolves to `(Object, Object)`, boxes the literal to `Integer`, and fails: `Integer.equals(Long)` is false for every value, producing the baffling `expected: <1> but was: <1>`. JUnit softens this by appending the types when the rendered strings match, so you see the class names in the message. The fix is to make the literal's type match — `assertEquals(1L, actual)` — or to unbox explicitly. The same class of problem appears with `char` versus `int`, and with `BigDecimal`, where `equals` also compares scale so `new BigDecimal("1.0")` is not equal to `new BigDecimal("1.00")`. ## Writing null-aware tests well - Use `assertNull` / `assertNotNull` for presence checks; keep `assertEquals` for values. - When a method returns `Optional`, assert on the `Optional` (`assertTrue(result.isPresent())`, `assertEquals(Optional.of(x), result)`) rather than unwrapping and null-checking. - Do not write assertions that pass when everything is null — `assertEquals(expected.name(), actual.name())` succeeds when both names are null, which may mean the fixture never populated them. If null-vs-null is a plausible false positive, assert the value explicitly first. - When asserting a whole object graph, remember that a null field on both sides passes silently; a test that never observes a non-null value is not proving much.

  • A test reports 'expected: <1> but was: <1>' and fails. What is going on?
    The two values render identically but are different types — almost always a boxed-number mismatch, such as an `int` literal compared against a `Long`, because `Integer.equals(Long)` is false for any value. JUnit detects the identical string rendering and appends type information to the message so you can see it. Fix it by matching the literal's type (`1L`) or by comparing primitives so the primitive overload is selected.
  • Is assertNotNull before a chain of assertions worth writing?
    Usually only when null is a genuine possible outcome you want named in the report; otherwise the next assertion would throw a NullPointerException and fail the test anyway, just with a less descriptive message. A good middle ground is to state it once at the top of the test, or to wrap the group in `assertAll` so a null does not hide the remaining checks. Avoid sprinkling it before every dereference — it is noise.

saying these in an interview costs you the question

  • Believing assertEquals throws a NullPointerException when the actual value is null.
  • Writing assertEquals(null, x) as the standard idiom instead of assertNull, losing intent and message quality.
  • Assuming assertArrayEquals(null, null) fails, or that a null array is an error rather than a comparable state.
  • Not knowing that assertEquals calls equals on the expected argument, so which side is null changes which code runs.
  • Writing field-by-field assertions that pass because both sides are null and calling the object 'verified'.

context