0

I'm struggling using Panache.withTransaction() in unit tests, whatever I do, I get a java.util.concurrent.TimeoutException.

Note: It works without transaction but I have to delete the inserts manually.

I want to chain insertKline and getOhlcList inside a transaction so I can benefit from the rollback:

@QuarkusTest
@Slf4j
class KlineServiceTest {
    @Inject
    KlineRepository klineRepository;

    @Inject
    CurrencyPairRepository currencyPairRepository;

    @Inject
    KlineService service;

    @Test
    @DisplayName("ohlc matches inserted kline")
    void ohlcMatchesInsertedKline() {
        // GIVEN
        val volume       = BigDecimal.valueOf(1d);
        val closeTime    = LocalDateTime.now().withSecond(0).withNano(0);
        val currencyPair = new CurrencyPair("BTC", "USDT");
        val currencyPairEntity = currencyPairRepository
                                     .findOrCreate(currencyPair)
                                     .await().indefinitely();

        val kline = KlineEntity.builder()
                               .id(new KlineId(currencyPairEntity, closeTime))
                               .volume(volume)
                               .build();

        val insertKline = Uni.createFrom().item(kline)
                             .call(klineRepository::persistAndFlush);

        val getOhlcList = service.listOhlcByCurrencyPairAndTimeWindow(currencyPair, ofMinutes(5));

        // WHEN
        val ohlcList = Panache.withTransaction(
                                  () -> Panache.currentTransaction()
                                               .invoke(Transaction::markForRollback)
                                               .replaceWith(insertKline)
                                               .chain(() -> getOhlcList))
                              .await().indefinitely();

        // THEN
        assertThat(ohlcList).hasSize(1);

        val ohlc = ohlcList.get(0);

        assertThat(ohlc).extracting(Ohlc::getCloseTime, Ohlc::getVolume)
                        .containsExactly(closeTime, volume);
    }
}

I get this exception:

java.lang.RuntimeException: java.util.concurrent.TimeoutException

    at io.quarkus.hibernate.reactive.panache.common.runtime.AbstractJpaOperations.executeInVertxEventLoop(AbstractJpaOperations.java:52)
    at io.smallrye.mutiny.operators.uni.UniRunSubscribeOn.subscribe(UniRunSubscribeOn.java:25)
    at io.smallrye.mutiny.operators.AbstractUni.subscribe(AbstractUni.java:36)

And looking at AbstractJpaOperations, I can see:

public abstract class AbstractJpaOperations<PanacheQueryType> {

    // FIXME: make it configurable?
    static final long TIMEOUT_MS = 5000;
    ...
}

Also, same issue when I tried to use runOnContext():

@Test
@DisplayName("ohlc matches inserted kline")
void ohlcMatchesInsertedKline() throws ExecutionException, InterruptedException {
    // GIVEN
    val volume       = BigDecimal.valueOf(1d);
    val closeTime    = LocalDateTime.now().withSecond(0).withNano(0);
    val currencyPair = new CurrencyPair("BTC", "USDT");

    val currencyPairEntity = currencyPairRepository
                                 .findOrCreate(currencyPair)
                                 .await().indefinitely();

    val kline = KlineEntity.builder()
                           .id(new KlineId(currencyPairEntity, closeTime))
                           .volume(volume)
                           .build();

    val insertKline = Uni.createFrom().item(kline)
                         .call(klineRepository::persist);

    val getOhlcList  = service.listOhlcByCurrencyPairAndTimeWindow(currencyPair, ofMinutes(5));
    val insertAndGet = insertKline.chain(() -> getOhlcList);

    // WHEN
    val ohlcList = runAndRollback(insertAndGet)
                       .runSubscriptionOn(action -> vertx.getOrCreateContext()
                                                         .runOnContext(action))
                       .await().indefinitely();

    // THEN
    assertThat(ohlcList).hasSize(1);

    val ohlc = ohlcList.get(0);

    assertThat(ohlc).extracting(Ohlc::getCloseTime, Ohlc::getVolume)
                    .containsExactly(closeTime, volume);
}

