spring-modulith
spring-modulith copied to clipboard
Allow inclusion of test-specific beans
Discussed in https://github.com/spring-projects/spring-modulith/discussions/201
Originally posted by genuss May 3, 2023
First of all, thank you for a great project. I just started to dig in and it looks very promising!
Currently I'm trying to arrange some module tests and faced up an issue which I can't solve now. To be more clear in what I'm going to explain now I created an example project.
The project consists of two modules: first and second which don't do anything as it's not important now.
What I'm trying to achieve is to create some FirstModuleTestHelper which acts as some text fixture and is used by autowiring it in test-classes or other fixtures. This has always worked and works now with @SpringBootTest but it doesn't work with @ApplicationModuleTest and fails with this exception:
Error creating bean with name 'com.example.springmodulithmoduletests.first.ModuleTest': Unsatisfied dependency expressed through field 'helper': No qualifying bean of type 'com.example.springmodulithmoduletests.first.FirstModuleTestHelper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.example.springmodulithmoduletests.first.ModuleTest': Unsatisfied dependency expressed through field 'helper': No qualifying bean of type 'com.example.springmodulithmoduletests.first.FirstModuleTestHelper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
at app//org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:713)
at app//org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:693)
at app//org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:133)
at app//org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:482)
at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1416)
at app//org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireBeanProperties(AbstractAutowireCapableBeanFactory.java:396)
at app//org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:142)
at app//org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:97)
at app//org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:241)
at app//org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:138)
at app//org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$10(ClassBasedTestDescriptor.java:377)
at app//org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.executeAndMaskThrowable(ClassBasedTestDescriptor.java:382)
at app//org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$11(ClassBasedTestDescriptor.java:377)
at [email protected]/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at [email protected]/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
at [email protected]/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
at [email protected]/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at [email protected]/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at [email protected]/java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:310)
at [email protected]/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:735)
at [email protected]/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:762)
at app//org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestInstancePostProcessors(ClassBasedTestDescriptor.java:376)
at app//org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$instantiateAndPostProcessTestInstance$6(ClassBasedTestDescriptor.java:289)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcessTestInstance(ClassBasedTestDescriptor.java:288)
at app//org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$4(ClassBasedTestDescriptor.java:278)
at [email protected]/java.util.Optional.orElseGet(Optional.java:364)
at app//org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$5(ClassBasedTestDescriptor.java:277)
at app//org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:31)
at app//org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$before$2(ClassBasedTestDescriptor.java:203)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.before(ClassBasedTestDescriptor.java:202)
at app//org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.before(ClassBasedTestDescriptor.java:84)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:148)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at [email protected]/java.util.ArrayList.forEach(ArrayList.java:1511)
at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
at app//org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at app//org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
at app//org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
at app//org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
at app//org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
at app//org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
at app//org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
at app//org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
at app//org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
at app//org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:110)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:90)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:85)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:62)
at [email protected]/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at [email protected]/java.lang.reflect.Method.invoke(Method.java:577)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
at jdk.proxy1/jdk.proxy1.$Proxy2.stop(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.springmodulithmoduletests.first.FirstModuleTestHelper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1824)
at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1383)
at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1337)
at app//org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:710)
... 81 more
I tried to debug this, but being unfamiliar with code, didn't have any luck. Looks like it's somehow connected to archunit's ImportOption but I'm not sure.
Anyway, what do you think about extending module test context to take into account such beans?
@odrotbohm I tried includind a WebTestClient in an integration test using @ApplicationModuleTest like this:
@ApplicationModuleTest
@AutoConfigureWebTestClient
class HabitControllerIntegrationTest {
@Autowired private lateinit var webTestClient: WebTestClient
@Test
fun `should return habits`() {
webTestClient.get().uri("/habits")
.exchange()
.expectStatus().isOk()
.expectBody<List<Habit>>()
.value { assertThat(it).hasSize(1) }
}
}
Test execution fails with No qualifying bean of type 'org.springframework.test.web.reactive.server.WebTestClient' available. Using @SpringBootTest fixes the issue.
I this a related issue, unrelated issue or am I doing something wrong entirely?
It looks like the ContextCustomizerFactory to register the WebTestClient instance is tied to @SpringBootTest as that allows the configuration of webEnvironment() which the factory implementation inspects to determine whether to register the configuration for the WTC instance.
Am I assuming right, that the working alternative is not a plain @SpringBootTest but a @SpringBootTest(webEnvironment=RANDOM_PORT / DEFINED_PORT)? The default MOCK does not seem to trigger the WTC instance configuration as it's not considered embedded (see WebTestClientContextCustomizer).
Thank you for pointing me to the WebTestClientContextCustomizer, I didn't know how the @AutoConfigureWebTestClient annotation works internally.
I totally agree with you that it should™ only work with webEnvironment=RANDOM_PORT / DEFINED_PORT. Interestingly enough, it works with a plain @SpringBootTest as well. I debugged the test to evaluate the code in the WebTestClientContextCustomizer and the following code block indeed does not execute registerWebTestClient(context).
if (springBootTest.webEnvironment().isEmbedded()) {
registerWebTeslient(context);
}
I am not sure why the test works, but it does. Is there something else besides the WebTestClientContextCustomizer that may register a WebTestClient instance?
Also: I guess you won't be able to fix the compatiblity with @ApplicationModuleTest in Spring Modulith since the @AutoConfigureWebTestClient Spring code is so inherently coupled with @SpringBootTest, right?
EDIT:
I checked the AutoConfigureWebTestClient.imports file. It loads the WebTestClientAutoConfiguration.
This is the code path used when using plain @SpringBootTest with its default parameters.
Wait a second, you're using @AutoConfigureWebTestClient explicitly. That reads the auto-configurations to include from META-INF/spring//META-INF/spring/org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient.imports which are in turn standard autoconfigurations and should kick in as expected.
Is there any chance you can put together a simple (Java/Maven) example project that shows the injection fail using @ApplicationModuleTest? I'd like to take a look and see why the autoconfiguration is not kicking in.
Wow you are fast. 😄 ❤️
As luck would have it, I set up a sample project today anyways to evaluate Spring Modulith for us. Here it is: https://github.com/denniseffing/spring-modulith-test
Oh sorry, it uses Kotlin/Gradle, I hope that's okay. Let me know if you need a Java/Maven sample as well.
If you don't mind, I'd really appreciate a Java/Maven version as I then don't have to fight those and IDEA to get the stuff debugged. 😬
Well I guess this explains why the Spring Modulith reference is Java/Maven only, considering almost all Spring references have documentation samples for Kotlin and Gradle as well. 😄
But no worries, here you go: https://github.com/denniseffing/spring-modulith-test-java
Thanks for that. The difference is the presence of a WebHandler bean that then triggers the inclusion of WebTestClientAutoConfiguration.webTestClient(). I assume that this in turn is registered due to the web environment defaulted to MOCK.
I think I'll have to give it some thought, but as it looks right now, we should be able to simply meta-annotate @ApplicationModuleTest with @SpringBootTest as the latter primarily registers additional extensions and a hook to constrain the classpath scanning. I couldn't see anything hazardous being kicked off by simply annotating the test with both annotations even.
I've filed and fixed #253 to make sure standard, test related auto-configuration can read configuration of the @SpringBootTest application. For a start, I have aliased the webEnvironment() attribute, because I guess it might come in handy when testing a vertical slice top to bottom.
Thank you for the fast fix!
@denniseffing – I've also created #255 and #256 to track the extension of the documentation for Gradle and Kotlin.