https://kotlinlang.org logo
#mockk
Title
# mockk
m

Mykola Gurov

11/14/2021, 2:27 PM
Hi, I'm stuck trying to nail down the root cause of this issue https://github.com/spring-projects/spring-boot/issues/28604 . There I wrap a Spring-managed bean as a Mockk's spy and inject back to the spring context as a
@Primary
bean - a convenient technique to spy on Beans and inject "strange" behaviors. All works good till we hit Spring AOP-proxied beans, e.g
@Cacheable
in my example. Some funky interactions are happening there and we end up seeing this exception 🧵:
Copy code
Caused by: io.mockk.MockKException: no answer found for: TargetSource(child of #1#2).getTarget()
	at io.mockk.impl.stub.MockKStub.defaultAnswer(MockKStub.kt:93) ~[mockk-1.12.0.jar:na]
	at io.mockk.impl.stub.MockKStub.answer(MockKStub.kt:42) ~[mockk-1.12.0.jar:na]
	at io.mockk.impl.recording.states.AnsweringState.call(AnsweringState.kt:16) ~[mockk-1.12.0.jar:na]
	at io.mockk.impl.recording.CommonCallRecorder.call(CommonCallRecorder.kt:53) ~[mockk-1.12.0.jar:na]
	at io.mockk.impl.stub.MockKStub.handleInvocation(MockKStub.kt:266) ~[mockk-1.12.0.jar:na]
	at io.mockk.impl.instantiation.JvmMockFactoryHelper$mockHandler$1.invocation(JvmMockFactoryHelper.kt:23) ~[mockk-1.12.0.jar:na]
	at io.mockk.proxy.jvm.advice.Interceptor.call(Interceptor.kt:21) ~[mockk-agent-jvm-1.12.0.jar:na]
	at io.mockk.proxy.jvm.advice.BaseAdvice.handle(BaseAdvice.kt:42) ~[mockk-agent-jvm-1.12.0.jar:na]
	at io.mockk.proxy.jvm.advice.jvm.JvmMockKProxyInterceptor.interceptNoSuper(JvmMockKProxyInterceptor.java:45) ~[mockk-agent-jvm-1.12.0.jar:na]
	at org.springframework.aop.TargetSource$Subclass0.getTarget(Unknown Source) ~[spring-aop-5.3.12.jar:5.3.12]
This doesn't look very good to me as I'd expect spies to be transparent for non-preprogrammed calls, but instead I can reproduce the situation with
Copy code
@Component
class Service() {

    @Cacheable("response_cache")
    fun respondCached(input: String) = prefix + "_" + Instant.now().toEpochMilli()

}

@Configuration
class InjectSpiesConfiguration {
    @Bean
    @Primary
    fun spiedService(service: Service): Service {
        val spyk = spyk(service)
        return spyk
    }
}

@SpringBootTest
class MockkSpykingTest(
    @Autowired private val spiedService: Service
) {


    @Test
    fun `hardcoded unwrapping`() {
        //NB: no failures when the following line is commented.
        every { spiedService.respondCached("empty") } returns ""

        val firstUnwrap = (spiedService as Advised).targetSource.target
        val secondUnwrap = (firstUnwrap as Advised).targetSource.target

        SoftAssertions.assertSoftly {
            it.assertThat(secondUnwrap).isNotInstanceOf(Advised::class.java)
            it.assertThat(secondUnwrap).isInstanceOf(Service::class.java)
        }

    }
}
Now, I understand that wrapping AOP'ed bean with spyk is probably not the best of the ideas and we should rather unwrap it first (e.g.
AopTestUtils.getUltimateTargetObject(service)
) but still - why does the mock's spy fails that invocation instead of preserving transparent behavior for non-intercepted calls?
m

Mattia Tommasone

11/14/2021, 9:16 PM
thanks for the detailed explanation 🙂 this looks pretty nasty indeed - my first guess would be that somehow AOP’s class decoration is being performed after the one done by mockk’s spy, so somehow mockk is convinced there is no answer defined for the method you are trying to invoke because it was added later by AOP. I don’t really know if this is the actual cause nor if there is a way to change the order to make sure mockk always does its magic last (and i don’t even know if it would be right), but it looks like a bug indeed.
m

Mykola Gurov

11/15/2021, 4:54 PM
39 Views