Skip to content

Commit

Permalink
Add the @TestTransaction annotation
Browse files Browse the repository at this point in the history
This allows you to run tests in a rollback only transaction.

Fixes quarkusio#6463
  • Loading branch information
stuartwdouglas committed Sep 2, 2020
1 parent a4f910f commit f7b47c8
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 0 deletions.
7 changes: 7 additions & 0 deletions docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,13 @@ public class TestStereotypeTestCase {
}
----

== Tests and Transactions

You can use the standard Quarkus `@Transactional` annotation on tests, but this means that the changes your
test makes to the database will be persistent. If you want any changes made to be rolled back at the end of
the test you can use the `io.quarkus.test.TestTransaction` annotation. This will run the test method in a
transaction, but roll it back once the test method is complete to revert any database changes.

== Enrichment via QuarkusTest*Callback

Alternatively or additionally to an interceptor, you can enrich *all* your `@QuarkusTest` classes by implementing the following callback interfaces:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@

import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;

import java.lang.reflect.Modifier;
import java.util.Properties;

import javax.annotation.Priority;
import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import javax.transaction.TransactionScoped;
import javax.transaction.UserTransaction;

import com.arjuna.ats.arjuna.common.ObjectStoreEnvironmentBean;
import com.arjuna.ats.internal.arjuna.coordinator.CheckedActionFactoryImple;
Expand All @@ -17,6 +24,8 @@

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.ContextRegistrarBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.processor.ContextRegistrar;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.Feature;
Expand All @@ -25,20 +34,29 @@
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.CapabilityBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.FieldCreator;
import io.quarkus.gizmo.FieldDescriptor;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.narayana.jta.runtime.CDIDelegatingTransactionManager;
import io.quarkus.narayana.jta.runtime.NarayanaJtaProducers;
import io.quarkus.narayana.jta.runtime.NarayanaJtaRecorder;
import io.quarkus.narayana.jta.runtime.TransactionManagerConfiguration;
import io.quarkus.narayana.jta.runtime.context.TransactionContext;
import io.quarkus.narayana.jta.runtime.interceptor.TestTransactionInterceptor;
import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorMandatory;
import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorNever;
import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorNotSupported;
import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired;
import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequiresNew;
import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorSupports;
import io.quarkus.runtime.LaunchMode;

class NarayanaJtaProcessor {

Expand Down Expand Up @@ -96,6 +114,40 @@ public void build(NarayanaJtaRecorder recorder,
recorder.setDefaultTimeout(transactions);
}

@BuildStep
void testTx(LaunchModeBuildItem lm, BuildProducer<GeneratedBeanBuildItem> generatedBeanBuildItemBuildProducer) {
if (lm.getLaunchMode() != LaunchMode.TEST) {
return;
}
//generate the annotated interceptor with gizmo
//all the logic is in the parent, but we don't have access to the
//binding annotation here
try (ClassCreator c = new ClassCreator(new GeneratedBeanGizmoAdaptor(generatedBeanBuildItemBuildProducer),
TestTransactionInterceptor.class.getName() + "Generated", null, TestTransactionInterceptor.class.getName())) {
c.addAnnotation("io.quarkus.test.TestTransaction");
c.addAnnotation(Interceptor.class.getName());
c.addAnnotation(Priority.class).addValue("value", Interceptor.Priority.PLATFORM_BEFORE + 200);

FieldCreator field = c.getFieldCreator("ut", UserTransaction.class);
field.setModifiers(Modifier.PUBLIC);
field
.addAnnotation(Inject.class);

MethodCreator m = c.getMethodCreator("work", Object.class, InvocationContext.class);
m.addAnnotation(AroundInvoke.class);
m.addException(Exception.class);

ResultHandle ut = m.readInstanceField(FieldDescriptor.of(c.getClassName(), "ut", UserTransaction.class),
m.getThis());
ResultHandle result = m
.invokeStaticMethod(MethodDescriptor.ofMethod(TestTransactionInterceptor.class, "intercept", Object.class,
UserTransaction.class, InvocationContext.class), ut, m.getMethodParam(0));

m.returnValue(result);
}

}

@BuildStep
public void transactionContext(
BuildProducer<ContextRegistrarBuildItem> contextRegistry) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.narayana.jta.runtime.interceptor;

import javax.interceptor.InvocationContext;
import javax.transaction.UserTransaction;

public class TestTransactionInterceptor {

public static Object intercept(UserTransaction userTransaction, InvocationContext context) throws Exception {
try {
userTransaction.begin();
return context.proceed();
} finally {
userTransaction.rollback();
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.quarkus.it.panache;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;

/**
* Tests that @TestTransaction works as expected
*/
@QuarkusTest
@TestTransaction
public class TestTransactionTest {

@Test
@TestTransaction
public void test1() {
Assertions.assertEquals(0, Beer.find("name", "Lager").count());
Beer b = new Beer();
b.name = "Lager";
Beer.persist(b);
}

@Test
@TestTransaction
public void test2() {
Assertions.assertEquals(0, Beer.find("name", "Lager").count());
Beer b = new Beer();
b.name = "Lager";
Beer.persist(b);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.test;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.interceptor.InterceptorBinding;

/**
* Indicates that this method should be run in a rollback only transaction.
*
* The allows the test method to modify the database as required, and then have
* these changes reverted at the end of the method.
*
*/
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface TestTransaction {
}

0 comments on commit f7b47c8

Please sign in to comment.