Spring-framework: Introduce reactive @Transactional support in the TestContext framework

Created on 18 Dec 2019  Â·  5Comments  Â·  Source: spring-projects/spring-framework

Since we have @Transactional working with R2DBC repositories in _1.0 M2_ (as said here), I would like to ask if there is a way to make @Transactional working with JUnit (integration) tests (the same way we are able to do when using JDBC repositories). Is this currently possible? Will this even be possible? What is the right approach to achieve transactional tests ?

Currently, running a @Transactional @SpringBootTest gives me java.lang.IllegalStateException: Failed to retrieve PlatformTransactionManager for @Transactional test (the same problem as this guy has: http://disq.us/p/2425ot1).

data test waiting-for-triage

Most helpful comment

Hey @mp911de! I had some trouble with this lately, and since the @Transactional annotation is not supported yet I came out with an small helper that transform any publisher to a rollback operation:

@Component
public class Transaction {

  private static TransactionalOperator rxtx;

  @Autowired
  public Transaction(final TransactionalOperator rxtx) {
    Transaction.rxtx = rxtx;
  }

  public static <T> Mono<T> withRollback(final Mono<T> publisher) {
    return rxtx.execute(tx -> {
      tx.setRollbackOnly();
      return publisher;
    })
    .next();
  }

  public static <T> Flux<T> withRollback(final Flux<T> publisher) {
    return rxtx.execute(tx -> {
      tx.setRollbackOnly();
      return publisher;
    });
  }
}

Then I can use it on tests like this:

@Test
void finds_the_account_and_return_it_as_user_details() {
  accountRepo.save(newAccount)
    .map(Account::getUsername)
    .flatMap(userDetailsService::findByUsername)
    .as(Transaction::withRollback) // <-- This makes the test rollback after the transaction
    .as(StepVerifier::create)
    .assertNext(user -> {
      assertThat(user.getUsername()).isEqualTo("[email protected]");
    })
    .verifyComplete();
}

I thought this helper can be used by the @Transactional annotation in some way. Maybe the annotation can find all publishers within the @Test and add the transformer right before the StepVerifier (if present?). I'm not sure how possible/easy that might be though 😅.
Another approach I was thinking is to add this right into the StepVerifier (again, not sure if possible since it requires Spring to work) or to another implementation of the StepVerifier (something like TxStepVerifier perhaps?). Of course, this approaches will not use the @Transactional annotation, but will add more control over which publisher should rollback and which should not 🙂

Hope this helps, at least as a brainstorm on some ways to solve the issue.

Cheers!!

All 5 comments

It is currently not possible, since the TransactionalTestExecutionListener in the Spring TestContext Framework only supports Spring's PlatformTransactionManager with transaction state bound to a ThreadLocal.

However, I have reworded the title of this issue and will leave it open for further discussion and brainstorming.

@mp911de, feel free to share any ideas you have regarding this topic.

The only supported approach is moving the Publisher into a @Transactional service method.

We're lacking a propagation model that would allow for reactive @Transactional test methods. The reason why this is lies in how reactive vs. imperative code is executed and tested.

Imperative interaction with data and the actual assertions are enclosed within a method. A @Transactional test method starts a transaction and the actual code runs within that scope. Synchronous transaction managers use ThreadLocal to propagate the transaction.

Reactive transactions require a Context to propagate the transactional state. While a reactive transaction _could_ be started with a @Transactional test method, there's no way how the transactional state could be propagated into the Publisher. A test method returns void.

Another issue are assertions: Assertions are typically modeled using StepVerifier, which is blocking in the end. Therefore, assertions happen outside of the flow, by subscribing to the Publisher as we cannot introduce blocking code to a reactive flow.

We need to solve both issues to make reactive @Transactional test methods work.

Basically, from a usage perspective we have two options:

  1. Adding elements (such as a method parameter) to the programming model that allow a transactional association
  2. Leaving the programming model as-is

In the first variant, code could look like:

@Test @Transactional 
void doTest(StepVerifier<Foo> verifier) {
  flux.subscribeWith(verifier).expectNext(…).verify();
}

or

@Test @Transactional 
void doTest(MyService myService) {
  myService.doWork(…).as(StepVerifier::create).expectNext(…).verify();
}

class MyService() {
  @Transactional
  Flux<…> doWork() {…};
}

Injecting parameters is intrusive and defeats simplicity. Probably, we don't want that kind of experience. It would be also too simple to forget the injection, and tests would fail (or pass) because of a different arrangement.

The next example leaves the programming model as-is which looks much more easier to use:

@Test @Transactional 
void doTest() {
  flux.as(StepVerifier::create).expectNext(…).verify();
}

Now, how can we associate the transaction with our flux? We could use a TestExecutionListener to use Reactor's onAssembly(…) hooks to make sure, when using Project Reactor, that operators become transaction-aware. It's a bit like Reactor's virtual time that allows the injection of behavioral aspects into a reactive flow.

@Test @Transactional 
void doTest() {
  flux.as(StepVerifier::create).expectNext(…).verify();

  anotherFlux.as(StepVerifier::create).expectNext(…).verify();
}

The same utility will work if we use more than one component to test as all reactive flows participate in the transaction. We have only one constraint, which is parallel test execution. Reactor's hooks are JVM-wide (scoped to the ClassLoader). If we would run multiple tests in parallel, then we would not be able to distinguish between transactions anymore. Using hooks for a transactional purpose compares well to a mutable global variable.

Another aspect that plays into the model is that typically, we expect propagation using Reactor's Context which is carried within a Subscriber. We don't return anything from our method and that is a bit weird. The behind-the-scenes propagation feels a bit like magic.

/cc @smaldini @bsideup

any updates on this issue? I see Spring Boot 2.3.0+ includes some updates to R2DBC Support but I am seeing this issue still exists.

Hey @mp911de! I had some trouble with this lately, and since the @Transactional annotation is not supported yet I came out with an small helper that transform any publisher to a rollback operation:

@Component
public class Transaction {

  private static TransactionalOperator rxtx;

  @Autowired
  public Transaction(final TransactionalOperator rxtx) {
    Transaction.rxtx = rxtx;
  }

  public static <T> Mono<T> withRollback(final Mono<T> publisher) {
    return rxtx.execute(tx -> {
      tx.setRollbackOnly();
      return publisher;
    })
    .next();
  }

  public static <T> Flux<T> withRollback(final Flux<T> publisher) {
    return rxtx.execute(tx -> {
      tx.setRollbackOnly();
      return publisher;
    });
  }
}

Then I can use it on tests like this:

@Test
void finds_the_account_and_return_it_as_user_details() {
  accountRepo.save(newAccount)
    .map(Account::getUsername)
    .flatMap(userDetailsService::findByUsername)
    .as(Transaction::withRollback) // <-- This makes the test rollback after the transaction
    .as(StepVerifier::create)
    .assertNext(user -> {
      assertThat(user.getUsername()).isEqualTo("[email protected]");
    })
    .verifyComplete();
}

I thought this helper can be used by the @Transactional annotation in some way. Maybe the annotation can find all publishers within the @Test and add the transformer right before the StepVerifier (if present?). I'm not sure how possible/easy that might be though 😅.
Another approach I was thinking is to add this right into the StepVerifier (again, not sure if possible since it requires Spring to work) or to another implementation of the StepVerifier (something like TxStepVerifier perhaps?). Of course, this approaches will not use the @Transactional annotation, but will add more control over which publisher should rollback and which should not 🙂

Hope this helps, at least as a brainstorm on some ways to solve the issue.

Cheers!!

For the record, this prevents Spring Boot to support any slice test that is transactional. @DataNeo4jTest, for instance, can't work with reactive repositories as we're facing the same problem.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sdeleuze picture sdeleuze  Â·  3Comments

manueljordan picture manueljordan  Â·  3Comments

spring-projects-issues picture spring-projects-issues  Â·  4Comments

alvaro-nogueira picture alvaro-nogueira  Â·  3Comments

spring-projects-issues picture spring-projects-issues  Â·  5Comments