private static Uni<List<Ohlc>> runAndRollback(Uni<List<Ohlc>> getOhlcList) {
    return Panache.withTransaction(
        () -> Panache.currentTransaction()
                     .invoke(Transaction::markForRollback)
                     .replaceWith(getOhlcList));
}
  • Panache.withTransaction must be called from the event loop, and you are not on one. You would need something like `@Inject Vertx vert` and `vertx.runOnContext(...)` (which would run the passed block on the event loop. – Clement Jun 17 '22 at 07:52
  • Hi @Clement, `vertx.runOnContext(...)` runs a runnable, I need a callable on the event loop as my code is fully reactive. Do you have an example I could use? – Frédéric Thomas Jun 17 '22 at 12:12
  • Have you tried Vert.x unit? https://vertx.io/docs/vertx-junit5/java/#_running_tests_on_a_vert_x_context – Davide D'Alto Jun 17 '22 at 18:23
  • I tried but it did not work, but thanks the same because it allowed me to do [that](https://gist.github.com/doublefx/4da6c6da202553a9d50befd276b8c111) on the same principle, despite it's still failing, I learned something about this test lib, also, see my comment [here](https://stackoverflow.com/questions/72583353/how-to-properly-batch-insert-using-quarkus-reactive-hibernate-with-panache) – Frédéric Thomas Jun 19 '22 at 06:50

2 Answers2

3

Annotation @TestReactiveTransaction

Quarkus provides the annotation @TestReactiveTransaction: it will wrap the test method in a transaction and rollback the transaction at the end.

I'm going to use quarkus-test-vertx for testing the reactive code:

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

Here's an example of a test class that can be used with the Hibernate Reactive quickstart with Panache (after adding the quarkus-test-vertx dependency):

The entity:

@Entity
public class Fruit extends PanacheEntity {

    @Column(length = 40, unique = true)
    public String name;

    ...
}

The test class:

package org.acme.hibernate.orm.panache;

import java.util.List;

import org.junit.jupiter.api.Test;

import io.quarkus.test.TestReactiveTransaction;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.vertx.UniAsserter;
import io.smallrye.mutiny.Uni;
import org.assertj.core.api.Assertions;

@QuarkusTest
public class ExampleReactiveTest {

    @Test
    @TestReactiveTransaction
    public void test(UniAsserter asserter) {
        printThread( "Start" );
        Uni<List<Fruit>> listAllUni = Fruit.<Fruit>listAll();
        Fruit mandarino = new Fruit( "Mandarino" );
        asserter.assertThat(
                () -> Fruit
                        .persist( mandarino )
                        .replaceWith( listAllUni ),
                result -> {
                    Assertions.assertThat( result ).hasSize( 4 );
                    Assertions.assertThat( result ).contains( mandarino );
                    printThread( "End" );
                }
        );
    }

    private void printThread(String step) {
        System.out.println( step + " - " + Thread.currentThread().getId() + ":" + Thread.currentThread().getName() );
    }
}

@TestReactiveTransaction runs the method in a transaction that it's going to be rollbacked at the end of the test. UniAsserter makes it possible to test reactive code without having to block anything.

Annotation @RunOnVertxContext

It's also possible to run a test in the Vert.x event loop using the annotation @RunOnVertxContext in the quarkus-vertx-test library:

This way you don't need to wrap the whole test in a trasaction:

import io.quarkus.test.vertx.RunOnVertxContext;

@QuarkusTest
public class ExampleReactiveTest {

    @Test
    @RunOnVertxContext
    public void test(UniAsserter asserter) {
        printThread( "Start" );
        Uni<List<Fruit>> listAllUni = Fruit.<Fruit>listAll();
        Fruit mandarino = new Fruit( "Mandarino" );
        asserter.assertThat(
                () -> Panache.withTransaction( () -> Panache
                        // This test doesn't have @TestReactiveTransaction
                        // we need to rollback the transaction manually
                        .currentTransaction().invoke( Mutiny.Transaction::markForRollback )
                        .call( () -> Fruit.persist( mandarino ) )
                        .replaceWith( listAllUni )
                ),
                result -> {
                    Assertions.assertThat( result ).hasSize( 4 );
                    Assertions.assertThat( result ).contains( mandarino );
                    printThread( "End" );
                }
        );
    }
Davide D'Alto
  • 7,421
  • 2
  • 16
  • 30
1

I finally managed to get it working, the trick was to defer the Uni creation:

Like in:

@QuarkusTest
public class ExamplePanacheTest {

    @Test
    public void test() {
        final var mandarino = new Fruit("Mandarino");

        final var insertAndGet = Uni.createFrom()
                                    .deferred(() -> Fruit.persist(mandarino)
                                                         .replaceWith(Fruit.<Fruit>listAll()));

        final var fruits = runAndRollback(insertAndGet)
                                 .await().indefinitely();

        assertThat(fruits).hasSize(4)
                          .contains(mandarino);
    }

    private static Uni<List<Fruit>> runAndRollback(Uni<List<Fruit>> insertAndGet) {
        return Panache.withTransaction(
            () -> Panache.currentTransaction()
                         .invoke(Transaction::markForRollback)
                         .replaceWith(insertAndGet));
    }
}