kotlinmockitoarrow-ktkotlin-context-receivers

Mocking a service that uses a context receiver Raise<BusinessError> using Mockito


Source code available on Github: https://github.com/codependent/context-receiver-sample

Suppose you're testing a service ServiceOne that has a dependency on ServiceTwo.

ServiceTwo has a method call() with a context receiver of Raise<BusinessError>:

class ServiceOne(private val serviceTwo: ServiceTwo) {

    fun call(code: String): String {
        val result = either {
            val r = serviceTwo.call(code)
            r
        }

        return result.fold(
            { "-1" }){ it }
    }
}
class ServiceTwo {

    context(Raise<BusinessError>)
    fun call(id: String): String {
        return id
    }

}
sealed class BusinessError {
    object SomeError: BusinessError()
}

I'd like to mock the dependency on ServiceTwo using Mockito. In order to do so, when mocking I need to provide a context for its context receiver so I wrapped it with an either block:

class ServiceOneTests {


    @Test
    fun `should return the correct aggregation result`() {

        val serviceTwo = mock(ServiceTwo::class.java)
        val serviceOne = ServiceOne(serviceTwo)

        val result = either {
            `when`(serviceTwo.call("1")).thenReturn("1")

            val mockResult = serviceTwo.call("1")
            assertEquals("1", mockResult)

            serviceOne.call("1")
        }

        when(result){
            is Either.Left -> fail()
            is Either.Right -> assertEquals("1", result.value)
        }
    }

}

When executing the test, you can see that mockResult, which is serviceTwo.call(code) has been correctly mocked, having a "1" value.

However, this test ends up failing because r in val r = serviceTwo.call(code) in serviceOne.call("1") is null.

I suspect it must be because the context (either {}) used during the mock setup is different from the context established in fun serviceOne.call(), which has it's own either {} block.

How would you fix this so the mocked call doesn't return null?


Solution

  • I suspect it must be because the context (either {}) used during the mock setup is different from the context established in fun serviceOne.call(), which has it's own either {} block.

    How would you fix this so the mocked call doesn't return null?

    This is indeed because Mockito only mocks for the exact instance passed of Either, and you should use an argument matcher instead.

    You can do this by casting to the underlying signature, and passing an explicit matcher for the context receiver.

    val serviceTwo = mock(ServiceTwo::class.java)
    val serviceOne = ServiceOne(serviceTwo)
    
    `when`(serviceTwo::call.invoke(anyObject(), eq("1"))).thenReturn("1")
    

    I had to define a helper to fix the non-null issue of Mockito, without brining in mockito-kotlin.

    object MockitoHelper {
        fun <T> anyObject(): T {
            Mockito.any<T>()
            return uninitialized()
        }
        @Suppress("UNCHECKED_CAST")
        fun <T> uninitialized(): T = null as T
    }
    

    Perhaps MockK will add support for this in the future, but I would recommend using interfaces and stubs instead of mocking. This would heavily simplify this pattern.