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?
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.