Skip to content

Conversation

mkouba
Copy link
Contributor

@mkouba mkouba commented Sep 19, 2025

The goal of this pull request is to refactor the class loading used for QuarkusComponentTest, i .e. use a transformation-aware class loader to load a test class annotated with @QuarkusComponentTest. This way we can support things like simplified constructor injection.

Implementation-wise - we hook into the FacaClassLoader logic (if present) and similarly to how @QuarkusTest is handled, we build the container (ArC in case of QuarkusComponentTest) when the test classes are loaded (note that there must be a special handling for continuous testing where io.quarkus.deployment.dev.testing.JunitTestRunner is used to load the test classes).

When I discussed this PR with Ladislav and Matej, we had an idea that quarkus-junit5 and quarkus-junit5-component could be merged in one artifact. However, it turns out we would have to add a bunch of dependencies to quarkus-junit5 (including ArC, but what's worse also Mockito + bytebuddy - this combo has ~ 4MB of jars). So I decided to introduce the io.quarkus.test.common.FacadeClassLoaders SPI instead. It lives in the quarkus-test-common and the name is terrible, but it works soo 🤷.

There's one use case where the new approach fails. It's a QuarkusDevModeTest iff quarkus-junit5 is not present, something along the lines of https://github.com/quarkusio/quarkus/blob/main/integration-tests/devmode/src/test/java/io/quarkus/test/component/ComponentContinuousTestingTest.java. I found that quarkus-junit5 provides QuarkusTestConfigProviderResolver that somehow overrides io.quarkus.test.config.TestConfigProviderResolver to avoid Context ClassLoader mismatch. But I have no idea why/how and why TestConfigProviderResolver exists for the first place.

This change should not break any existing apps but the test coverage is limited..

@Ladicek
Copy link
Contributor

Ladicek commented Sep 22, 2025

When I discussed this PR with Ladislav and Matej, we had an idea that quarkus-junit5 and quarkus-junit5-component could be merged in one artifact. However, it turns out we would have to add a bunch of dependencies to quarkus-junit5 (including ArC, but what's worse also Mockito + bytebuddy - this combo has ~ 4MB of jars). So I decided to introduce the io.quarkus.test.common.FacadeClassLoaders SPI instead. It lives in the quarkus-test-common and the name is terrible, but it works soo 🤷.

Not an expert, but could we maybe move the whole infrastructure to quarkus-test-common? (By infrastructure, I mean CustomLauncherInterceptor, FacadeClassLoader, the SPI you added here and maybe a few other classes.) This would require quarkus-test-common to actually depend on junit-jupiter, which doesn't necessarily have to be a problem, and it would let us ditch ComponentLauncherSessionListener. We would always have the same one present. The SPI you added here (FacadeClassLoaders) should be renamed (:laughing:) and provide at least some notion of lifecycle (to allow cleaning up at the end). Again, not an expert, perhaps that doesn't make much sense.

Also, I'm not sure I'm able to do a proper review here, certainly not without playing with this locally for a while :-)

@mkouba
Copy link
Contributor Author

mkouba commented Sep 22, 2025

When I discussed this PR with Ladislav and Matej, we had an idea that quarkus-junit5 and quarkus-junit5-component could be merged in one artifact. However, it turns out we would have to add a bunch of dependencies to quarkus-junit5 (including ArC, but what's worse also Mockito + bytebuddy - this combo has ~ 4MB of jars). So I decided to introduce the io.quarkus.test.common.FacadeClassLoaders SPI instead. It lives in the quarkus-test-common and the name is terrible, but it works soo 🤷.

Not an expert, but could we maybe move the whole infrastructure to quarkus-test-common? (By infrastructure, I mean CustomLauncherInterceptor, FacadeClassLoader, the SPI you added here and maybe a few other classes.) This would require quarkus-test-common to actually depend on junit-jupiter, which doesn't necessarily have to be a problem, and it would let us ditch ComponentLauncherSessionListener. We would always have the same one present.

I'm not an expert either but I don't feel confident enough to do such a bold move. Also FaceClassLoader depends on many other classes from the quarkus-junit such as io.quarkus.test.junit.AppMakerHelper and I'm not really sure it will be that easy to move them without breaking stuff.

The SPI you added here (FacadeClassLoaders) should be renamed (:laughing:) and provide at least some notion of lifecycle (to allow cleaning up at the end). Again, not an expert, perhaps that doesn't make much sense.

Right now, I think that we don't need any cleanup... which doesn't mean we won't need it in the future but I'd like to keep the SPI minimal. If possible, it should be considered internal. But AFAIK the only thing we could do is to add a sentence in the javadoc.

Speaking of the name - do you have a better idea? Because I'm out of ideas...

Also, I'm not sure I'm able to do a proper review here, certainly not without playing with this locally for a while :-)

No problem. Your feeedback is really appreciated! 🙏

@Ladicek
Copy link
Contributor

Ladicek commented Sep 22, 2025

I'm not an expert either but I don't feel confident enough to do such a bold move. Also FaceClassLoader depends on many other classes from the quarkus-junit such as io.quarkus.test.junit.AppMakerHelper and I'm not really sure it will be that easy to move them without breaking stuff.

Makes sense.

The SPI you added here (FacadeClassLoaders) should be renamed (:laughing:) and provide at least some notion of lifecycle (to allow cleaning up at the end). Again, not an expert, perhaps that doesn't make much sense.

Right now, I think that we don't need any cleanup... which doesn't mean we won't need it in the future but I'd like to keep the SPI minimal. If possible, it should be considered internal. But AFAIK the only thing we could do is to add a sentence in the javadoc.

👍

Speaking of the name - do you have a better idea? Because I'm out of ideas...

Nothing extraordinary. I'd just call it FacadeClassLoaderProvider or something.

@mkouba mkouba force-pushed the component-classloading branch from 44f090b to 2ba3a7d Compare September 23, 2025 07:53
@mkouba mkouba marked this pull request as ready for review September 23, 2025 07:53
@manovotn
Copy link
Contributor

I am going through the code now but in the meantime - I was thinking whether it would be worth adding a note to the documentation stating that component tests now support transformations?

This comment has been minimized.

@mkouba
Copy link
Contributor Author

mkouba commented Sep 23, 2025

I am going through the code now but in the meantime - I was thinking whether it would be worth adding a note to the documentation stating that component tests now support transformations?

Once we merge this pull request I'd like to write a blog post that would mention all recent improvements (incl. @InjectMock Event). Basically a follow-up of https://quarkus.io/blog/quarkus-component-test/. Speaking of the docs - we didn't mention it does not work so I'm not really sure it's important 🤷.

@manovotn
Copy link
Contributor

I am going through the code now but in the meantime - I was thinking whether it would be worth adding a note to the documentation stating that component tests now support transformations?

Once we merge this pull request I'd like to write a blog post that would mention all recent improvements (incl. @InjectMock Event). Basically a follow-up of https://quarkus.io/blog/quarkus-component-test/.

That's great idea! 👍

Speaking of the docs - we didn't mention it does not work so I'm not really sure it's important 🤷.

I know, but fair enough.
My thinking was that users might have encountered this shortcoming before and gave up on using the extension; I was just looking for a way to rise awareness. I suppose a blogpost would work just as well :)

@mkouba
Copy link
Contributor Author

mkouba commented Sep 24, 2025

BTW I think that this would open the door to other possibilities. For example, we could try to add support Panache mocking for the active record pattern.

Copy link
Contributor

@manovotn manovotn left a comment

Choose a reason for hiding this comment

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

Added a few comments and questions.
Take them with a pinch of salt as while I understand what the PR does, I certainly do have hard time understanding how exactly with all the class loader juggling :)

@mkouba
Copy link
Contributor Author

mkouba commented Sep 24, 2025

Added a few comments and questions. Take them with a pinch of salt as while I understand what the PR does, I certainly do have hard time understanding how exactly with all the class loader juggling :)

Totally understand, it took me a few days to understand at least part of the class loading code...

@mkouba mkouba force-pushed the component-classloading branch from 2ba3a7d to e88c55f Compare September 25, 2025 10:25
Copy link

quarkus-bot bot commented Sep 25, 2025

Status for workflow Quarkus CI

This is the status report for running Quarkus CI on commit e88c55f.

✅ The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

You can consult the Develocity build scans.


Flaky tests - Develocity

⚙️ JVM Tests - JDK 17

📦 extensions/smallrye-reactive-messaging/deployment

io.quarkus.smallrye.reactivemessaging.hotreload.ConnectorChangeTest.testUpdatingConnector - History

  • Expecting actual: ["-4","-5","-6","-7","-8","-9","-10","-11"] to start with: ["-3", "-4", "-5", "-6"] - java.lang.AssertionError
java.lang.AssertionError: 

Expecting actual:
  ["-4","-5","-6","-7","-8","-9","-10","-11"]
to start with:
  ["-3", "-4", "-5", "-6"]

	at io.quarkus.smallrye.reactivemessaging.hotreload.ConnectorChangeTest.testUpdatingConnector(ConnectorChangeTest.java:36)

⚙️ JVM Tests - JDK 21

📦 extensions/smallrye-openapi/deployment

io.quarkus.smallrye.openapi.test.vertx.OpenApiHttpRootPathCorsTestCase.testCorsFilterProperties - History

  • 1 expectation failed. Expected status code <200> but was <500>. - java.lang.AssertionError
java.lang.AssertionError: 
1 expectation failed.
Expected status code <200> but was <500>.

	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486)
	at org.codehaus.groovy.reflection.CachedConstructor.invoke(CachedConstructor.java:73)

⚙️ Maven Tests - JDK 17

📦 integration-tests/devmode

io.quarkus.test.devui.DevUIGrpcSmokeTest.testTestService - History

  • Too many recursions, message not returned for id [1184811301] - java.lang.RuntimeException
java.lang.RuntimeException: Too many recursions, message not returned for id [1184811301]
	at io.quarkus.devui.tests.DevUIJsonRPCTest.objectResultFromJsonRPC(DevUIJsonRPCTest.java:175)
	at io.quarkus.devui.tests.DevUIJsonRPCTest.objectResultFromJsonRPC(DevUIJsonRPCTest.java:178)
	at io.quarkus.devui.tests.DevUIJsonRPCTest.objectResultFromJsonRPC(DevUIJsonRPCTest.java:178)
	at io.quarkus.devui.tests.DevUIJsonRPCTest.objectResultFromJsonRPC(DevUIJsonRPCTest.java:178)
	at io.quarkus.devui.tests.DevUIJsonRPCTest.objectResultFromJsonRPC(DevUIJsonRPCTest.java:178)
	at io.quarkus.devui.tests.DevUIJsonRPCTest.objectResultFromJsonRPC(DevUIJsonRPCTest.java:178)
	at io.quarkus.devui.tests.DevUIJsonRPCTest.objectResultFromJsonRPC(DevUIJsonRPCTest.java:178)

⚙️ Gradle Tests - JDK 17

📦 integration-tests/gradle

io.quarkus.gradle.TestFixturesClientExceptionMapperTest.testBasicMultiModuleBuild - History

  • Gradle build failed with exit code 1 - java.lang.AssertionError
java.lang.AssertionError: Gradle build failed with exit code 1
	at io.quarkus.gradle.QuarkusGradleWrapperTestBase.runGradleWrapper(QuarkusGradleWrapperTestBase.java:173)
	at io.quarkus.gradle.QuarkusGradleWrapperTestBase.runGradleWrapper(QuarkusGradleWrapperTestBase.java:87)
	at io.quarkus.gradle.QuarkusGradleWrapperTestBase.runGradleWrapper(QuarkusGradleWrapperTestBase.java:82)
	at io.quarkus.gradle.TestFixturesClientExceptionMapperTest.testBasicMultiModuleBuild(TestFixturesClientExceptionMapperTest.java:12)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
  • Gradle build failed with exit code 1 - java.lang.AssertionError
java.lang.AssertionError: Gradle build failed with exit code 1
	at io.quarkus.gradle.QuarkusGradleWrapperTestBase.runGradleWrapper(QuarkusGradleWrapperTestBase.java:173)
	at io.quarkus.gradle.QuarkusGradleWrapperTestBase.runGradleWrapper(QuarkusGradleWrapperTestBase.java:87)
	at io.quarkus.gradle.QuarkusGradleWrapperTestBase.runGradleWrapper(QuarkusGradleWrapperTestBase.java:82)
	at io.quarkus.gradle.TestFixturesClientExceptionMapperTest.testBasicMultiModuleBuild(TestFixturesClientExceptionMapperTest.java:12)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
  • Gradle build failed with exit code 1 - java.lang.AssertionError
java.lang.AssertionError: Gradle build failed with exit code 1
	at io.quarkus.gradle.QuarkusGradleWrapperTestBase.runGradleWrapper(QuarkusGradleWrapperTestBase.java:173)
	at io.quarkus.gradle.QuarkusGradleWrapperTestBase.runGradleWrapper(QuarkusGradleWrapperTestBase.java:87)
	at io.quarkus.gradle.QuarkusGradleWrapperTestBase.runGradleWrapper(QuarkusGradleWrapperTestBase.java:82)
	at io.quarkus.gradle.TestFixturesClientExceptionMapperTest.testBasicMultiModuleBuild(TestFixturesClientExceptionMapperTest.java:12)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

@mkouba
Copy link
Contributor Author

mkouba commented Sep 29, 2025

@Ladicek @manovotn @holly-cummins I'd like to merge this pull request today. It's not perfect but we could address potential issues in the follow ups. I already have a POC for extension points called QuarkusComponentTestCallbacks and integration with quarkus-panache-mock that makes it possible to mock Panache entitites in a QuarkusComponentTest.

@mkouba mkouba merged commit e5167c6 into quarkusio:main Sep 29, 2025
57 checks passed
@quarkus-bot quarkus-bot bot added this to the 3.29 - main milestone Sep 29, 2025
@quarkus-bot quarkus-bot bot added the kind/enhancement New feature or request label Sep 29, 2025
Comment on lines +106 to +117
private static boolean mustDelegateToParent(String name) {
return name.startsWith("java.")
|| name.startsWith("jdk.")
|| name.startsWith("javax.")
|| name.startsWith("jakarta.")
|| name.startsWith("sun.")
|| name.startsWith("com.sun.")
|| name.startsWith("org.w3c.")
|| name.startsWith("org.xml.")
|| name.startsWith("org.junit.")
|| name.startsWith("org.mockito.")
|| PARENT_CL_CLASSES.contains(name);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any way to get this code more common with the QuarkusClassLoader? I seem to recall doing a few hacky overrides to what gets loaded parent-first over there to support dev services work. It might be (a) less code and (b) fewer confusing bugs if we had a common service.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure TBH. But I admit that this does not look very smart (it's more or less a result of experimentation) and we should get back to that sometime.

Copy link
Contributor

Choose a reason for hiding this comment

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

The list here looks quite close to what I had in #35473 and that wasn't a result of experimentation -- that was a list of packages provided by the JDK. There's a few packages added here (jakarta.*, org.junit.*, org.mockito.*) which make perfect sense, but still -- it's a list of packages we know are never provided by the application and make no sense to be loaded by the application class loader.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's a list of packages we know are never provided by the application and make no sense to be loaded by the application class loader.

On the other hand, I observed weird errors while adding other packages which are also never provided by the app 🤷.

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, that sounds strange then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

QuarkusComponentTest: support simplified constructor injection
4 participants