skip to content

When an extension function is stubbed through MockK's mockkStatic, how does the extension's receiver take part in matching inside every/verify, and why do extensions declared inside a class or object behave differently?

level: seniorimportance: should knowfreq 36%

answer

  1. receiver = first parameter
  2. any<String>().ext() matches any receiver
  3. member extension -> mock the declaring object
  4. mock never intercepts a top-level extension on it
  5. inline extension = nothing to instrument

basics

~20 s

A top-level extension compiles to a static method whose first parameter is the receiver, so in every/verify the receiver is just argument zero: any<String>().shout() matches any receiver. An extension declared inside a class or object is an instance method of that class, so the file facade does not hold it — mock the declaring object or instance instead.

solid answer

~50 s

Kotlin compiles a top-level extension `fun String.shout()` into a static method on the file facade with the receiver as its **first parameter**. MockK records calls at that level, so inside `every`/`verify` the receiver behaves exactly like an argument: `every { any<String>().shout() } returns "X"` matches every receiver, while `every { "hi".shout() } returns "X"` only matches the receiver equal to `"hi"`. Verification reads the same way. An extension declared **inside** a class or object is a member: it compiles to an instance method of the declaring type that takes the receiver as a parameter. It is not on any file facade, so `mockkStatic("pkg.FileKt")` will never intercept it — you mock the declaring `object` (or the instance holding it) instead. The consequence people trip on: calling a top-level extension **on a mock** is not intercepted by that mock. `mockedUser.displayName()` where `displayName` is a top-level extension is a static call that never reaches the mock's dispatch — only `mockkStatic` on the facade will stub it.

code

kotlin · 6 lines
kotlin
mockkStatic("com.acme.GreetingsKt")

every { any<String>().shout() } returns "ANY"
every { "hi".shout() } returns "HI"

verify { "hi".shout() }

go deeper

for a junior

Know that a top-level extension is really a static function with the receiver as the first parameter, and that mockkStatic targets the file that declares it.

for a middle

Show matching on the receiver with any<T>() versus a literal, and explain why a member extension needs the declaring object mocked instead.

for a senior

Lead with the diagnosis of the mock-does-not-intercept-my-extension case and the inline limitation, and say what you would change in the design.

for a principal

Treat extension functions as a testability decision: behaviour you may need to substitute belongs on an injectable boundary, not on a top-level or inline extension.

## How an extension actually compiles An extension function is not a member of the receiver type; nothing is added to `String`. For a top-level declaration such as ```kotlin package com.acme fun String.shout(): String = uppercase() ``` the compiler emits a static method on the file facade with the signature `static String shout(String $this)`. The receiver becomes the first parameter. A call site `"hi".shout()` compiles to `GreetingsKt.shout("hi")`. That single fact explains almost everything about mocking extensions. ## Matching: the receiver is argument zero MockK intercepts the static method, and it records the receiver in the same list as the declared parameters. So inside a recording block the receiver is matched by the ordinary rules: - `every { any<String>().shout() } returns "X"` — matches whatever receiver the code under test uses. - `every { "hi".shout() } returns "X"` — matches only when the receiver is equal to `"hi"`; a call on `"bye"` falls through to the real implementation. - `verify { any<String>().shout() }` and `verify { "hi".shout() }` read the same way, and a captured slot can capture the receiver just as it captures a parameter. A very common surprise is a test that stubs a literal receiver and then observes real behaviour, because the code under test built a receiver that is only *equal-looking* — a different instance whose `equals` does not hold, for example. Matching on the receiver uses the same comparison MockK uses for arguments, so identity-versus-equality questions apply to it too. ## Member extensions are a different animal Declare the same extension inside a class or object: ```kotlin object Formatting { fun String.shout(): String = uppercase() } ``` Now it is a member of `Formatting`, compiled as an instance method taking the receiver as a parameter. It has two receivers conceptually (the dispatch receiver `Formatting`, the extension receiver `String`) and it lives on `Formatting`, not on any file facade. No amount of `mockkStatic("com.acme.FormattingKt")` will touch it. You mock the declaring object, and the call is then intercepted like any other member call. The same holds for an extension declared inside a normal class: you mock or spy the instance. ## Extensions on a mocked type are not intercepted by the mock This is the one that costs teams an afternoon. Suppose `User` has a top-level extension `fun User.displayName(): String`. In a test: ```kotlin val user = mockk<User>() every { user.name } returns "Ada" user.displayName() // NOT dispatched through the mock ``` `displayName()` is a static call to the facade; the mock never sees it. The real extension body runs, and whatever member calls it makes on the mock are what the mock answers. Two consequences: unstubbed members inside that body will blow up on a strict mock, and to replace the extension itself you must `mockkStatic` its facade. When people say "my mock ignores the extension function", this is nearly always what happened. ## Inline extensions cannot be mocked at all If the extension is declared `inline`, the compiler copies its body into every call site. There is no static method left to instrument at the call site, so `mockkStatic` has nothing to intercept and the real logic runs. This applies to most of the Kotlin standard library's extension functions. The diagnosis is simple: if the declaration says `inline`, stop trying to mock it and change the seam instead. ## Practical rules 1. Look at where the extension is **declared**, not where it is called: top level -> file facade + `mockkStatic`; inside a class/object -> mock that class/object. 2. Treat the receiver as argument zero when writing matchers, captures and verifications. 3. Never expect a mock of the receiver type to intercept a top-level extension on it. 4. `inline` means unmockable — accept it and inject a collaborator instead.

  • You mock a User and call a top-level extension on it; the test fails inside the extension body. What is happening and what are your options?
    The extension is a static call that bypasses the mock, so its real body executes and calls members on the mock that you never stubbed. Options are to stub the members the body needs, to mockkStatic the extension's file facade and stub the extension itself, or to stop using an extension for behaviour you need to substitute and inject a collaborator.
  • Can you capture the receiver of an extension function with a MockK slot?
    Yes. Because the receiver is recorded as the first argument of the static method, a slot placed in receiver position captures it exactly as it would capture a parameter. This is useful when the code under test builds the receiver internally and you want to assert on it.

saying these in an interview costs you the question

  • Thinking an extension is a member of the receiver type and so is intercepted by a mock of that type
  • Trying to mockkStatic a file facade for an extension declared inside a class or object
  • Treating the receiver as untouchable rather than as argument zero for matchers
  • Expecting inline extensions to be stubbable
  • Assuming a literal receiver in every{} matches any equal-looking instance regardless of how equality is defined

context