Quarkus での reactive hibernate のテストで rollback する方法

Quarkus で Reactive Hibernate のテストをする際にトランザクションを開始してロールバックする方法があるか検証してみました。

単純にロールバックする処理を書くのでもいいのですが、フレームワーク (Quarkus) 側で用意されている仕組みを探してみました。フレームワークの仕組みに沿って書くとコードが短くなるのと、テスト中に例外が発生した場合もうまく処理してくれるだろうという考えです。

テストコード(コミットされる)

今回テストするコードは以下です。DB へデータを登録して、登録したデータを取得できるかをテストします。

@QuarkusTest
class ReactiveEntityTest {

    @Inject
    Session session;

    @Test
    void testInsert() {
        var apple = new Fruit();
        apple.name = "りんご";

        var subscriber = apple.<Fruit>persist()

            // DB へ反映
            .call(e -> session.flush())

            // 1 次キャッシュクリア
            .invoke(e -> session.clear())

            // 登録した情報を取得 (キャッシュを消したので DB から取得)
            .chain(e -> Fruit.<Fruit>findById(e.id))
            
            // 購読者を指定して処理を実施
            .subscribe().withSubscriber(UniAssertSubscriber.create());
        
        var actual = subscriber
            // ブロックして結果を待つ
            .awaitItem()
            // 完了すること(例外が発生しないこと)
            .assertCompleted()
            // 結果を取得
            .getItem();

        // 結果の検証
        assertEquals("りんご", actual.name);

        // DB から再取得して新しいオブジェクト ID になっていることを確認
        assertNotSame(System.identityHashCode(apple), System.identityHashCode(actual));
    }
}

これはロールバックしていないので、テスト実行後にデータが残ります。

@TestTransaction は使えない

Quarkus のガイドには @TestTransaction アノテーションを使うよう書かれていますが、これは imperative (reactive じゃない方) の書き方なので、ロールバックされずコミットされます。

@QuarkusTest
class ReactiveEntityTest {

    @Test
    @TestTransaction
    void testInsert() {
        // テスト内容は上記と同じです
        // リアクティブの場合はコミットされます
    }
}

@TestReactiveTransaction 失敗例

Quarkus のソースコードを眺めていたら、 @TestReactiveTransaction というアノテーションが使われているのを発見しました。ただこれを付ければいいというわけでは無いようで、アノテーションを付けるだけでは動作しません。

@QuarkusTest
class ReactiveEntityTest {

    @Test
    @TestReactiveTransaction
    void testInsert() {
        // テスト内容は上記と同じです
        // 例外が発生します
    }
}

以下のエラーが数回出た後に、

