-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Quarkus CDI-managed beans not available in Pact state callbacks #22611
Comments
This is indeed the case, two instances are created in different ClassLoaders, unfortunately there is no way around that.
This is bound to break a lot of things :) |
That's what I thought, would also be too easy :D. The I remember now, that Quarkus provides its own callbacks with the proper ClassLoader, maybe we can "hack" a way to wrap the Pact class with this. |
Yeah, we would need something like that |
FYI: We now have a super hacky solution, but at least we can continue work now First we have an abstract class, which every REST Pact Verificiation test relies on and in that we use two helper classes ( First here's the abstract class, maybe just skim over it - the usage of the helper classes are explained after the code. public abstract class AbstractPactHttpTest {
protected static Map<String, Object> instanceMap = loadInstanceMap();
@Inject
@Any
protected InMemoryConnector connector;
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
if (context != null) {
context.verifyInteraction();
}
}
@BeforeEach
void before(PactVerificationContext context) {
if (context != null) {
HttpTestTarget testTarget = new HttpTestTarget("localhost", 8081);
context.setTarget(testTarget);
final QuarkusClassLoader quarkusClassLoader = (QuarkusClassLoader) connector.getClass().getClassLoader();
// store the QuarkusClassLoader, so we can use it later to load classes
putInstance(QuarkusClassLoader.class, quarkusClassLoader);
// store CDI-managed beans for use within the helpers
putInstance(InMemoryConnector.class, connector);
// initialize helpers with the instanceMap
putInstance(PactRestVerificationStates.class, new PactRestVerificationStates(instanceMap));
}
}
@SuppressWarnings("unchecked")
protected static Map<String, Object> loadInstanceMap() {
try {
// load PactClassLoaderAttributeHolder from the system classloader, so it can be shared between distinct classloaders.
final Class<?> attributeHolderClass = ClassLoader.getSystemClassLoader().loadClass(PactClassLoaderAttributeHolder.class.getName());
final Method getInstanceMapMethod = attributeHolderClass.getMethod("getInstanceMap");
return (Map<String, Object>) getInstanceMapMethod.invoke(null);
} catch(Exception e) {
throw new IllegalStateException("An unexpected error occurred, while loading PactRestVerificationStates: ", e);
}
}
protected static <T> void putInstance(Class<T> clazz, T instance) {
instanceMap.put(clazz.getName(), instance);
}
@SuppressWarnings("unchecked")
protected static <T> T getInstance(Class<T> clazz) {
return (T) getInstance(clazz.getName());
}
protected static Object getInstance(String className) {
return instanceMap.get(className);
}
@SuppressWarnings("unchecked")
protected static <T> T invokeMethod(String helperClassName, String methodName) {
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
try {
// load the helper instance within the quarkus classloader context, so we can use the CDI beans.
Thread.currentThread().setContextClassLoader(getInstance(QuarkusClassLoader.class));
Object helperInstance = getInstance(helperClassName);
if (helperInstance == null) {
throw new IllegalArgumentException("Could not find instance for " + helperClassName);
}
final Method helperMethod = helperInstance.getClass().getMethod(methodName);
return (T) helperMethod.invoke(helperInstance);
} catch(Exception e) {
throw new IllegalStateException("An unexpected error occurred, while loading invoking the helper: ", e);
} finally {
Thread.currentThread().setContextClassLoader(originalClassLoader);
}
}
} In that we use a class ( public class PactClassLoaderAttributeHolder {
private static Map<String, Object> instanceMap;
public static Map<String, Object> getInstanceMap() {
if (instanceMap == null) {
instanceMap = new HashMap<>();
}
return instanceMap;
}
} These instances will then be used by another class, which will be loaded with the Quarkus ClassLoader: public class PactRestVerificationStates {
private final Map<String, Object> instanceMap;
public PactRestVerificationStates(Map<String, Object> instanceMap) {
this.instanceMap = instanceMap;
}
public void createMySuperAwesomeState() {
// get the required CDI bean
InMemoryConnector connector = (InMemoryConnector) instanceMap.get(InMemoryConnector.class.getName());
// do something with the CDI bean
}
} Then finally we have the actual Pact provider test, which unfortunately cannot use direct referenced, or the classes are loaded within the wrong ClassLoader: @QuarkusTest
@Provider("my-provider")
public class MyRestProviderPactVerification extends AbstractPactHttpTest {
@State("my super awesome state")
public void mySuperAwesomeState() {
invokeMethod("com.example.infrastructure.pact.PactRestVerificationStates", "createMySuperAwesomeState");
}
} |
I would say, that this can be closed - as it's unlikely that something about the ClassLoader will be changed because of this. It's still not working without the above mentioned workaround, but at least that workaround is available. |
@JapuDCret thanks for your workaround - Can you please share the import section of your classes too . I am currently wondering which |
@EnvyIT we use |
Well, this is still a major issue in my opinion. The fact that it is not possible to use DI or Mockito for state callbacks are a huge disadvantage in comparison to other frameworks. This workaround is indeed super-hacky and not working when you want to use Mockito proxy classes. |
@edeandrea has come up with another workaround, which is still pretty hacky, but is slightly less verbose. In a So we can do the state processing in
|
You also still need the |
/cc @geoand |
I've semi-duplicated this as quarkiverse/quarkus-pact#2. It may be too hard/irrelevant to fix in main Quarkus, but something that an extension can fix. I wonder if #27821 is related? Both have to do with undesirable classloaders being used in some parts of JUnit 5 test execution. |
Any updates on this? I've stumbled upon this too, it is very annoying having to do a weird workaround to be able to access Hibernate Entities through the |
"Update" is a strong word, but I have a plan-for-a-plan. The root cause of many of the issues we see where code in Pact tests loses access to Quarkus-y enhancements (like injected CDI beans) is classloading. We want the test classes to run with the Quarkus classloader, but because of the JUnit lifecycle manipulations, it ends up running in the system classloader. We've been a bit blocked, because there wasn't a way to tell JUnit to use an alternate classloader. However, apparently in JUnit 5.10, that limitation is lifted. JUnit 5.10 isn't released yet, so any fix would be a ways off, but I'm planning to start experiments to confirm we actually can switch classloaders, and see how many of these kinds of issues get fixed. |
Thanks for the update @holly-cummins! Now I know someone is looking on it and has some sort of plan at least, I understand it can take awhile, but that's alright to me. I'm just happy the issue hasn't been tossed to the side and forgotten in the dust. 🙂 |
No no this is very much in the forefront! @holly-cummins and I are doing a lot of talking about Pact coming up in the next few months (https://devnexus.com/presentations/avoiding-common-pitfalls-with-modern-microservices-testing, https://www.devoxx.co.uk/talk/?id=1970, https://www.devbcn.com/talk/423678). @holly-cummins is definitely the brains behind the operation, but it is definitely something we are continually enhancing. |
Hi guys, any updates? |
Sort of! I'm in the (slow) process of rewriting how Quarkus does its test classloading. I think that work should resolve this issue. I'd missed this when I made the list of issues https://github.com/orgs/quarkusio/projects/30 should/might fix, so I've added it to the tracking for that project. |
Problem
We have a Pact REST provider test with state callbacks, where we do not want to use testcontainers to startup the application, but rather a normal
@QuarkusTest
.For easy use-cases this works, as mentioned in #9677 and demonstrated in https://github.com/skattela/pact-workshop-jvm-quarkus.
But when you need to use
@State
callbacks to initialize some state in your application, then you'll find out, that you cannot use CDI-managed beans.Investigation
I investigated this issue and found out, that with that setup we generate two different test class instances.
I think this is normally also the case, but here Pact uses the instance, where Quarkus does not inject anything.
Here's the first instance creation (which will then be used by Pact)
https://github.com/quarkusio/quarkus/blob/2.6/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java#L687
A little below
initTestState(extensionContext, state);
is called, and it creates the "proper" instance:https://github.com/quarkusio/quarkus/blob/2.6/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java#L727
Possible solutions
Maybe we can return the(see first comment)actualTestInstance
withininterceptTestClassConstructor()
.Environment (please complete the following information):
Windows 10 - Version 20H2 (Build 19042.1415)
openjdk version "11.0.10" 2021-01-19
n/a
2.5.0-Final
Apache Maven 3.8.4
Misc
Originally posted by @JapuDCret in #9677 (comment)
The text was updated successfully, but these errors were encountered: