2

I'm trying to use Mockk to mock a method with context receiver:

class MyClass {
    // The method I'm going to mock
    context(CallContext)
    fun myMethod(a: Int) Int { a }
}

It's hard to get the instance of CallContext in the unit test. So I hope I can write a unit test in this way:

/* 
This should work, but I can't get the CallContext instance 
with(callContextInstance) {
    Every { mockedMyClass.myMethod(1) } returns 2
}
*/

// I hope a unit test can be written like this... But it won't compile now.
with(any<CallContext>) {
    Every { mockedMyClass.myMethod(1) } returns 2
}

So what should I do? Thanks in advance.

ffcactus
  • 152
  • 8

2 Answers2

1

At the time of writing, MockK does not support context receivers, and it probably won't until context receivers are released - so some time after Kotlin 1.9, so maybe in 2024).

(Context receivers are explicitly described as not ready for production. A stable release won't be available until after the K2 release, and the K2 beta is targeted for Kotlin 1.9, which has a planned release of December 2023.)

That said, if anyone wants to attempt support, then get stuck in! MockK is an community supported open source project that accepts PRs.

Confounding factors

However, there are two hinderances before MockK can fully support context receivers:

  • Context receivers aren't finished, nor is their current implementation stable. KT-10468. Their implementation could change significantly. Trying to implement support for a moving target is challenging.
  • IDE support is limited, which makes developing with them difficult (follow KTIJ-20857 for updates)

Workaround

In the meantime, you could adjust your code to allow for manual mocking.

First, adjust MyClass to either be an open class, or introduce a new interface that describes the behaviour you want to mock (code to an interface).

/** Describe the API that [MyClass] will implement */
interface MyClassSpec {
  context(CallContext)
  fun myMethod(a: Int): Int
}

And then implement the interface

/** Concrete implementation of [MyClassSpec] */
class MyClass: MyClassSpec {
  context(CallContext)
  override fun myMethod(a: Int): Int = a
}

Now in your test you can create a mock by creating an anonymous object that implements MyClassSpec - and now you have a mock that supports context receivers.

@Test
fun myTest() {
  val myClassMock = object : MyClassSpec {
    context(CallContext)
    override fun myMethod(a: Int): Int = 123
  }
}
aSemy
  • 5,485
  • 2
  • 25
  • 51
1

If I get the idea what exactly do you try to mock, the following works with mockk 1.13.3:

interface CallContext

class MyClass {
    context(CallContext)
    fun myMethod(a: Int): Int = a
}

class ContextMockTest {
    private val myClassMock: MyClass = mockk()

    @Test
    fun mockContextWorks() {
        every {
            with(any<CallContext>()) {
                myClassMock.myMethod(any())
            }
        } returns 123

        val context = object : CallContext { }

        with(context) {
            assertEquals(123, myClassMock.myMethod(1))
        }

        verify {
            with(any<CallContext>()) {
                myClassMock.myMethod(1)
            }
        }
    }
}

A link to the gist just in case

Motorro
  • 227
  • 2
  • 10