2021-11-03 09:14:22,233 WARN [io.ver.cor.imp.BlockedThreadChecker] (vertx-blocked-thread-checker) Thread Thread[vert.x-eventloop-thread-0,5,main] has been blocked for 8573 ms, time limit is 2000 ms: io.vertx.core.VertxException: Thread blocked
	at java.base@11.0.11/jdk.internal.misc.Unsafe.park(Native Method)
	at java.base@11.0.11/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:234)
	at java.base@11.0.11/java.util.concurrent.CompletableFuture$Signaller.block(CompletableFuture.java:1798)
	at java.base@11.0.11/java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3128)
	at java.base@11.0.11/java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1868)
	at java.base@11.0.11/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2021)
	at io.smallrye.mutiny.helpers.test.UniAssertSubscriber.awaitEvent(UniAssertSubscriber.java:255)
	at io.smallrye.mutiny.helpers.test.UniAssertSubscriber.awaitItem(UniAssertSubscriber.java:128)
	at io.smallrye.mutiny.helpers.test.UniAssertSubscriber.awaitItem(UniAssertSubscriber.java:115)
	at com.zu_min.playground.quarkus.ReactiveEntityTest.testInsert(ReactiveEntityTest.java:126)
	at com.zu_min.playground.quarkus.ReactiveEntityTest_Subclass.testInsert$$superforward1(ReactiveEntityTest_Subclass.zig:89)
	at com.zu_min.playground.quarkus.ReactiveEntityTest_Subclass$$function$$1.apply(ReactiveEntityTest_Subclass$$function$$1.zig:24)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:54)
	at io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactionalInterceptorBase.lambda$intercept$2(ReactiveTransactionalInterceptorBase.java:49)
	at io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactionalInterceptorBase$$Lambda$1411/0x0000000800ad2840.apply(Unknown Source)
	at org.hibernate.reactive.mutiny.impl.MutinySessionImpl$Transaction.lambda$execute$0(MutinySessionImpl.java:434)
	at org.hibernate.reactive.mutiny.impl.MutinySessionImpl$Transaction$$Lambda$1434/0x0000000800b1f440.get(Unknown Source)
	at io.smallrye.mutiny.Uni.lambda$chain$0(Uni.java:494)
	at io.smallrye.mutiny.Uni$$Lambda$1435/0x0000000800b1f840.apply(Unknown Source)
	at io.smallrye.context.impl.wrappers.SlowContextualFunction.apply(SlowContextualFunction.java:21)
	at io.smallrye.mutiny.operators.uni.UniOnItemTransformToUni$UniOnItemTransformToUniProcessor.performInnerSubscription(UniOnItemTransformToUni.java:68)
	at io.smallrye.mutiny.operators.uni.UniOnItemTransformToUni$UniOnItemTransformToUniProcessor.onItem(UniOnItemTransformToUni.java:57)
	at io.smallrye.mutiny.operators.uni.builders.UniCreateFromCompletionStage$CompletionStageUniSubscription.forwardResult(UniCreateFromCompletionStage.java:63)
	at io.smallrye.mutiny.operators.uni.builders.UniCreateFromCompletionStage$CompletionStageUniSubscription$$Lambda$1449/0x0000000800b26840.accept(Unknown Source)
	at java.base@11.0.11/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:859)
	at java.base@11.0.11/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:837)
	at java.base@11.0.11/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
	at java.base@11.0.11/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2073)
	at io.vertx.core.Future.lambda$toCompletionStage$2(Future.java:360)
	at io.vertx.core.Future$$Lambda$1430/0x0000000800b1e440.handle(Unknown Source)
	at io.vertx.core.impl.future.FutureImpl$3.onSuccess(FutureImpl.java:141)
	at io.vertx.core.impl.future.FutureBase.emitSuccess(FutureBase.java:60)
	at io.vertx.core.impl.future.FutureImpl.tryComplete(FutureImpl.java:211)
	at io.vertx.core.impl.future.PromiseImpl.tryComplete(PromiseImpl.java:23)
	at io.vertx.core.impl.future.PromiseImpl.onSuccess(PromiseImpl.java:49)
	at io.vertx.core.impl.future.PromiseImpl.handle(PromiseImpl.java:41)
	at io.vertx.sqlclient.impl.TransactionImpl.lambda$wrap$0(TransactionImpl.java:72)
	at io.vertx.sqlclient.impl.TransactionImpl$$Lambda$1470/0x0000000800b3f040.handle(Unknown Source)
	at io.vertx.core.impl.future.FutureImpl$3.onSuccess(FutureImpl.java:141)
	at io.vertx.core.impl.future.FutureBase.emitSuccess(FutureBase.java:60)
	at io.vertx.core.impl.future.FutureImpl.tryComplete(FutureImpl.java:211)
	at io.vertx.core.impl.future.PromiseImpl.tryComplete(PromiseImpl.java:23)
	at io.vertx.core.impl.future.PromiseImpl.onSuccess(PromiseImpl.java:49)
	at io.vertx.core.impl.future.PromiseImpl.handle(PromiseImpl.java:41)
	at io.vertx.core.impl.future.PromiseImpl.handle(PromiseImpl.java:23)
	at io.vertx.pgclient.impl.PgSocketConnection.lambda$doSchedule$2(PgSocketConnection.java:160)
	at io.vertx.pgclient.impl.PgSocketConnection$$Lambda$1474/0x0000000800b3e040.handle(Unknown Source)
	at io.vertx.sqlclient.impl.command.CommandResponse.fire(CommandResponse.java:46)
	at io.vertx.sqlclient.impl.SocketConnectionBase.handleMessage(SocketConnectionBase.java:287)
	at io.vertx.pgclient.impl.PgSocketConnection.handleMessage(PgSocketConnection.java:96)
	at io.vertx.sqlclient.impl.SocketConnectionBase.lambda$init$0(SocketConnectionBase.java:99)
	at io.vertx.sqlclient.impl.SocketConnectionBase$$Lambda$1462/0x0000000800b22c40.handle(Unknown Source)
	at io.vertx.core.net.impl.NetSocketImpl.lambda$new$1(NetSocketImpl.java:97)
	at io.vertx.core.net.impl.NetSocketImpl$$Lambda$1456/0x0000000800b24440.handle(Unknown Source)
	at io.vertx.core.streams.impl.InboundBuffer.handleEvent(InboundBuffer.java:240)
	at io.vertx.core.streams.impl.InboundBuffer.write(InboundBuffer.java:130)
	at io.vertx.core.net.impl.NetSocketImpl.lambda$handleMessage$9(NetSocketImpl.java:390)
	at io.vertx.core.net.impl.NetSocketImpl$$Lambda$1467/0x0000000800b38c40.handle(Unknown Source)
	at io.vertx.core.impl.EventLoopContext.emit(EventLoopContext.java:50)
	at io.vertx.core.impl.ContextImpl.emit(ContextImpl.java:274)
	at io.vertx.core.impl.EventLoopContext.emit(EventLoopContext.java:22)
	at io.vertx.core.net.impl.NetSocketImpl.handleMessage(NetSocketImpl.java:389)
	at io.vertx.core.net.impl.ConnectionBase.read(ConnectionBase.java:155)
	at io.vertx.core.net.impl.VertxHandler.channelRead(VertxHandler.java:154)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
	at io.vertx.pgclient.impl.codec.PgEncoder.lambda$write$0(PgEncoder.java:87)
	at io.vertx.pgclient.impl.codec.PgEncoder$$Lambda$1465/0x0000000800b38440.handle(Unknown Source)
	at io.vertx.pgclient.impl.codec.PgCommandCodec.handleReadyForQuery(PgCommandCodec.java:139)
	at io.vertx.pgclient.impl.codec.PgDecoder.decodeReadyForQuery(PgDecoder.java:237)
	at io.vertx.pgclient.impl.codec.PgDecoder.channelRead(PgDecoder.java:96)
	at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base@11.0.11/java.lang.Thread.run(Thread.java:829)

