skip to content

Equality & Type Matchers

The core matchers cover equality, identity, nullability, and runtime type checks, and the type matchers smart-cast their subject for follow-up assertions. Knowing shouldBe's equality semantics vs reference identity is a classic gotcha question.

on this pageshow

explore

questions

5

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?

level: middleimportance: must knowfreq 45%

answer

  1. Type-dispatched Eq, not raw ==
  2. Arrays compared by contents
  3. Data class = field-level diff message
  4. Fallback is plain equals
  5. BigDecimal scale / floating point still bite

basics

~20 s

Kotest'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 lines
kotlin
val 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 toStrings

go deeper

for a junior

Know that shouldBe asserts equality, that arrays compare by contents, and that floating-point comparisons need plusOrMinus.

for a middle

Explain the type dispatch, name the array and data-class cases, and state clearly that the fallback is plain equals with all its traps.

for a senior

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.

for a principal

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

context

open as a page

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%

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.

open as a page

Kotest offers both shouldBeInstanceOf<T>() and shouldBeTypeOf<T>(). What is the difference in what they assert, and what can neither of them check?

level: middleimportance: should knowfreq 38%

basics

~20 s

shouldBeInstanceOf<T>() passes when the value is a T or any subtype. shouldBeTypeOf<T>() requires the runtime class to be exactly T, so a subclass fails. Neither can check generic type arguments, because those are erased at runtime.

open as a page

A Kotest assertion using shouldBe fails even though the values look right — for example two BigDecimals, two computed doubles, or two entities differing only in a generated id and timestamp. Which Kotest matchers do you reach for instead, and why?

level: seniorimportance: should knowfreq 30%

basics

~20 s

shouldBe ultimately uses equals, which is the wrong test in those cases. Use plusOrMinus for floating point tolerance, shouldBeEqualComparingTo for BigDecimal (compareTo ignores scale), and the reflective matchers shouldBeEqualToIgnoringFields or shouldBeEqualToUsingFields for objects with noisy fields.

open as a page

Kotest's shouldBeInstanceOf<T>() and shouldNotBeNull() return a value rather than Unit. What do they return, how does that interact with Kotlin smart casts, and why does it matter for test readability?

level: seniorimportance: should knowfreq 28%

basics

~20 s

They return the value narrowed to the asserted type — T for shouldBeInstanceOf, the non-null T for shouldNotBeNull — and they carry Kotlin contracts so the original variable also smart-casts afterwards. That replaces as casts and !! with assertions that fail with real messages.

open as a page