Excluding configuration in test classes from @ComponentScan

10,251

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.

Share:
10,251
jwilner
Author by

jwilner

Updated on June 14, 2022

Comments

  • jwilner
    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 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.

  • jwilner
    jwilner about 8 years
    Yeah, 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
    jwilner about 8 years
    Changing the packaging would be in conflict with package local scoping, which is obviously important for testing. This is unsatisfactory.