Kotest offers both shouldBeInstanceOf<T>() and shouldBeTypeOf<T>(). What is the difference in what they assert, and what can neither of them check?
answer
- instanceOf = is-a, subtypes pass
- typeOf = exact runtime class
- Both reified, both return narrowed T
- Erasure: List<String> check only proves List
- Exact-type assertions are brittle by design
basics
~20 sshouldBeInstanceOf<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.
solid answer
~50 sBoth are reified inline matchers, so you write the type as a type argument rather than passing a `KClass`. - `value.shouldBeInstanceOf<Payment>()` is an `is`-style check: it accepts `Payment` and every subtype. Use it when the contract is "returns something that is a Payment". - `value.shouldBeTypeOf<CardPayment>()` compares the runtime class for exact equality. A subclass of `CardPayment` fails it. Use it when the point of the test is that a factory or deserializer produced *that* concrete type and not a specialisation or proxy. Both return the value narrowed to `T`, so you can keep asserting on it without a cast. The shared limitation is erasure: `shouldBeInstanceOf<List<String>>()` only proves the value is a `List` — the `String` argument is not present at runtime and is not checked. A list of `Int` passes. If element types matter, assert on the elements.
code
kotlin · 11 linesopen class Failure(val message: String)
class Timeout(message: String) : Failure(message)
val e: Any = Timeout("slow")
e.shouldBeInstanceOf<Failure>() // passes
e.shouldBeTypeOf<Failure>() // fails: actual class is Timeout
e.shouldBeTypeOf<Timeout>() // passes
val f = e.shouldBeInstanceOf<Failure>()
f.message shouldBe "slow" // no cast neededgo deeper
Recall that one accepts subtypes and one demands the exact class, and that both let you keep using the value afterwards.
Give a concrete subtype example, explain the reified/narrowed return, and name erasure as the shared blind spot.
Argue about brittleness: default to instanceOf, justify typeOf, and show how the narrowed return replaces casts in assertion chains.
Discuss what type assertions imply about API contracts — asserting on concrete types couples tests to implementation choices, so prefer asserting on behaviour or sealed-hierarchy branches.
## The two matchers Kotest gives you two distinct type assertions, and the distinction is the classic subtype-versus-exact-type one. ```kotlin sealed interface Result data class Ok(val value: Int) : Result open class Failure(val message: String) : Result class Timeout(message: String) : Failure(message) val r: Result = Timeout("slow") r.shouldBeInstanceOf<Failure>() // passes: Timeout IS-A Failure r.shouldBeTypeOf<Failure>() // fails: runtime class is Timeout, not Failure r.shouldBeTypeOf<Timeout>() // passes: exact match ``` `shouldBeInstanceOf<T>()` behaves like Kotlin's `is` operator: true for `T` and any subtype. `shouldBeTypeOf<T>()` compares the runtime class of the value against `T` for equality, so anything more specific fails. ## Why both exist Most assertions want the loose one. You care that a repository returned *a* `DomainEvent`, that an error channel produced *an* `IllegalStateException` (subtypes acceptable), that a sealed-hierarchy branch was taken. The exact one earns its keep in a narrower set of cases: - **Factories and mappers** where returning a more specific subtype would be a behaviour change your test is guarding: "this builder must produce a plain `ArrayList`, not a synchronized wrapper". - **Deserialization** where the polymorphic resolver must land on exactly one concrete class. - **Guarding against accidental proxying or decoration**, where a framework may hand back a subclass that passes an `is` check while being the wrong thing. Because exact-type assertions are brittle by design — introducing a legitimate subclass breaks them — reach for `shouldBeInstanceOf` unless exactness is genuinely the property under test. ## Both are reified They are `inline fun <reified T>`, so the type is written as a type argument and no `KClass` literal is needed. This has two nice consequences: the type is checked at compile time (a typo is a compile error, not a runtime surprise), and the matcher can return the value already narrowed to `T`. There is also a non-reified matcher form, `beInstanceOf<T>()`, usable with `should(...)` and composable with the matcher combinators — worth knowing exists, but the `shouldBe...` forms are what you write day to day. ## The narrowed return Both matchers return the value as `T`, which is the reason to use them instead of a bare `is` check followed by a cast: ```kotlin val ok = handle(command).shouldBeInstanceOf<Ok>() ok.value shouldBe 42 // or in one expression handle(command).shouldBeInstanceOf<Ok>().value shouldBe 42 ``` If the type is wrong you get a proper assertion failure naming both the expected and the actual class. The alternative — `(handle(command) as Ok).value shouldBe 42` — throws a `ClassCastException`, which reads like a bug in the test rather than a described expectation. ## What neither can do: erasure Generic type arguments are erased on the JVM. A `List<String>` and a `List<Int>` are the same runtime class. ```kotlin val xs: Any = listOf(1, 2, 3) xs.shouldBeInstanceOf<List<String>>() // passes! only List-ness is checked ``` This is a favourite follow-up because candidates often claim the reified type argument "solves erasure". It does not: `reified` means the type argument is available to the *inline body* at compile time, and the body can only perform the runtime checks the JVM supports — which for generics means the raw class. If element types matter, assert on the elements: ```kotlin val xs: Any = listOf(1, 2, 3) val list = xs.shouldBeInstanceOf<List<*>>() list.forEach { it.shouldBeInstanceOf<String>() } ``` The same caveat applies to nullable type arguments and to any type whose distinguishing information lives only in the type system. ## Related but different: identity Neither matcher says anything about *which* instance you have. `shouldBeSameInstanceAs` is the reference check; `shouldBe` is the value check. A candidate who conflates "exact type" with "same object" has not understood the split. ## Interview-ready summary Subtype versus exact runtime class; both reified; both return the narrowed value so you keep asserting without a cast and get a readable failure instead of a `ClassCastException`; neither sees generic type arguments, because erasure. Default to `shouldBeInstanceOf`, and justify `shouldBeTypeOf` when you use it.
- Does the `reified` type parameter let shouldBeInstanceOf<List<String>>() verify the element type?No. `reified` makes the type argument available inside the inlined body at compile time, but the check it can perform at runtime is limited by JVM erasure, so only `List` is verified. A `List<Int>` passes. Assert on the elements individually if their type matters.
- When would you deliberately choose shouldBeTypeOf over shouldBeInstanceOf?When the exact concrete class is the property under test — a factory that must return a specific implementation, a polymorphic deserializer that must resolve to one concrete subtype, or a guard against a framework handing back a decorated subclass. Otherwise the exactness makes the test break on harmless refactorings that introduce a subtype.
saying these in an interview costs you the question
- Saying the two matchers are aliases for each other
- Claiming reified type parameters defeat erasure for generic arguments
- Using `as` casts plus shouldBe and calling the ClassCastException a good failure message
- Reaching for shouldBeTypeOf by default and breaking tests whenever a subtype appears
- Confusing exact-type assertions with instance identity