In JUnit 5, what is the difference between assertEquals and assertSame, and when would you deliberately reach for assertSame?
answer
- assertEquals -> equals(), assertSame -> ==
- expected.equals(actual): expected side drives
- assertSame for cache, singleton, return this
- Integer cache -128..127 makes assertSame lie
- failure prints @identityHash when toString matches
basics
~20 sassertEquals compares with equals() — value equality. assertSame compares with == — the same object in memory. Use assertSame only when identity is the contract you are testing: caches, interned or flyweight instances, singletons, or a method that returns this.
solid answer
~50 s`assertEquals(expected, actual)` delegates to `expected.equals(actual)` (null-safe: both null passes). `assertSame(expected, actual)` uses reference identity, `==`. For most value objects you want `assertEquals` — two separately constructed `BigDecimal`s or DTOs with a proper `equals` are equal but not the same. `assertSame` is the right tool when identity itself is the behaviour under test: a cache returning the *same* instance on a second lookup, `Optional.empty()` or `Collections.emptyList()` returning the shared singleton, an interned enum or constant, a fluent builder returning `this`, or verifying that a method did not defensively copy. There is also `assertNotSame` for the opposite contract — proving a defensive copy *was* made. The common bug is using `assertSame` by accident on value objects: it passes locally when the JVM happens to reuse an instance (small Integer cache, string literals) and then fails when the value changes. If you did not mean "identical object", use `assertEquals`.
code
java · 16 lines@Test
void valueVsIdentity() {
Money a = new Money("10.00", "EUR");
Money b = new Money("10.00", "EUR");
assertEquals(a, b); // passes: equals() compares amount + currency
assertNotSame(a, b); // they are distinct objects
// identity IS the contract here: second lookup must not rebuild
Rates rates = new CachingRates();
assertSame(rates.forDay(TODAY), rates.forDay(TODAY));
// fluent builder must return itself
OrderBuilder builder = new OrderBuilder();
assertSame(builder, builder.withCustomer("ACME"));
}go deeper
State the core fact cleanly: assertEquals uses equals(), assertSame uses ==, default to assertEquals. One concrete example (two equal Money objects) is enough.
Add the mechanics: null handling, that the expected argument's equals() runs, that a class without an equals override makes assertEquals degrade to identity, and the Integer-cache / string-literal traps.
Frame it as intent and over-specification: name the identity contracts worth asserting (cache, singleton, return this, defensive copy) and explain why an identity assertion on a value object is a latent failure.
Talk about equality as an API contract — which types in the codebase are values and must have equals/hashCode, which are entities identified by id, and how that decision drives test style and comparison utilities across the suite.
## The two notions of equality Java has two distinct questions you can ask about a pair of references. `a == b` asks *are these the same object* — do both references point to one location on the heap. `a.equals(b)` asks *do these represent the same value* — and the answer is whatever the class's `equals` method says. JUnit 5's `org.junit.jupiter.api.Assertions` gives you one assertion per question: `assertSame` for identity and `assertEquals` for value equality. `assertEquals(Object expected, Object actual)` is implemented as a null-safe `equals` call: if `expected` is null the assertion passes only when `actual` is null; otherwise it calls `expected.equals(actual)`. Note the direction — the *expected* value's `equals` runs. That matters when the two arguments are of different classes with asymmetric `equals` implementations. `assertSame(Object expected, Object actual)` performs `expected == actual` and nothing else. No `equals` method is consulted, so it cannot be fooled by a broken or overly permissive `equals`, and it cannot be satisfied by an equal-but-distinct object. ## Why the distinction bites in practice If a class does not override `equals`, `Object.equals` falls back to `==`, so for such classes `assertEquals` and `assertSame` behave identically. That is exactly what makes the mistake hard to catch: a test written with the wrong assertion passes for the wrong reason. Two frequent traps: - **Boxed integers.** `Integer.valueOf(100) == Integer.valueOf(100)` is true because of the JVM's `Integer` cache for values in −128..127, but `Integer.valueOf(1000) == Integer.valueOf(1000)` is false. An `assertSame` on boxed numbers passes for a small fixture value and fails when someone bumps the test data. - **String literals.** Compile-time constants are interned, so `assertSame("ab", "ab")` passes, while `assertSame("ab", "a" + suffix)` computed at runtime fails. Never assert string content with `assertSame`. ## When assertSame is genuinely correct Identity is a real contract in several places, and there `assertSame` says something `assertEquals` cannot: - **Caching / memoization.** `assertSame(first, cache.get(key))` proves the second lookup did not recompute or reconstruct the value. An `assertEquals` here would pass even with a broken cache that rebuilds the object every time. - **Shared singletons and constants.** `Optional.empty()`, `Collections.emptyList()`, `Comparator.naturalOrder()`, enum constants and interned domain constants are documented to return one shared instance. - **Fluent APIs.** A builder method that must return `this` for chaining: `assertSame(builder, builder.withName("x"))`. - **No-copy / copy contracts.** `assertNotSame(input, service.process(input))` proves the service returned a defensive copy rather than handing back the caller's mutable object; `assertSame` proves the opposite contract when the API promises to pass the instance through. - **Exception propagation.** When a wrapper must rethrow the *original* exception object, `assertSame(original, thrown.getCause())` is stronger than comparing messages. ## Failure output Both assertions throw `AssertionFailedError`. When `assertSame` fails on two objects whose `toString()` is identical — the very common case, since equal-looking objects are why you got confused — JUnit appends the identity hash so the message is not the useless `expected: <Point(1,2)> but was: <Point(1,2)>`; it renders as `expected: Point(1,2)@1b6d3586 but was: Point(1,2)@4554617c`. Seeing an `@hash` in a diff is your cue that you are looking at an identity assertion. ## Choosing in real tests Default to `assertEquals`. It is what you want for values, DTOs, records, collections and results, and it keeps the test resilient to harmless changes in how instances are created. Reach for `assertSame` only when you can state the identity contract in one sentence — "the cache must return the very same instance", "the builder must return itself". If you cannot say that sentence, `assertSame` is over-specification: it will break the day someone introduces a legitimate copy, and it tells the reader something about the implementation rather than the behaviour. A related discipline: for value objects, `assertEquals` is only as good as the class's `equals`. If a class has no `equals` override, `assertEquals` silently degrades to identity and will fail on a logically-correct result. Records and Lombok `@Value`/`@EqualsAndHashCode` give you a value `equals` for free; for hand-written classes, either implement `equals` or assert field by field.
- A test uses assertSame on two boxed Integer values and passes. Why might it start failing after someone changes the fixture number?The JVM caches `Integer` instances for values from −128 to 127, so `Integer.valueOf(5) == Integer.valueOf(5)` is true and the identity assertion passes by accident. Change the fixture to 1000 and autoboxing creates two distinct objects, so `==` is false and the test fails even though the values are equal. The test was never asserting what the author meant; `assertEquals` is the correct assertion.
- assertEquals passes on two objects you believe are different. What would you check first?The `equals` implementation of the expected object's class — `assertEquals` calls `expected.equals(actual)`, so an overly permissive or field-omitting `equals` makes the assertion vacuous. Check whether `equals` compares every field that matters, whether the class uses `getClass()` vs `instanceof`, and whether a Lombok/record-generated `equals` excludes a field. Also confirm the two arguments are not literally the same reference.
- Your class has no equals() override. What does assertEquals do then?It falls back to `Object.equals`, which is reference identity, so `assertEquals` behaves exactly like `assertSame` and fails for two logically-equal instances. The fix is either to give the type a real value `equals` (a record or a generated implementation), or to assert the relevant fields individually / with a recursive-comparison assertion library.
assertEquals asks "are these two banknotes the same amount?"; assertSame asks "is this the very same banknote, same serial number?". Most of the time you care about the amount.
saying these in an interview costs you the question
- Saying assertEquals compares references and assertSame compares content — the two are swapped.
- Claiming assertSame is 'stricter so it is safer' and using it for value objects; it over-specifies and breaks on legitimate copies.
- Assuming assertSame on strings is reliable because 'strings are interned' — only compile-time constants are.
- Not knowing that assertEquals calls equals() on the expected argument, so a broken or asymmetric equals makes it meaningless.
- Believing assertEquals throws a NullPointerException when the expected value is null; both-null passes.