In Kotest, what equality does the shouldBe matcher actually apply — how does it compare arrays, data classes and collections, and where does that differ from a plain Kotlin == check?
answer
- Type-dispatched Eq, not raw ==
- Arrays compared by contents
- Data class = field-level diff message
- Fallback is plain equals
- BigDecimal scale / floating point still bite
basics
~20 sKotest's shouldBe does not simply call equals. It dispatches on runtime type first: arrays are compared by contents rather than by reference, iterables and maps element-wise with a positional diff, data classes with per-field failure reporting. Everything else falls back to equals.
solid answer
~50 s`shouldBe` inspects the runtime types of actual and expected and picks a specialised equality implementation (Kotest 5 keeps these under `io.kotest.assertions.eq`) before falling back to plain `equals`. What that buys you: - **Arrays**: `arrayOf(1, 2) shouldBe arrayOf(1, 2)` passes, because Kotest compares array contents. A raw `==` on arrays is reference equality and would fail. - **Data classes**: the comparison is still `equals`, but the failure message is field-oriented — it names the properties that differ instead of dumping two long `toString`s. - **Iterables and maps**: compared element-wise, and the report shows where they diverge plus missing/extra elements. - **Everything else**: ordinary `equals` semantics, with all the usual traps — `BigDecimal` scale, floating point, classes with identity-based `equals`. So `shouldBe` changes the *reporting* and the array case; for ordinary types it is still `equals`, and it does not make two unequal objects equal.
code
kotlin · 9 linesval a = arrayOf(1, 2, 3)
val b = arrayOf(1, 2, 3)
a shouldBe b // passes: contents compared
(a === b) shouldBe false // still different objects
data class User(val id: Long, val name: String, val email: String)
User(1, "Ann", "[email protected]") shouldBe User(1, "Ann", "[email protected]")
// failure names the differing property rather than dumping both toStringsgo deeper
Know that shouldBe asserts equality, that arrays compare by contents, and that floating-point comparisons need plusOrMinus.
Explain the type dispatch, name the array and data-class cases, and state clearly that the fallback is plain equals with all its traps.
Add the diagnosis angle: whole-object assertions plus field-level failure output beat per-field assertions, and know when to switch to comparingTo/reflective matchers.
Frame it as an assertion-conventions decision: what your codebase asserts on (whole aggregates vs projections), how equality is defined on domain types, and how that choice affects failure readability across a large suite.
## Why interviewers ask it People coming from JUnit assume `shouldBe` is `assertEquals` with the arguments swapped. It is not quite. Kotest treats equality as a dispatched operation, which explains both a real behaviour difference (arrays) and the much better failure output. Getting this right also tells the interviewer you know where `shouldBe` will *not* save you. ## The dispatch model When you write `actual shouldBe expected`, Kotest does not immediately call `actual == expected`. It looks at the runtime types of the two values and selects a comparison strategy. In Kotest 5 these strategies live in the `io.kotest.assertions.eq` package: there are separate implementations for nulls, arrays, iterables, maps, data classes, throwables, and a default that delegates to `equals`. The selected strategy decides both *whether* the values are equal and *what the failure message looks like*. ## Arrays: contents, not references This is the one case where Kotest's answer differs from `==`. ```kotlin val a = arrayOf(1, 2, 3) val b = arrayOf(1, 2, 3) a == b // false — reference equality a shouldBe b // passes — Kotest compares contents ``` Kotlin's `Array.equals` is inherited from `Any`, so `==` is identity. Kotest compares element by element (including nested arrays), which is almost always what a test means. If you genuinely want to assert that two references are the same object, that is `shouldBeSameInstanceAs`, not `shouldBe`. ## Data classes: same verdict, better message For a `data class`, `equals` is already generated as component-wise equality, so Kotest's verdict is the same as `==`. What changes is diagnosis: instead of "expected X but was Y" with two 300-character `toString`s that you have to diff by eye, Kotest reports which fields differ. On a domain aggregate with fifteen properties this is the difference between a five-second fix and a five-minute squint. Note what this does *not* do: it does not compare non-data classes field-by-field. A plain class without a hand-written `equals` still uses identity, so two structurally identical instances fail `shouldBe`. That surprises people who assume Kotest reflects over everything. (Kotest does offer explicit reflective matchers — `shouldBeEqualToComparingFields` and friends — but you have to ask for them.) ## Iterables and maps Lists, sets and maps are compared element-wise and the failure output describes the divergence: which elements are missing from the actual value, which are unexpectedly present, and where the first difference sits. Again this is about the report; the pass/fail answer for two `List`s matches `equals`. ## Where shouldBe is still just equals The fallback path is plain `equals`, so every equality trap of the JVM survives: - **Floating point**: `(0.1 + 0.2) shouldBe 0.3` fails. Use the tolerance form, `(0.1 + 0.2) shouldBe (0.3 plusOrMinus 1e-9)`. - **`BigDecimal`**: `BigDecimal("1.0") shouldBe BigDecimal("1.00")` fails, because `BigDecimal.equals` compares scale as well as value. Compare with `shouldBeEqualComparingTo`, which uses `compareTo`. - **Identity-based equals**: JPA entities, framework proxies and hand-rolled classes without `equals` compare by reference. - **Types you did not intend to compare**: comparing a value to something of an unrelated type simply fails at runtime rather than at compile time, so a typo in the expected value shows up as a confusing failure, not a compile error. ## Null handling `null shouldBe null` passes, and `x shouldBe null` is a legal way to assert nullity. Kotest also has the dedicated `shouldBeNull()` / `shouldNotBeNull()` matchers; the latter is worth preferring because it hands back the non-null value for the rest of the test. ## Practical guidance 1. Prefer asserting on whole objects with `shouldBe` rather than a pile of per-field assertions — the field-level failure report makes the whole-object form readable, and it catches fields you forgot to assert. 2. Do not use `shouldBe` for numeric tolerance or for scaled decimals; reach for `plusOrMinus` and `shouldBeEqualComparingTo`. 3. Do not read anything about identity into a passing `shouldBe` — it says the values are equal, not that they are the same object. 4. If you find yourself asserting on arrays a lot, remember the array behaviour is Kotest-specific; the same expression in a plain Kotlin `check(a == b)` behaves differently.
- Two structurally identical instances of a non-data class fail `shouldBe`. Why, and what would you do?Because the fallback comparison is `equals`, and a plain class without an overridden `equals` uses identity. Either give the class a real `equals` if value semantics are part of its contract, or assert with Kotest's reflective equality matchers such as `shouldBeEqualToComparingFields`, which compare property by property without touching the class.
- Does `shouldBe` passing tell you anything about object identity?No. It tells you the values compare equal under the strategy Kotest chose — for arrays that is contents, for most types it is `equals`. Two distinct objects routinely satisfy it. If identity is what you mean, use `shouldBeSameInstanceAs`, which is a reference (`===`) check.
Think of shouldBe as a receptionist who looks at what you brought before choosing a specialist: arrays go to the contents desk, data classes to the field-by-field auditor, everything else to the generic equals clerk.
saying these in an interview costs you the question
- Saying shouldBe is just assertEquals with reversed arguments and nothing else
- Claiming Kotest compares any two objects field-by-field via reflection by default
- Expecting arrays to fail because `==` on arrays is reference equality
- Using shouldBe for doubles and calling the resulting failure a Kotest bug
- Believing a passing shouldBe implies the same instance