skip to content

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%

answer

  1. shouldBe == equals, and equals is the wrong question
  2. plusOrMinus for doubles
  3. shouldBeEqualComparingTo for BigDecimal scale
  4. IgnoringFields vs UsingFields vs ComparingFields
  5. Better fix: inject Clock and id generator

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.

solid answer

~40 s

Each failure is a case where `equals` does not express the intended notion of sameness. - **Doubles/floats**: `(0.1 + 0.2) shouldBe 0.3` fails on binary representation. Assert with tolerance: `(0.1 + 0.2) shouldBe (0.3 plusOrMinus 1e-9)`. - **BigDecimal**: `BigDecimal("1.0")` and `BigDecimal("1.00")` are unequal because `equals` compares scale. Use `shouldBeEqualComparingTo`, which compares with `compareTo`. - **Entities with generated fields**: an id assigned by the database or a `createdAt` timestamp makes whole-object equality useless. Use `actual.shouldBeEqualToIgnoringFields(expected, Order::id, Order::createdAt)` to exclude them, or `shouldBeEqualToUsingFields(expected, Order::customer, Order::total)` to name the only fields that matter. `shouldBeEqualToComparingFields` compares property-by-property reflectively, which also rescues classes with identity-based `equals`. The judgement point: prefer fixing the model when you can — if `equals` is wrong for the domain, the matcher is a workaround. Reach for ignoring/using-fields when the noisy value is genuinely environmental.

code

kotlin · 6 lines
kotlin
(0.1 + 0.2) shouldBe (0.3 plusOrMinus 1e-9)

BigDecimal("1.0") shouldBeEqualComparingTo BigDecimal("1.00")

saved.shouldBeEqualToIgnoringFields(expected, Order::id, Order::createdAt)
saved.shouldBeEqualToUsingFields(expected, Order::customerId, Order::total)

go deeper

for a junior

Recall that shouldBe uses equals, so doubles need plusOrMinus and BigDecimal scale differences need a compareTo-based matcher.

for a middle

Name the three matcher families and give the entity example with a generated id and timestamp.

for a senior

Lead with diagnosis, then argue for injecting a Clock and id generator over excluding fields, and note the coverage each exclusion costs.

for a principal

Turn it into a convention: control non-determinism at the seams, define equality on domain types deliberately, and treat field-exclusion matchers as a documented exception rather than the default.

## The framing `shouldBe` answers "are these equal?" using the type's own `equals` (plus Kotest's array and reporting specialisations). A whole family of test failures come from `equals` being a *different* question from the one the test wants to ask. A senior candidate is expected to recognise the category and know the matcher for each member. ## 1. Floating point: representation, not logic `0.1 + 0.2` is `0.30000000000000004` in IEEE-754 doubles. No amount of squinting makes `equals` accept it. Kotest's answer is a tolerance matcher: ```kotlin (0.1 + 0.2) shouldBe (0.3 plusOrMinus 1e-9) ``` `plusOrMinus` builds a range-style expectation that `shouldBe` accepts. Pick the tolerance from the domain — an accumulated financial figure and a physics simulation deserve different epsilons — and never from "whatever made the test pass". The deeper judgement: if money is involved, the real fix is usually to stop using `Double` in the model at all. ## 2. BigDecimal: value versus scale `BigDecimal.equals` is documented to compare *both* unscaled value and scale, so `1.0` and `1.00` are unequal objects representing the same number. `compareTo` compares numeric value only. Kotest exposes the latter: ```kotlin BigDecimal("1.0") shouldBeEqualComparingTo BigDecimal("1.00") // passes BigDecimal("1.0") shouldBe BigDecimal("1.00") // fails ``` `shouldBeEqualComparingTo` works for any `Comparable`, so it also helps with types whose `equals` and `compareTo` disagree in other ways. Alternatively normalise (`setScale`) before comparing — but then be explicit about which scale is the contract. ## 3. Objects with fields the test cannot predict This is the most common one in service tests. You saved an `Order`, the database assigned `id = 87`, and `createdAt` is `Instant.now()`. The expected object you constructed in the test has neither. Whole-object `shouldBe` fails on fields nobody is actually asserting. Kotest's reflective equality matchers (in `io.kotest.matchers.equality`) address this directly: ```kotlin actual.shouldBeEqualToIgnoringFields(expected, Order::id, Order::createdAt) actual.shouldBeEqualToUsingFields(expected, Order::customerId, Order::total, Order::status) actual.shouldBeEqualToComparingFields(expected) ``` - **IgnoringFields** — compare everything except the listed properties. Best default for generated ids and timestamps, because a newly added field is compared automatically; the test keeps its coverage as the model grows. - **UsingFields** — compare only the listed properties. Tighter and more readable when you genuinely care about three fields of a twenty-field DTO, but it silently ignores everything new. Use it as a deliberate projection, not as a way to make a test pass. - **ComparingFields** — property-by-property reflective comparison of the whole object. This is the rescue for classes that have no meaningful `equals` (framework entities, generated classes) and for getting a field-level failure report out of a type that would otherwise compare by identity. ## 4. Rather than a matcher: fix the shape of the test The matcher list is not the whole answer, and a strong candidate says so. - **Inject the clock and the id generator.** If `createdAt` comes from an injected `Clock` fixed in the test, and ids come from a stubbed generator, whole-object `shouldBe` works and you have also gained control over time in your tests. That is usually the better engineering answer. - **Assert on a projection.** Map the result to a small data class or a `Pair` of the fields you care about, and compare that with `shouldBe`. This makes the intent explicit in the code rather than in matcher arguments. - **Fix `equals` if it is genuinely wrong.** If your domain says two `Money` values with the same amount and currency are the same, that belongs in `equals`, not in every test. ## Trade-offs to state out loud - Ignoring/using-fields matchers rely on reflection over property names, so a rename that your IDE misses in a string form would break silently — prefer the `KProperty` reference form, which the compiler checks. - Every excluded field is coverage you have given up. Excluding `id` and `createdAt` is fine; a test that ignores half its object is asserting nothing. - Tolerance assertions hide genuine drift if the epsilon is set generously. Justify the number. ## The one-paragraph answer "`shouldBe` is `equals`, and these are all cases where `equals` is not the question. Tolerance for doubles via `plusOrMinus`, `compareTo` semantics for `BigDecimal` via `shouldBeEqualComparingTo`, and for entities with generated ids or timestamps either exclude those fields with `shouldBeEqualToIgnoringFields` or — better — inject a fixed `Clock` and a stub id generator so plain equality works again."

  • When would you choose shouldBeEqualToUsingFields over shouldBeEqualToIgnoringFields?
    UsingFields is a deliberate projection: use it when only a handful of properties of a large object are part of the contract under test, and say so in the test name. IgnoringFields is the safer default for generated ids and timestamps, because any newly added property is still compared — with UsingFields, new fields silently escape assertion.
  • What is the better fix for a test failing on a createdAt timestamp?
    Inject a `Clock` (or a time provider) into the component and fix it in the test, so the timestamp becomes deterministic and plain whole-object equality works again. Excluding the field with a matcher is a workaround that permanently removes that field from the test's coverage; controlling time also makes other time-dependent behaviour testable.

saying these in an interview costs you the question

  • Loosening the tolerance until the double assertion passes, with no domain justification
  • Claiming BigDecimal equals ignores scale
  • Using shouldBeEqualToUsingFields on most fields and calling it a strong assertion
  • Deleting the assertion on the noisy field entirely instead of ignoring it explicitly
  • Assuming Kotest compares arbitrary objects field-by-field without being asked

context