Skip to content

Commit

Permalink
Merge pull request #11798 from stuartwdouglas/testtransaction
Browse files Browse the repository at this point in the history
Add the @TestTransaction annotation
  • Loading branch information
stuartwdouglas authored Sep 7, 2020
2 parents 7e26e8d + a2d057f commit 2f645d4
Show file tree
Hide file tree
Showing 15 changed files with 334 additions and 29 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
@@ -0,0 +1,21 @@
package io.quarkus.hibernate.orm.deployment.test;

import javax.enterprise.inject.spi.CDI;
import javax.persistence.EntityManager;

import io.quarkus.narayana.jta.runtime.test.TestTransactionCallback;

public class HibernateTestTransactionCallback implements TestTransactionCallback {
@Override
public void postBegin() {

}

@Override
public void preRollback() {
for (EntityManager i : CDI.current().select(EntityManager.class)) {
i.flush();
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.quarkus.hibernate.orm.deployment.test.HibernateTestTransactionCallback
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import java.util.Properties;

import javax.annotation.Priority;
import javax.interceptor.Interceptor;
import javax.transaction.TransactionScoped;

import com.arjuna.ats.arjuna.common.ObjectStoreEnvironmentBean;
Expand All @@ -17,10 +19,13 @@

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.deployment.UnremovableBeanBuildItem;
import io.quarkus.arc.processor.ContextRegistrar;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.IsTest;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Record;
Expand All @@ -29,11 +34,13 @@
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.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;
Expand All @@ -44,6 +51,8 @@

class NarayanaJtaProcessor {

private static final String TEST_TRANSACTION = "io.quarkus.test.TestTransaction";

@BuildStep
public NativeImageSystemPropertyBuildItem nativeImageSystemPropertyBuildItem() {
return new NativeImageSystemPropertyBuildItem("CoordinatorEnvironmentBean.transactionStatusManagerEnable", "false");
Expand Down Expand Up @@ -98,6 +107,24 @@ public void build(NarayanaJtaRecorder recorder,
recorder.setDefaultTimeout(transactions);
}

@BuildStep(onlyIf = IsTest.class)
void testTx(BuildProducer<GeneratedBeanBuildItem> generatedBeanBuildItemBuildProducer,
BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
//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 = ClassCreator.builder()
.classOutput(new GeneratedBeanGizmoAdaptor(generatedBeanBuildItemBuildProducer)).className(
TestTransactionInterceptor.class.getName() + "Generated")
.superClass(TestTransactionInterceptor.class).build()) {
c.addAnnotation(TEST_TRANSACTION);
c.addAnnotation(Interceptor.class.getName());
c.addAnnotation(Priority.class).addValue("value", Interceptor.Priority.PLATFORM_BEFORE + 200);
}
additionalBeans.produce(AdditionalBeanBuildItem.builder().addBeanClass(TestTransactionInterceptor.class)
.addBeanClass(TEST_TRANSACTION).build());
}

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

import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;

import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
import javax.transaction.UserTransaction;

import io.quarkus.narayana.jta.runtime.test.TestTransactionCallback;

public class TestTransactionInterceptor {

static final List<TestTransactionCallback> CALLBACKS;

static {
List<TestTransactionCallback> callbacks = new ArrayList<>();
for (TestTransactionCallback i : ServiceLoader.load(TestTransactionCallback.class)) {
callbacks.add(i);
}
CALLBACKS = callbacks;
}

@Inject
UserTransaction userTransaction;

@AroundInvoke
public Object intercept(InvocationContext context) throws Exception {
try {
userTransaction.begin();
for (TestTransactionCallback i : CALLBACKS) {
i.postBegin();
}
Object result = context.proceed();
for (TestTransactionCallback i : CALLBACKS) {
i.preRollback();
}
return result;
} finally {
userTransaction.rollback();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.quarkus.narayana.jta.runtime.test;

public interface TestTransactionCallback {

void postBegin();

void preRollback();

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package io.quarkus.arc.processor;

import static io.quarkus.arc.processor.IndexClassLookupUtils.getClassByName;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.enterprise.inject.spi.InterceptionType;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
Expand Down Expand Up @@ -47,39 +52,56 @@ public class InterceptorInfo extends BeanInfo implements Comparable<InterceptorI
null, null, null, Collections.emptyList(), null, false);
this.bindings = bindings;
this.priority = priority;
MethodInfo aroundInvoke = null;
MethodInfo aroundConstruct = null;
MethodInfo postConstruct = null;
MethodInfo preDestroy = null;
for (MethodInfo method : target.asClass().methods()) {
if (aroundInvoke == null && method.hasAnnotation(DotNames.AROUND_INVOKE)) {
aroundInvoke = method;
} else if (method.hasAnnotation(DotNames.AROUND_CONSTRUCT)) {
// validate compliance with rules for AroundConstruct methods
if (!method.parameters().equals(Collections.singletonList(
Type.create(DotName.createSimple("javax.interceptor.InvocationContext"), Type.Kind.CLASS)))) {
throw new IllegalStateException(
"@AroundConstruct must have exactly one argument of type javax.interceptor.InvocationContext, but method "
+ method.asMethod() + " declared by " + method.declaringClass()
+ " violates this.");
List<MethodInfo> aroundInvokes = new ArrayList<>();
List<MethodInfo> aroundConstructs = new ArrayList<>();
List<MethodInfo> postConstructs = new ArrayList<>();
List<MethodInfo> preDestroys = new ArrayList<>();

ClassInfo aClass = target.asClass();
Set<DotName> scanned = new HashSet<>();
while (aClass != null) {
if (!scanned.add(aClass.name())) {
continue;
}
for (MethodInfo method : aClass.methods()) {
if (Modifier.isStatic(method.flags())) {
continue;
}
if (!method.returnType().kind().equals(Type.Kind.VOID) &&
!method.returnType().name().equals(DotNames.OBJECT)) {
throw new IllegalStateException("Return type of @AroundConstruct method must be Object or void, but method "
+ method.asMethod() + " declared by " + method.declaringClass()
+ " violates this.");
if (method.hasAnnotation(DotNames.AROUND_INVOKE)) {
aroundInvokes.add(method);
} else if (method.hasAnnotation(DotNames.AROUND_CONSTRUCT)) {
// validate compliance with rules for AroundConstruct methods
if (!method.parameters().equals(Collections.singletonList(
Type.create(DotName.createSimple("javax.interceptor.InvocationContext"), Type.Kind.CLASS)))) {
throw new IllegalStateException(
"@AroundConstruct must have exactly one argument of type javax.interceptor.InvocationContext, but method "
+ method.asMethod() + " declared by " + method.declaringClass()
+ " violates this.");
}
if (!method.returnType().kind().equals(Type.Kind.VOID) &&
!method.returnType().name().equals(DotNames.OBJECT)) {
throw new IllegalStateException(
"Return type of @AroundConstruct method must be Object or void, but method "
+ method.asMethod() + " declared by " + method.declaringClass()
+ " violates this.");
}
aroundConstructs.add(method);
} else if (method.hasAnnotation(DotNames.POST_CONSTRUCT)) {
postConstructs.add(method);
} else if (method.hasAnnotation(DotNames.PRE_DESTROY)) {
preDestroys.add(method);
}
aroundConstruct = method;
} else if (postConstruct == null && method.hasAnnotation(DotNames.POST_CONSTRUCT)) {
postConstruct = method;
} else if (preDestroy == null && method.hasAnnotation(DotNames.PRE_DESTROY)) {
preDestroy = method;
}

DotName superTypeName = aClass.superName();
aClass = superTypeName == null || DotNames.OBJECT.equals(superTypeName) ? null
: getClassByName(beanDeployment.getIndex(), superTypeName);
}
this.aroundInvoke = aroundInvoke;
this.aroundConstruct = aroundConstruct;
this.postConstruct = postConstruct;
this.preDestroy = preDestroy;

this.aroundInvoke = aroundInvokes.isEmpty() ? null : aroundInvokes.get(0);
this.aroundConstruct = aroundConstructs.isEmpty() ? null : aroundConstructs.get(0);
this.postConstruct = postConstructs.isEmpty() ? null : postConstructs.get(0);
this.preDestroy = preDestroys.isEmpty() ? null : preDestroys.get(0);
if (aroundConstruct == null && aroundInvoke == null && preDestroy == null && postConstruct == null) {
LOGGER.warnf("%s declares no around-invoke method nor a lifecycle callback!", this);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.arc.test.interceptors.inheritance;

import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;

@One
@Interceptor
public class Interceptor1 extends OverridenInterceptor {

@AroundInvoke
@Override
public Object intercept(InvocationContext ctx) throws Exception {
return ctx.proceed() + "1";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.quarkus.arc.test.interceptors.inheritance;

import javax.interceptor.Interceptor;

@Two
@Interceptor
public class Interceptor2 extends OverridenInterceptor {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.quarkus.arc.test.interceptors.inheritance;

import static org.junit.jupiter.api.Assertions.assertEquals;

import io.quarkus.arc.Arc;
import io.quarkus.arc.test.ArcTestContainer;
import javax.enterprise.context.ApplicationScoped;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

/**
* "Around-invoke methods may be defined on interceptor classes and/or the target class and/or super-
* classes of the target class or the interceptor classes. However, only one around-invoke method may be
* defined on a given class."
*/
public class InterceptorSuperclassTest {

@RegisterExtension
public ArcTestContainer container = new ArcTestContainer(Interceptor1.class, Interceptor2.class, One.class, Two.class,
OverridenInterceptor.class, Fool.class);

@Test
public void testInterception() {
Fool fool = Arc.container().instance(Fool.class).get();
assertEquals("ping1", fool.pingOne());
assertEquals("pingoverriden", fool.pingTwo());
}

@ApplicationScoped
public static class Fool {

@One
String pingOne() {
return "ping";
}

@Two
String pingTwo() {
return "ping";
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.arc.test.interceptors.inheritance;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.interceptor.InterceptorBinding;

@Target({ TYPE, METHOD })
@Retention(RUNTIME)
@Documented
@InterceptorBinding
public @interface One {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.quarkus.arc.test.interceptors.inheritance;

import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;

public class OverridenInterceptor {

@AroundInvoke
public Object intercept(InvocationContext ctx) throws Exception {
return ctx.proceed() + "overriden";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.arc.test.interceptors.inheritance;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.interceptor.InterceptorBinding;

@Target({ TYPE, METHOD })
@Retention(RUNTIME)
@Documented
@InterceptorBinding
public @interface Two {

}
Loading

0 comments on commit 2f645d4

Please sign in to comment.