Ben
April 2, 2020
Recently we started moving to Micronaut and Kotlin as the main development language/framework. As it is more inline with our desire to move to serverless and is better suited to microservices then the technologies we currently use.
The mix of Kotlin and Micronaut proved to work very well together for nearly everything, however, when it came to testing, we found that there where some complications that where not immediately clear.
As with everything in Micronaut, there is auto wiring available for the test framework. This is all very well documented on the Micronaut Test examples pages. Giving examples of a few test frameworks, including KotlinTest (now known as Kotest). This is the primary resource we used for referencing while trying to get the tests to execute correctly.
The main thing to know is the use of @MicronautTest and @MockBean. These allow the Micronaut auto wiring to work for the tests, and allow you to replace beans with mock beans for unit tests. While using this approach I found that sometimes I would encounter some strange behaviour, if the annotation @MockBean does not define the class then it does not always work, but sometimes does.
@MockBean
fun auditProducer(): AuditProducer = mockk(relaxed = true)
We have a bean that uses Kafka to send some audit data on interactions with the service. We were trying to test that the messages to Kafka where correct, however when we used the above code to mock the KafkaClient producer the tests would fail with the error 'Multiple possible bean candidates found' (details below)! After some time debugging we randomly put the class into the annotation and the test then correctly got the mocked KafkaClient! Even though this fixed the issue we still have some tests that work fine without the class defined in the annotation.
Failed to inject value for parameter [auditProducer] of class: com.intergral.micronaut.audit.AuditInterceptor
Message: Multiple possible bean candidates found: [com.intergral.micronaut.audit.$AuditInterceptorAsActionTest$AuditProducer0Definition$Intercepted, com.intergral.micronaut.AuditProducer$Intercepted]
Path Taken: new AuditInterceptorAsActionTest([AuditInterceptor auditInterceptor]) --> new AuditInterceptor([AuditProducer auditProducer],HttpClientAddressResolver httpClientAddressResolver)
io.micronaut.context.exceptions.DependencyInjectionException: Failed to inject value for parameter [auditProducer] of class: com.intergral.micronaut.audit.AuditInterceptor
Message: Multiple possible bean candidates found: [com.intergral.micronaut.audit.$AuditInterceptorAsActionTest$AuditProducer0Definition$Intercepted, com.intergral.micronaut.AuditProducer$Intercepted]
Path Taken: new AuditInterceptorAsActionTest([AuditInterceptor auditInterceptor]) --> new AuditInterceptor([AuditProducer auditProducer],HttpClientAddressResolver httpClientAddressResolver)
When using Micronaut you will quite often define an interface rather than the implementation, e.g. for HTTP or Kafka clients. This means that at runtime the class you get is something that implements this interface, and not just your definition. This is also true during testing, when you define a MockBean for a KafkaClient or HttpClient - what you receive in the test is some mock implementation of this.
The problem is that the Micronaut runtime is expecting the mock to meet its needs for the client. Meaning it will wrap the mock bean in the normal interceptors that it uses during runtime. The mock framework however is expecting the mock it created, and not the Micronaut wrapper. This can result in some errors if you have missed the documentation that states it is needed (like I did).
@MicronautTest
class AuditInterceptorTest(auditInterceptor: AuditInterceptor) : BehaviorSpec({
given("A call to an audited PUT method, with params, and not audit return") {
`when`("we have auth data, with supported method") {
then("we get the audit message we expect") {
val methodInvocation = mockk<methodinvocationcontext<any, any="">>()</methodinvocationcontext<any,>
auditInterceptor.intercept(methodInvocation)
val auditProducerMock = auditInterceptor.auditProducer
verify { auditProducerMock.sendAudit(any()) }
}
}
}
}) {
@MockBean(AuditProducer::class)
fun auditProducer(): AuditProducer = mockk(relaxed = true)
}
So lets say you have a test like above, you are likely to encounter an error like the following.
No lock present for object: AuditProducer(#67)
java.lang.IllegalStateException: No lock present for object: AuditProducer(#67)
at io.micronaut.runtime.context.scope.refresh.RefreshScope.getLock(RefreshScope.java:158)
at io.micronaut.runtime.context.scope.refresh.RefreshInterceptor.intercept(RefreshInterceptor.java:46)
at io.micronaut.aop.chain.MethodInterceptorChain.proceed(MethodInterceptorChain.java:69)
This is basically saying that the AuditProducer is not what you think it is, if you see this error it means that you have missed a call to getMock. In this case the fix is to add a call to getMock before the verify call.
val auditProducerMock = getMock(auditInterceptor.auditProducer)
We found that when using the @MockBean annotation for mocking beans with Micronaut and KotlinTest you should always define the class in the annotation. If you do not you could encounter some errors with duplicate beans which can take some time to resolve.
We also found that it is very easy to forget the call to getMock so if you start getting errors about 'No lock present for object'. Check that you have the correct calls to the getMock before calls to verify.
Experienced developer in various languages, currently a product owner of nerd.vision leading the back end architecture.