Home > database >  How to chain 2 Uni<?> in unit test using Panache.withTransaction() without getting a java.util
How to chain 2 Uni<?> in unit test using Panache.withTransaction() without getting a java.util

Time:06-20

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));
}

CodePudding user response:

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));
    }
}

CodePudding user response:

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.

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" );
                }
        );
    }
  • Related