Skip to content

Instantly share code, notes, and snippets.

@bkahlert
Last active September 13, 2024 08:15
Show Gist options
  • Save bkahlert/79cf9709b9056d8c9b612ef8dd4cc291 to your computer and use it in GitHub Desktop.
Save bkahlert/79cf9709b9056d8c9b612ef8dd4cc291 to your computer and use it in GitHub Desktop.
Mocking non-mocks using mockk / misassignment bug

Mocking non-mocks using mockk / misassignment bug

The Kotlin mocking library mockk lets's you mock a class like this:

class Foo {
    fun foo() = "foo"
}

val foo = mockk<Foo>()
every { foo.foo() } returns "bar"  // ๐Ÿ‘ˆ make the foo mock return "bar"

foo.foo() shouldBe "bar"

It also throws an error if you try to mock an instance that is no mock:

val foo = Foo()
every { foo.foo() } returns "bar"  // ๐Ÿ‘ˆ trying to mock an instance that is no mock

Missing mocked calls inside every { ... } block: make sure the object inside the block is a mock

But if you look at the following example, mocking non-mock instances seems to work:

class Bar {
    fun bar() = "bar"
}

class Foo(private val bar: Bar) {
    fun foo() = bar.bar()
}

val foo = Foo(mockk())
every { foo.foo() } returns "baz"  // ๐Ÿ‘ˆ make the foo instance return "baz"

foo.foo() shouldBe "baz"

The above code works! foo is no mock but the foo.foo() call actually returns baz.

How is that?

During the every call, mockk switches to the recording mode.

It records calls to mocks and assigns the return value to the recorded invocation.

Calls to non-mocks are not recorded, but the invocation takes place nonetheless.

In the example foo.foo() is called. foo.foo() internally calls bar.bar() whereat bar is a mock.
That is, the bar.bar() call is recorded and the return value baz is assigned to the bar.bar() call.

In short, syntactically it looks like we are mocking the non-mock foo, but in reality we are accidentally interacting with the mock bar which foo delegates to.

This becomes more obvious when we have Foo.foo() modify the output of bar.bar():

class Bar {
    fun bar() = "bar"
}

class Foo(private val bar: Bar) {
    fun foo() = "foo-" + bar.bar() // ๐Ÿ‘ˆ prepend `foo-` to its return value
}

val foo = Foo(mockk())
every { foo.foo() } returns "baz" 

foo.foo() shouldBe "foo-baz"       // ๐Ÿ‘ˆ only the returned value of `bar.bar()` is modified

Conclusion

Mocking non-mocks using mockk is not possible.

If you find yourself in a situation where you think you are mocking a non-mock, you are probably interacting with a mock that the non-mock delegates to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment