Skip to content

Support for bean instances located in test sources for @ApplicationModuleTest #202

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

Closed
odrotbohm opened this issue May 5, 2023 Discussed in #201 · 23 comments
Closed

Support for bean instances located in test sources for @ApplicationModuleTest #202

odrotbohm opened this issue May 5, 2023 Discussed in #201 · 23 comments
Assignees
Labels
in: test support Spring Boot integration testing type: enhancement Major enhanvements, new features
Milestone

Comments

@odrotbohm
Copy link
Member

Discussed in #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 odrotbohm added the in: test support Spring Boot integration testing label May 5, 2023
@odrotbohm odrotbohm self-assigned this May 5, 2023
@odrotbohm odrotbohm changed the title Module tests with test beans Allow inclusion of test-specific beans May 5, 2023
@denniseffing
Copy link

@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?

@odrotbohm
Copy link
Member Author

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).

@denniseffing
Copy link

denniseffing commented Aug 1, 2023

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.

@odrotbohm
Copy link
Member Author

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.

@denniseffing
Copy link

denniseffing commented Aug 1, 2023

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.

@odrotbohm
Copy link
Member Author

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. 😬

@denniseffing
Copy link

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

@odrotbohm
Copy link
Member Author

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.

odrotbohm added a commit that referenced this issue Aug 2, 2023
…tTest.

@ApplicationModuleTest is now meta-annotated with @SpringBootTest. This allows us to remove a couple of declarations that we actually had copied from it (such as the TestContextBootstrapper, the SpringExtension etc.)

The presence of the original annotation allow test-related auto-configuration to inspect @SprignBootTest for particular configuration. For example, we now alias the WebEnvironment to make it configurable for the test execution.
@odrotbohm
Copy link
Member Author

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.

@denniseffing
Copy link

Thank you for the fast fix!

@odrotbohm
Copy link
Member Author

@denniseffing – I've also created #255 and #256 to track the extension of the documentation for Gradle and Kotlin.

@KarasDominik
Copy link

I read through this discussion but either I missed the solution of the topic or it is not there. Currently, I am working on a small Spring Modulith project and I just found out that @ApplicationModuleTest annotation does not import test beans to the spring context. Is there any other solution to this apart from doing it manually with @componentscan for example?

@odrotbohm
Copy link
Member Author

@KarasDominik – Can you elaborate what “… @ApplicationModuleTest annotation does not import test beans to the spring context.” means? The annotation does not import anything. All it does is restricting autoconfiguration and component scanning to be restricted to the packages of modules included in the test run.

Do you have a reproducer handy that shows behavior you consider erroneous?

@KarasDominik
Copy link

@odrotbohm I have exactly the same problem as described here: #201 (or in the first post of this issue). I have some module which contains some beans and when I try to test this module I need an additional bean (which is defined only in tests, not production code). As a result, when try to autowire this bean into my test and run it I get the following error:
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.karasdominik.QuickCart.product.infrastructure.adapters.outbound.persistence.ProductAssertions' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

@odrotbohm
Copy link
Member Author

It's hard to assess without a reproducer. Are you sure the type of that test bean is located in any of the packages scanned during the test execution?

@KarasDominik
Copy link

KarasDominik commented Mar 14, 2025

Here is some sample repo: https://github.com/KarasDominik/QuickCart/
If you go to src/test/java/com/karasdominik/QuickCart/product/infrastructure/adapters/inbound/rest/ package you will find a test class with @componentscan annotation, without the test fails, you can run it locally

@odrotbohm
Copy link
Member Author

Lovely, I'll have a look ASAP.

@odrotbohm
Copy link
Member Author

I found the root of the problem. We hook into the component scanning in a way that checks whether scanned classes are contained in the packages we selected for bootstrap. That contains check inspects the actual classes in that package based on the ArchUnit import of types. That in turn only sees classes from the production classpath. This is why any code from the test sources is excluded and the bootstrap fails.

I guess we need to switch to a name comparison for this particular use case as at this point, we're only interested whether a type is logically located in the package, not necessarily physically located in the package we ArchUnit-scanned.

@KarasDominik
Copy link

Thanks for breaking it down! I am not gonna lie, I am new to spring modulith and I am not sure if the solution you explained is available within the library?

@odrotbohm
Copy link
Member Author

Nope, I need to fix it. I just wanted to give you a heads up.

odrotbohm added a commit that referenced this issue Mar 17, 2025
…cationModuleTest.

Prior to this commit, the TypeExcludeFilter registered by @ApplicationModuleTest decided whether to include a type based on the ApplicationModules instance and the content of the modules' backing JavaPackage instances. Those in turn always consider the classes scanned by ArchUnit to decide whether they include a type or not. As an ApplicationModules instance is set up to only consider production code, any type located in the test sources was disregarded from component scanning.

The checks for package inclusion for a test execution have now been revamped to consider the sole package names when filtering types for inclusion.
@odrotbohm
Copy link
Member Author

This should be in place. Would you mind giving the 1.4 snapshots a try?

@odrotbohm odrotbohm added the type: enhancement Major enhanvements, new features label Mar 18, 2025
@odrotbohm odrotbohm added this to the 1.4 M3 milestone Mar 18, 2025
@odrotbohm odrotbohm changed the title Allow inclusion of test-specific beans Support for bean instances located in test sources for @ApplicationModuleTest Mar 18, 2025
@KarasDominik
Copy link

Thanks for taking care of it. Sorry, but how do I access 1.4.0-M3 version? On Spring Milestones I see only 1.4.0-M2 and this version does not solve the issue

@odrotbohm
Copy link
Member Author

M3 is due this Friday. Meanwhile, please use 1.4.0-SNAPSHOT from https://repo.spring.io/snapshot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: test support Spring Boot integration testing type: enhancement Major enhanvements, new features
Projects
None yet
Development

No branches or pull requests

3 participants