24

I am testing a Spring Boot application. I have several test classes, each of which needs a different set of mocked or otherwise customized beans.

Here is a sketch of the setup:

src/main/java:

package com.example.myapp;

@SpringBootApplication
@ComponentScan(
        basePackageClasses = {
                MyApplication.class,
                ImportantConfigurationFromSomeLibrary.class,
                ImportantConfigurationFromAnotherLibrary.class})
@EnableFeignClients
@EnableHystrix
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

package com.example.myapp.feature1;

@Component
public class Component1 {
    @Autowired
    ServiceClient serviceClient;

    @Autowired
    SpringDataJpaRepository dbRepository;

    @Autowired
    ThingFromSomeLibrary importantThingIDontWantToExplicitlyConstructInTests;

    // methods I want to test...
}

src/test/java:

package com.example.myapp;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApplication.class)
@WebAppConfiguration
@ActiveProfiles("test")
public class Component1TestWithFakeCommunication {

    @Autowired
    Component1 component1; // <-- the thing we're testing. wants the above mock implementations of beans wired into it.

    @Autowired
    ServiceClient mockedServiceClient;

    @Configuration
    static class ContextConfiguration {
        @Bean
        @Primary
        public ServiceClient mockedServiceClient() {
            return mock(ServiceClient.class);
        }
    }

    @Before
    public void setup() {
        reset(mockedServiceClient);
    }

    @Test
    public void shouldBehaveACertainWay() {
        // customize mock, call component methods, assert results...
    }
}

package com.example.myapp;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApplication.class)
@WebAppConfiguration
@ActiveProfiles("test")
public class Component1TestWithRealCommunication {

    @Autowired
    Component1 component1; // <-- the thing we're testing. wants the real implementations in this test.

    @Autowired
    ServiceClient mockedServiceClient;

    @Before
    public void setup() {
        reset(mockedServiceClient);
    }

    @Test
    public void shouldBehaveACertainWay() {
        // call component methods, assert results...
    }
}

The problem with the above setup is that the component scan configured in MyApplication picks up Component1TestWithFakeCommunication.ContextConfiguration, so I get a mock ServiceClient even in Component1TestWithRealCommunication where I want the real ServiceClient implementation.

Although I could use @Autowired constructors and build up the components myself in both tests, there is a sufficient amount of stuff with complicated setup that I would rather have Spring TestContext set up for me (for example, Spring Data JPA repositories, components from libraries outside the app that pull beans from the Spring context, etc.). Nesting a Spring configuration inside the test that can locally override certain bean definitions within the Spring context feels like it should be a clean way to do this; the only downfall is that these nested configurations end up affecting all Spring TestContext tests that base their configuration on MyApplication (which component scans the app package).

How do I modify my setup so I still get a "mostly real" Spring context for my tests with just a few locally overridden beans in each test class?

Jonathan Fuerth
  • 2,080
  • 2
  • 18
  • 21

4 Answers4

14

The following should help you to achieve your goal by introducing a new fake-communication profile that is applicable only to the current test class.

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApplication.class)
@WebAppConfiguration
@ActiveProfiles({"test", "fake-communication"})
public class Component1TestWithFakeCommunication {

    // @Autowired ...

    @Profile("fake-communication")
    @Configuration
    static class ContextConfiguration {
        @Bean
        @Primary
        public ServiceClient mockedServiceClient() {
            return mock(ServiceClient.class);
        }
    }
}
Sam Brannen
  • 29,611
  • 5
  • 104
  • 136
  • Thanks. This is a nice, obvious way of restricting the configuration to the one test class that shouldn't cause any unexpected surprises in the rest of the test suite. – Jonathan Fuerth Sep 14 '16 at 10:59
  • 2
    This is a working solution but sort-of "work-around-ish" and also a little inconvenient. Spring boot provides a better way with its automatic exclude filter. An example attached in a different answer. – Marwin Nov 27 '17 at 10:43
  • Seems overly complicated. Have you tried just using `@MockBean` instead of mocking a bean yourself in a test configuration and autowiring it? `@MockBean` basically does both for you. – Frans Sep 23 '20 at 09:59
5

You may use additional explicit profiles to avoid such test configurations to be picked up (as suggested in another answer). I also did it and even created some library support for that.

However, Spring-Boot is clever and it has a built-in "type filter" to resolve this issue automatically. For this to work, you need to remove your @ComponentScan annotation, which would find your test configurations, and let the @SpringBootApplication do the work. In your example, just remove this:

@SpringBootApplication
@ComponentScan(
    basePackageClasses = {
            MyApplication.class,
            ImportantConfigurationFromSomeLibrary.class,
            ImportantConfigurationFromAnotherLibrary.class})

and replace it with:

@SpringBootApplication(scanBasePackageClasses= {
            MyApplication.class,
            ImportantConfigurationFromSomeLibrary.class,
            ImportantConfigurationFromAnotherLibrary.class})

You may also need to annotate your test as @SpringBootTest. This should avoid auto-scanning any inner-class configurations (and components) except for those residing in the current test.

Marwin
  • 2,655
  • 1
  • 19
  • 17
5

If you have a @SpringBootTest you can just annotate the service you want to mock with @MockBean. As simple as that.

Jorge Viana
  • 396
  • 5
  • 12
1

I would do a couple of things:

  1. Move your test classes into a different package to avoid @ComponentScaning them.
  2. In Component1TestWithFakeCommunication, change @SpringApplicationConfiguration(classes = MyApplication.class) to @SpringApplicationConfiguration(classes = {MyApplication.class, Component1TestWithFakeCommunication.ContextConfiguration.class})

That should give Spring sufficient information to mock up the test beans, but it should prevent the runtime ApplicationContext from noticing your test beans as well.

Sam Brannen
  • 29,611
  • 5
  • 104
  • 136
Josh Ghiloni
  • 1,260
  • 8
  • 19
  • 1
    Thank you for the suggestion. This would certainly resolve my issue, but our team has 14 (and growing) similarly structured codebases and this would be the only one (for now) that needed its tests moved to a different package. I envision that we'd keep tripping over that difference. Also, the fix might be too subtle for someone to easily replicate in one of the other projects with the standard test package structure. I prefer Sam Brannen's answer for those reasons. – Jonathan Fuerth Sep 14 '16 at 11:10
  • don't forget to remove `@Configuration` from `ContextConfiguration`. So moving to the different package is not necessary – Ilya Serbis Jan 19 '20 at 17:22