skip to content

A relaxed MockK mock backs a repository whose method returns a generic type parameter `T`, and the test dies with a `ClassCastException` at the call site rather than inside MockK. Why does relaxed mode struggle with generic return types, and what is the fix?

level: seniorimportance: should knowfreq 32%

answer

  1. erasure → MockK sees Any / the upper bound
  2. default child mock ≠ the inferred type
  3. compiler's checked cast fails in YOUR code
  4. fix: every { store.load<User>("k") } returns …
  5. bound zeroable → silent wrong value, no exception

basics

~20 s

Type erasure: at runtime MockK only sees the erased return type (often Any), so it generates a child mock of that, and the compiler-inserted cast in your calling code fails. Fix it by stubbing the call explicitly with a real value of the expected type.

solid answer

~50 s

Relaxed mode picks a default value from the **runtime** return type. For `fun <T : Any> load(key: String): T`, erasure means the runtime signature returns `Object`/`Any`, so MockK creates a relaxed mock of `Any` — it has no way to know the call site wanted a `User`. Kotlin then inserts a checked cast at the call site (`val user: User = store.load("u1")`), and that cast throws `ClassCastException` pointing at *your* code, not at MockK. The stack trace makes it look like a production bug, which is why this one eats time. The fix is to stop relying on the default for that call: ```kotlin every { store.load<User>("u1") } returns User("u1") ``` An explicit stub supplies a value of the right type and takes precedence over relaxation. The general rule: **relaxed mode is for calls whose return value the test does not care about; any generic-returning call whose value flows into your assertions must be stubbed explicitly.**

code

kotlin · 12 lines
kotlin
interface Store {
    fun <T : Any> load(key: String): T
}

val store = mockk<Store>(relaxed = true)

// val user: User = store.load("u1")
// ClassCastException at THIS line: erased to Any -> relaxed child mock of Any

every { store.load<User>("u1") } returns User("u1")

val user: User = store.load("u1")   // now fine: explicit stub wins over the default

go deeper

for a junior

Know that a generic return type plus a relaxed mock can produce a ClassCastException, and that stubbing the call explicitly fixes it.

for a middle

Explain erasure: MockK sees the erased return type, generates a default for it, and the compiler's checked cast at the call site fails.

for a senior

Add the diagnosis path — flip to a strict mock to get a named unstubbed call — and the silent variant where a zeroable upper bound yields a wrong value with no exception at all.

for a principal

Read it as an API signal: inference-driven generic returns are hostile to both stubbing and readability, so prefer concrete return types or an explicit type argument in code you own.

## The setup ```kotlin interface Store { fun <T : Any> load(key: String): T } val store = mockk<Store>(relaxed = true) val user: User = store.load("u1") // ClassCastException ``` Nothing here is a MockK bug, but the failure is genuinely confusing the first time. ## Why erasure breaks the default generator Relaxed mode works by asking: *what type does this member return?* — and answering with a per-type default. It reads that type at **runtime**, from the method's signature as the JVM sees it. JVM generics are erased. `fun <T : Any> load(key: String): T` compiles to a method returning `Object` (`Any`). The upper bound is all the runtime signature preserves — with `T : Number` the erased return is `Number`. MockK therefore generates a default for the erased type: for `Any`, a relaxed child mock of `Any`. The *call site*, meanwhile, knows exactly what it wanted. Kotlin infers `T = User` from the expected type and emits a checked cast on the returned value. The generated child mock is not a `User`, the cast fails, and the exception is thrown in your code — not inside MockK, and not with any mention of stubbing. That location is what sends people debugging production code that is perfectly correct. ## Why it does not always blow up The cast only fails when it happens. Two situations hide the problem: - **Erased flow.** If the value is stored in a variable of the erased type or passed straight into something generic, no cast is emitted at that point and the mock travels further. It surfaces later — sometimes in a wholly unrelated assertion. - **Bound-compatible defaults.** If the erased upper bound is a type MockK can zero out (say a `T : Number` returning `0`), you get a plausible value rather than an exception, which is worse: a silent wrong answer instead of a loud failure. So "my generic method works fine with relaxed" is not evidence of safety; it is evidence that the cast has not been reached yet. ## The fix Stub explicitly and give MockK a real value: ```kotlin every { store.load<User>("u1") } returns User("u1") ``` Explicit stubs always take precedence over relaxed defaults, so the generated child mock never appears. Supplying the type argument at the `every` call site also keeps the DSL unambiguous. When the same generic call is made with several types in one test, stub each of them, distinguished by their arguments — and if the arguments are identical, that itself is a design smell worth naming: the code is asking one method for two different types with no information to distinguish them. Related design fixes, in code you own: - **Return a concrete type.** `fun loadUser(key: String): User` is mockable, readable, and needs no cast. - **Take the type explicitly.** A `KClass`/`Class` parameter (`load(key, User::class)`) is often clearer than inference-driven generics and gives MockK a matchable argument. ## Recognising it in the wild A quick checklist when a `ClassCastException` appears in a MockK test: 1. Is the mock relaxed? If not, you would have seen `no answer found for:` instead. 2. Does the failing call site have a generic return type? 3. Does the exception mention a MockK-generated class or a bare `Object`/`Any` being cast to your domain type? Three yeses means an unstubbed generic call, every time. The confirming experiment is to flip the mock to `mockk<Store>()` — the strict version replaces the cast failure with a precise message naming exactly the call you forgot. ## Interview framing The answer: *erasure — MockK only sees the erased return type, generates a default for that, and the compiler's checked cast at the call site fails, throwing inside your code rather than inside MockK. Fix by stubbing the call explicitly; and note that when the erased bound happens to be a type MockK can zero out you get a silent wrong value instead of an exception, which is why generic-returning calls should not rely on relaxed defaults at all.*

  • Why is the exception thrown at your call site rather than inside MockK?
    MockK legitimately returns a value for the erased return type — from its point of view nothing is wrong. The type mismatch is only detected by the checked cast the Kotlin compiler inserts where the inferred type is applied, which lives in your test or production code. That is why the stack trace looks like a domain bug and why flipping the mock to strict, which fails inside MockK with a named call, is the fastest way to identify the real cause.
  • The generic method's bound is `T : Number` and the test gets `0` instead of an exception. Is that better?
    It is worse. MockK zeroes out the erased bound, so the call returns a plausible `0` and no cast fails; the test then asserts on a number that no collaborator ever produced. Failures land far from the missing stub, or the test passes for the wrong reason — which is the argument for stubbing every generic-returning call whose value the test actually reasons about.

saying these in an interview costs you the question

  • "MockK can read the inferred type parameter at runtime" — it is erased; only the upper bound survives.
  • "It's a MockK bug that the exception is in my code" — MockK returned a valid value for the erased type; the compiler's cast is what fails.
  • "Adding relaxUnitFun or making the mock more relaxed fixes it" — more relaxation cannot invent the right type.
  • "If a generic call doesn't throw, relaxed handled it correctly" — the cast may simply not have been reached, or the bound was zeroable.
  • "Casting the result yourself with `as User` fixes it" — that is the same cast that is already failing.

context