kotlinmicronautmicronaut-datakotest

TransactionalEventListener not invoked in @MicronautTest


Problem description While system end-to-end tests are invoking methods annotated with @TransactionalEventListener, I'm not able to invoke the same methods in narrower tests annotated with @MicronautTest.

I've tested numerous variants with both injected EntityManager and SessionFactory. @MicronautTest(transactional = false) is also tested. Calling JPA-method inside TestSvcWithTxMethod#someMethod is also tested with same result. I've also tried tests without mocking TestAppEventListener.

The below test/code yields

Verification failed: call 1 of 1: TestAppEventListener(#1).beforeCommit(any())) was not called. java.lang.AssertionError: Verification failed: call 1 of 1: TestAppEventListener(#1).beforeCommit(any())) was not called.

Calls to same mock: 1) TestAppEventListener(#1).hashCode()

Environment: Micronaut 3.7.5, Micronaut Data 3.9.3

Minimal reproducible code Test is failing as well with transactional = false

import io.kotest.core.spec.style.BehaviorSpec
import io.micronaut.test.annotation.MockBean
import io.micronaut.test.extensions.kotest5.MicronautKotest5Extension.getMock
import io.micronaut.test.extensions.kotest5.annotation.MicronautTest
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import no.mycompany.myapp.eventstore.services.appeventpublisher.testinfra.DefaultTestAppEventListener
import no.mycompany.myapp.eventstore.services.appeventpublisher.testinfra.TestAppEventListener
import no.mycompany.myapp.eventstore.services.appeventpublisher.testinfra.TestSvcWrapper

@MicronautTest
class AppEventWithBeforeCommitListenerMockTest(
    testSvcWrapper: TestSvcWrapper,
    testAppEventListener: TestAppEventListener
) : BehaviorSpec({

    given("context with app event listener") {
        `when`("calling someMethod") {
            val mockBeforeCommitTestListener = getMock(testAppEventListener)
            every { mockBeforeCommitTestListener.beforeCommit(any()) } answers {}
            every { mockBeforeCommitTestListener.afterRollback(any()) } answers {}

            testSvcWrapper.someMethod(message = "call #1")

            verify { mockBeforeCommitTestListener.beforeCommit(any()) }
        }
    }
}) {
    @MockBean(DefaultTestAppEventListener::class)
    fun mockTestAppEventListener(): TestAppEventListener = mockk()
}

TestSvcWrapper

import jakarta.inject.Singleton

@Singleton
class TestSvcWrapper(
    private val testSvcWithTxMethod: TestSvcWithTxMethod
) {
    fun someMethod(message: String) {
        testSvcWithTxMethod.someMethod(message)
    }
}

TestSvcWithTxMethod

import io.micronaut.context.event.ApplicationEventPublisher
import jakarta.inject.Singleton
import javax.transaction.Transactional

@Singleton
open class TestSvcWithTxMethod(
    private val eventPublisher: ApplicationEventPublisher<TestEvent>
) {
    @Transactional(Transactional.TxType.REQUIRES_NEW)
    open fun someMethod(message: String) {
        eventPublisher.publishEvent(TestEvent(message))
    }
}

TestEvent

import io.micronaut.core.annotation.Introspected

@Introspected
data class TestEvent(val message: String)

TestAppEventListener

interface TestAppEventListener {
    fun beforeCommit(event: TestEvent)

    fun afterRollback(event: TestEvent)
}

DefaultTestAppEventListener

import io.micronaut.transaction.annotation.TransactionalEventListener
import jakarta.inject.Singleton
import java.util.concurrent.atomic.AtomicInteger

@Singleton
open class DefaultTestAppEventListener : TestAppEventListener {

    val receiveCount = AtomicInteger()

    @TransactionalEventListener(TransactionalEventListener.TransactionPhase.BEFORE_COMMIT)
    override fun beforeCommit(event: TestEvent) {
        receiveCount.getAndIncrement()
    }

    @TransactionalEventListener(TransactionalEventListener.TransactionPhase.AFTER_ROLLBACK)
    override fun afterRollback(event: TestEvent) {
        receiveCount.getAndIncrement()
    }
}

Solution

  • The answer was found in the micronaut-test repo. Key is to inject SynchronousTransactionManager<Any>, create and then commit/rollback transaction.

    I was not able to make mock-test from question pass, most likely because of the annotations, but the following tests are working. I made some modifications to the types in question, hence I added code for the new implementations below.

    import io.kotest.core.spec.style.BehaviorSpec
    import io.kotest.matchers.shouldBe
    import io.micronaut.test.extensions.kotest5.annotation.MicronautTest
    import io.micronaut.transaction.SynchronousTransactionManager
    import io.micronaut.transaction.support.DefaultTransactionDefinition
    import no.mycompany.myapp.eventstore.services.appeventpublisher.testinfra.TestAppEventListener
    import no.mycompany.myapp.eventstore.services.appeventpublisher.testinfra.TestSvcWithTxMethod
    
    @MicronautTest(transactional = false)
    class AppEventWithBeforeCommitListenerTest(
        testSvcWithTxMethod: TestSvcWithTxMethod,
        testAppEventListener: TestAppEventListener,
        transactionManager: SynchronousTransactionManager<Any>
    ) : BehaviorSpec({
    
        given("context with app event listener") {
    
            `when`("calling someMethod with commit") {
                val tx = transactionManager.getTransaction(DefaultTransactionDefinition())
                testSvcWithTxMethod.someMethod(message = "call #1")
                transactionManager.commit(tx)
    
                then("TestAppEventListener should have received message") {
                    testAppEventListener.beforeCommitReceiveCount.get() shouldBe 1
                }
            }
    
            `when`("calling someMethod with rollback") {
                val tx = transactionManager.getTransaction(DefaultTransactionDefinition())
                testSvcWithTxMethod.someMethod(message = "call #2")
                transactionManager.rollback(tx)
    
                then("TestAppEventListener should have received message") {
                    testAppEventListener.afterRollbackReceiveCount.get() shouldBe 1
                }
            }
        }
    })
    

    TestSvcWithTxMethod

    import io.micronaut.context.event.ApplicationEventPublisher
    import jakarta.inject.Singleton
    import javax.transaction.Transactional
    
    @Singleton
    open class TestSvcWithTxMethod(
        private val eventPublisher: ApplicationEventPublisher<TestEvent>
    ) {
        @Transactional
        open fun someMethod(message: String) {
            eventPublisher.publishEvent(TestEvent(message))
        }
    }
    

    TestAppEventListener

    import io.micronaut.transaction.annotation.TransactionalEventListener
    import jakarta.inject.Singleton
    import java.util.concurrent.atomic.AtomicInteger
    
    @Singleton
    open class TestAppEventListener {
    
        val beforeCommitReceiveCount = AtomicInteger()
        val afterRollbackReceiveCount = AtomicInteger()
    
        @TransactionalEventListener(TransactionalEventListener.TransactionPhase.BEFORE_COMMIT)
        open fun beforeCommit(event: TestEvent) {
            beforeCommitReceiveCount.getAndIncrement()
        }
    
        @TransactionalEventListener(TransactionalEventListener.TransactionPhase.AFTER_ROLLBACK)
        open fun afterRollback(event: TestEvent) {
            afterRollbackReceiveCount.getAndIncrement()
        }
    }
    

    TestEvent (unchanged)

    import io.micronaut.core.annotation.Introspected
    
    @Introspected
    data class TestEvent(val message: String)