skip to content

What is the difference between Kotest's shouldBeSameInstanceAs and shouldBe, and when would you assert the former in a real test?

level: juniorimportance: should knowfreq 30%

answer

  1. shouldBe = value, sameInstanceAs = ===
  2. Data class: equal but not identical
  3. Cache-hit test needs identity
  4. Defensive copy = shouldBe + shouldNotBeSameInstanceAs
  5. Never assert identity on boxed primitives

basics

~20 s

shouldBe asserts the two values compare equal (equals, or Kotest's type-aware comparison). shouldBeSameInstanceAs asserts they are literally the same object in memory — a reference check like ===. Two equal data class instances pass the first and fail the second.

solid answer

~50 s

`shouldBe` is a value comparison; `shouldBeSameInstanceAs` is a reference comparison, the matcher form of Kotlin's `===`. There is also the negated `shouldNotBeSameInstanceAs`. ```kotlin val a = User(1, "Ann") val b = User(1, "Ann") a shouldBe b // passes: data class equality a shouldBeSameInstanceAs b // fails: different objects ``` You assert identity when identity is the behaviour under test: - a **cache** or memoization returns the stored object on the second call rather than rebuilding it; - a **singleton** or object pool hands out one instance; - a **defensive copy** was made — here you assert `shouldNotBeSameInstanceAs`, proving the returned collection is not the internal one; - a builder that returns `this` for chaining. Everywhere else, assert values. Identity assertions on ordinary results are over-specification: they break the moment an implementation legitimately returns an equal-but-new object.

code

kotlin · 9 lines
kotlin
// cache hit: identity is the contract
val a = registry.lookup("eur")
val b = registry.lookup("eur")
a shouldBeSameInstanceAs b

// defensive copy: equal contents, different object
val exposed = account.transactions()
exposed shouldBe internalList
exposed shouldNotBeSameInstanceAs internalList

go deeper

for a junior

State the value-versus-reference distinction and give the data class example where one passes and the other fails.

for a middle

Name concrete cases where identity is the contract — cache hits, singletons, defensive copies, fluent this — and the negated matcher.

for a senior

Emphasise over-specification: identity assertions couple tests to implementation and should appear only where reuse or copying is the behaviour under test.

for a principal

Tie it to API design: whether a type has value semantics determines which assertion is meaningful, so the choice is really a statement about the domain model.

## The two notions of "the same" Every JVM language has to distinguish *equal* from *identical*. Kotlin spells them `==` (which calls `equals`) and `===` (reference identity). Kotest gives each a matcher: - `actual shouldBe expected` — value comparison. For most types this is `equals`; Kotest additionally compares arrays by contents and produces type-aware failure messages. - `actual shouldBeSameInstanceAs expected` — reference comparison. True only when both names point at the same object. - `actual shouldNotBeSameInstanceAs expected` — the negation, which is often the more useful of the pair. ## Why the distinction is testable at all For a `data class`, equality is generated component-wise, so two separately constructed instances with the same contents are equal but not identical. For a class without an `equals` override, equality *is* identity, and the two matchers coincide — which is why a candidate who only ever tests plain classes may never have noticed the difference. ```kotlin data class User(val id: Long, val name: String) class Session(val id: Long) // no equals override User(1, "Ann") shouldBe User(1, "Ann") // passes User(1, "Ann") shouldNotBeSameInstanceAs User(1, "Ann") // passes Session(1) shouldNotBe Session(1) // no equals override -> not equal ``` ## Where identity assertions are the right tool **Caching and memoization.** The whole point of a cache is that the second call does not do the work again. Asserting `first shouldBe second` proves nothing — a cache that recomputes an equal value would also pass. `first shouldBeSameInstanceAs second` is the assertion that actually tests caching. ```kotlin val first = registry.lookup("eur") val second = registry.lookup("eur") first shouldBeSameInstanceAs second ``` **Singletons and pools.** Same reasoning: the contract is "one instance", so identity is the contract. **Defensive copying.** Here you want the negation. If a class exposes an internal list, callers can mutate your state; the fix is returning a copy, and the test that proves it is: ```kotlin val exposed = account.transactions() exposed shouldBe internalList // equal contents exposed shouldNotBeSameInstanceAs internalList // but not the same object ``` The pair of assertions together says exactly what defensive copying means. Either one alone is incomplete. **Fluent builders.** `builder.withName("x") shouldBeSameInstanceAs builder` documents that the builder mutates and returns itself rather than producing a new one. **Interning / flyweights.** Small-value caches (a currency registry, an enum-like lookup) are usually specified in terms of instance reuse. ## Where identity assertions are a mistake Asserting identity on an ordinary result over-specifies the implementation. If a mapper today returns the very object it was given and tomorrow returns an equal copy, the behaviour has not changed for any caller who compares by value — but an identity assertion turns that refactoring into a red test. Tests should pin down the contract, not the object graph. A second, subtler mistake is using identity to "strengthen" a passing value assertion — writing both when only the value one is meaningful. That is noise that will eventually block a harmless change. ## Common confusions worth naming - **"shouldBe checks reference equality for arrays."** No — Kotest compares array contents. If you want array identity you must use `shouldBeSameInstanceAs`. - **"If they're equal they must be the same instance."** Only for types whose `equals` is identity. - **"If they're the same instance the values must be equal."** True in practice, and it is why `shouldBeSameInstanceAs` is the strictly stronger assertion — which is exactly why it should be used sparingly. - **Boxed primitives.** Identity assertions on boxed numbers are a trap: small values may be cached by the JVM and larger ones not, so identity results can differ by value. Never assert identity on boxed primitives or strings. ## Interview-ready summary Value versus reference; `shouldBe` is `equals`-flavoured, `shouldBeSameInstanceAs` is `===`. Use identity when reuse or copying *is* the behaviour (cache hit, singleton, defensive copy, fluent `this`), and value comparison for everything else, because identity assertions couple the test to implementation detail.

  • You want to prove a getter returns a defensive copy. Which assertions do you write?
    Two of them: `returned shouldBe internal` to prove the contents are right, and `returned shouldNotBeSameInstanceAs internal` to prove it is a different object. Only the pair expresses defensive copying; the value assertion alone would pass even if the internal list were exposed directly.
  • For a class with no equals() override, do shouldBe and shouldBeSameInstanceAs behave the same?
    Effectively yes — the inherited `equals` is identity, so both assertions succeed only for the same object. The distinction becomes visible as soon as the type has value semantics, such as a data class or a class with a hand-written `equals`.

Two identical printed copies of a contract are equal; only one of them is the copy sitting in your drawer. shouldBe checks the wording, shouldBeSameInstanceAs checks it is the very sheet of paper you filed.

saying these in an interview costs you the question

  • Using shouldBeSameInstanceAs as a generally 'stronger' shouldBe
  • Testing a cache with shouldBe and claiming it proves the value was cached
  • Thinking Kotest's shouldBe compares arrays by reference
  • Asserting identity on boxed primitives or interned strings
  • Saying equal implies same instance for data classes

context