Excluding configuration in test classes from @ComponentScan
I see you are trying to fake Spring beans during integration test. If you combine @Profile
and @ActiveProfiles
annotation with @Primary
annotation, most of your headaches should go away and you shouldn't need to mark production beans with @Profile("!test")
.
I wrote a blog post on the topic with Github examples.
Reaction on comment:
By package structure. Component scan scans all packages within current package and sub-packages. IF you don't want to scan beans, just amend your package structure the way that bean wouldn't be under your component scan umbrella.
Spring doesn't differentiate packages from src/test/java
or src/main/java
. Trying to exclude production beans with @Profile("!test")
is design smell. You should avoid it. I would suggest to give a chance to approach from mentioned blog.
Notice that when you override the bean with @Primary annotation, you may need to use @DirtiesContext
annotation to have clean sheet for other tests.
jwilner
Updated on June 14, 2022Comments
-
jwilner almost 2 years
I've been running into
@ComponentScan
issues with@Configuration
classes for tests -- namely, the@ComponentScan
is pulling in unintended@Configuration
during integration tests.For example, say you've got some global config in
src/main/java
which pulls in components withincom.example.service
,com.example.config.GlobalConfiguration
:package com.example.config; ... @Configuration @ComponentScan(basePackageClasses = ServiceA.class) public class GlobalConfiguration { ... }
It's intended to pull in two services,
com.example.services.ServiceA
andcom.example.services.ServiceB
, annotated with@Component
and@Profile("!test")
(omitted for brevity).Then in src/test/java,
com.example.services.ServiceATest
:package com.example.services; ... @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = ServiceATest.ServiceATestConfiguration.class) public class ServiceATest { ... @Configuration public static class ServiceATestConfiguration { @Bean public ServiceA serviceA() { return ServiceA(somemocking...); } } }
And also
com.example.ServiceBIntegrationTest
, which needs to pull inGlobalConfiguration.class
in order to be an integration test, but still avoids pulling in dangerous implementations with@ActiveProfiles("test")
:package com.example.services; ... @RunWith(SpringJUnit4ClassRunner.class) @ActiveProfiles("test") @ContextConfiguration(classes = {GlobalConfiguration.class, ServiceBIntegrationTest.ServiceBIntegrationTestConfiguration.class}) public class ServiceBIntegrationTest { ... @Configuration public static class ServiceBIntegrationTestConfiguration { @Bean public ServiceB serviceB() { return ServiceB(somemocking...); } } }
The obvious intention of the
ServiceBIntegrationTest
is to pull in the completesrc/main/java
application configuration viaGlobalConfiguration
, exclude dangerous components via@ActiveProfiles("test")
and replace those excluded components with its own implementations. However, during tests the namespace ofsrc/main/java
andsrc/test/java
are combined, soGlobalConfiguration
's@ComponentScan
finds more in the classpath than it normally would -- namely, theServiceA
bean defined inServiceA.ServiceATestConfiguration
. That could easily lead to conflicts and unintended results.Now, you could do something on
GlobalConfiguration
like@ComponentScan(..., excludeFilters= @ComponentScan.Filter(type = FilterType.REGEX, pattern = "\\.*(T|t)est\\.*"))
, but that has issues of its own. Relying on naming conventions is pretty brittle; still, even if you backed out a@TestConfiguration
annotation and usedFilterType.ANNOTATION
, you'd effectively be making yoursrc/main/java
aware of yoursrc/test/java
, which it shouldn't be, IMO (see note below).As it stands, I've solved my problem by using an additional profile. On
ServiceA
, I add a unique profile name -- so that its profile annotation becomes something like@ActiveProfiles("test,serviceatest")
. Then, onServiceATest.ServiceATestConfiguration
I add the annotation@Profile("serviceatest")
. This effectively limits the scope ofServiceATestConfiguration
with relatively little overhead, but it seems like either:a) I am using
@ComponentScan
incorrectly, orb) There should be a much cleaner pattern for handling this problem
Which is it?
note: yes, the app is test-aware because it's using
@Profile("!test")
, but I'd argue making the application slightly test-aware to defend against improper resource usage and making it test-aware to ensure correctness of tests are very different things. -
jwilner about 8 yearsYeah, there are some advantages to that pattern, but that's not what my question is about. I'm asking how to best limit the scope of configuration within a component-scanned package.
-
jwilner about 8 yearsChanging the packaging would be in conflict with package local scoping, which is obviously important for testing. This is unsatisfactory.