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 within com.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
and com.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 in GlobalConfiguration.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 complete src/main/java
application configuration via GlobalConfiguration
, exclude dangerous components via @ActiveProfiles("test")
and replace those excluded components with its own implementations. However, during tests the namespace of src/main/java
and src/test/java
are combined, so GlobalConfiguration
's @ComponentScan
finds more in the classpath than it normally would -- namely, the ServiceA
bean defined in ServiceA.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 used FilterType.ANNOTATION
, you'd effectively be making your src/main/java
aware of your src/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, on ServiceATest.ServiceATestConfiguration
I add the annotation @Profile("serviceatest")
. This effectively limits the scope of ServiceATestConfiguration
with relatively little overhead, but it seems like either:
a) I am using @ComponentScan
incorrectly, or
b) 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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With