Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the @TestTransaction annotation #11798

Merged
merged 2 commits into from
Sep 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Member

@famod famod Sep 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for having considered my flush()-suggestion. Very cool approach with this new TestTransactionInterceptor!

I am wondering whether this block could kick in cases where it is not really needed? Usually ServiceLoader calls are not super-cheap.
I used to apply https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom in my projects but I don't know whether this would make any difference here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, the callbacks are only loaded once per test config...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said, we could easily turn the list into a io.quarkus.arc.impl.LazyValue<List<TestTransactionCallback>> ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current code is fine. ServiceLoader is expensive in terms of 'don't do this every request so you are doing it tens of thousands of times per second', its fine for this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for your feedback.

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