最終的に以下の例外で失敗します。

io.smallrye.mutiny.TimeoutException
    at io.smallrye.mutiny.operators.uni.UniBlockingAwait.await(UniBlockingAwait.java:58)
    at io.smallrye.mutiny.groups.UniAwait.atMost(UniAwait.java:61)
    at io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactionalInterceptorBase.intercept(ReactiveTransactionalInterceptorBase.java:56)
    at io.quarkus.hibernate.reactive.panache.common.runtime.TestReactiveTransactionalInterceptorGenerated_Bean.intercept(TestReactiveTransactionalInterceptorGenerated_Bean.zig:275)
    at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:41)
    at io.quarkus.arc.impl.AroundInvokeInvocationContext.perform(AroundInvokeInvocationContext.java:41)
    at io.quarkus.arc.impl.InvocationContexts.performAroundInvoke(InvocationContexts.java:32)
    at com.zu_min.playground.quarkus.ReactiveEntityTest_Subclass.testInsert(ReactiveEntityTest_Subclass.zig:188)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at io.quarkus.test.junit.QuarkusTestExtension.runExtensionMethod(QuarkusTestExtension.java:922)
    at io.quarkus.test.junit.QuarkusTestExtension.interceptTestMethod(QuarkusTestExtension.java:752)
    at org.junit.jupiter.engine.execution.ExecutableInvokerReflectiveInterceptorCall.lambdaReflectiveInterceptorCall.lambdaofVoidMethod$0(ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
    at org.junit.jupiter.engine.execution.ExecutableInvokerReflectiveInterceptorCall.lambdaReflectiveInterceptorCall.lambdaofVoidMethod$0(ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:95)
    at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:91)
    at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:60)
    at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:98)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:40)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:529)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:756)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:452)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)

@TestReactiveTransaction を使うには

まず quarkus-junit5-vertx を maven の依存関係に追加する必要があります。pom.xml の dependencies に以下を追加します。

    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-junit5-vertx</artifactId>
      <scope>test</scope>
    </dependency>

この状態で先ほどの @TestReactiveTransaction を付けたテストを実行するとエラー内容が変わり、今度は以下の例外が発生します。

java.lang.RuntimeException: Unsupported return type void in method void com.zu_min.playground.quarkus.ReactiveEntityTest.testInsert(): only Uni is supported when using @ReactiveTransaction if you are running on a VertxThread
    at io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactionalInterceptorBase.intercept(ReactiveTransactionalInterceptorBase.java:41)
    at io.quarkus.hibernate.reactive.panache.common.runtime.TestReactiveTransactionalInterceptorGenerated_Bean.intercept(TestReactiveTransactionalInterceptorGenerated_Bean.zig:270)
    at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:41)
    at io.quarkus.arc.impl.AroundInvokeInvocationContext.perform(AroundInvokeInvocationContext.java:41)
    at io.quarkus.arc.impl.InvocationContexts.performAroundInvoke(InvocationContexts.java:32)
    at com.zu_min.playground.quarkus.ReactiveEntityTest_Subclass.testFailureCaseOfInsertWithTestReactiveTransaction(ReactiveEntityTest_Subclass.zig:226)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at io.quarkus.test.junit.vertx.RunOnVertxContextTestMethodInvoker$RunTestMethodOnContextHandler.doRun(RunOnVertxContextTestMethodInvoker.java:129)
    at io.quarkus.test.junit.vertx.RunOnVertxContextTestMethodInvoker$RunTestMethodOnContextHandler.handle(RunOnVertxContextTestMethodInvoker.java:118)
    at io.quarkus.test.junit.vertx.RunOnVertxContextTestMethodInvoker$RunTestMethodOnContextHandler.handle(RunOnVertxContextTestMethodInvoker.java:89)
    at io.vertx.core.impl.AbstractContext.dispatch(AbstractContext.java:100)
    at io.vertx.core.impl.AbstractContext.dispatch(AbstractContext.java:63)
    at io.vertx.core.impl.EventLoopContext.lambda$runOnContext$0(EventLoopContext.java:38)
    at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
    at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:469)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:500)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:829)

void の返却はサポートしていないと言われているので、テストコードを書き替える必要があります。ただ、単純に返却を void から Uni にするだけではテストが実行されなくなる1JUnit 的には void を返すのが基本だからでしょうか ので、 UniAsserter を使って書き換える必要があるようです。

UniAsserter を使う

上記 quarkus-junit5-vertx を追加した後に、 Uni を返すようテストコードを書き替えます。

@QuarkusTest
class ReactiveEntityTest {

    @Inject
    Session session;

    @Test
    @TestReactiveTransaction
    void testInsert(UniAsserter asserter) {
        var apple = new Fruit();
        apple.name = "りんご";
        
        asserter.assertThat(() -> {

            return apple.<Fruit>persist()

                // DB へ反映
                .call(e -> session.flush())

                // 1 次キャッシュクリア
                .invoke(e -> session.clear())

                // 登録した情報を取得 (キャッシュを消したので DB から取得)
                .chain(e -> Fruit.<Fruit>findById(e.id));
        }, actual -> {

            // 結果の検証
            assertEquals("りんご", actual.name);

            // DB から再取得して新しいオブジェクト ID になっていることを確認
            assertNotSame(System.identityHashCode(apple), System.identityHashCode(actual));
        });
    }
}

これでロールバックされるようになりました。

UniAsserter にはいろいろメソッドが用意されているので、それぞれ調べて理解すればより便利に使えそうです。

アノテーションを書く位置

今回は同じクラスに色々なパターンを書きたかったのでメソッドに @TestTransaction@TestReactiveTransaction アノテーションを付けましたが、クラスにつけることも可能です。

確認した環境について

本記事の内容は Quarkus のドキュメントに書かれているわけでは無いので、今後変更される可能性があります。バージョンは 2.4.0.Final で確認しています。

コメントする

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください