124

For my Spring-Boot app I provide a RestTemplate though a @Configuration file so I can add sensible defaults(ex Timeouts). For my integration tests I would like to mock the RestTemplate as I dont want to connect to external services - I know what responses to expect. I tried providing a different implementation in the integration-test package in the hope that the latter will override the real implementation , but checking the logs it`s the other way around : the real implementation overrides the test one.

How can I make sure the one from the TestConfig is the one used?

This is my config file :

@Configuration
public class RestTemplateProvider {

    private static final int DEFAULT_SERVICE_TIMEOUT = 5_000;

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate(buildClientConfigurationFactory());
    }

    private ClientHttpRequestFactory buildClientConfigurationFactory() {
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setReadTimeout(DEFAULT_SERVICE_TIMEOUT);
        factory.setConnectTimeout(DEFAULT_SERVICE_TIMEOUT);
        return factory;
    }
}

Integration test:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestConfiguration.class)
@WebAppConfiguration
@ActiveProfiles("it")
public abstract class IntegrationTest {}

TestConfiguration class:

@Configuration
@Import({Application.class, MockRestTemplateConfiguration.class})
public class TestConfiguration {}

And finally MockRestTemplateConfiguration

@Configuration
public class MockRestTemplateConfiguration {

    @Bean
    public RestTemplate restTemplate() {
        return Mockito.mock(RestTemplate.class)
    }
}
luboskrnac
  • 23,973
  • 10
  • 81
  • 92
mvlupan
  • 3,536
  • 3
  • 22
  • 35
  • 1
    Switch the order of the imports, they are parsed in the way they are read, so the later ones override the previous ones. – M. Deinum Mar 02 '16 at 09:23
  • Tried it ...same thing. I`ll update my question to reflect the changes – mvlupan Mar 02 '16 at 09:27
  • Possible duplicate of [Overriding an Autowired Bean in Unit Tests](https://stackoverflow.com/questions/28605833/overriding-an-autowired-bean-in-unit-tests) – LoganMzz Aug 31 '17 at 15:19

11 Answers11

57

Since Spring Boot 1.4.x there is an option to use @MockBean annotation to fake Spring beans.

Reaction on comment:

To keep context in cache do not use @DirtiesContext, but use @ContextConfiguration(name = "contextWithFakeBean") and it will create separate context, while it will keep default context in cache. Spring will keep both (or how many contexts you have) in cache.

Our build is this way, where most of the tests are using default non-poluted config, but we have 4-5 tests that are faking beans. Default context is nicely reused

luboskrnac
  • 23,973
  • 10
  • 81
  • 92
  • 1
    agree that this is the way to go in most cases. The down-side that I found with this approach is that you will lose the benefit of Context caching in your next test class. To make our integrations tests as fast as possible we extend one class whenever possible. – mvlupan Jan 04 '17 at 11:43
  • 1
    true, but sometimes there's no other way. E.g. when you need to fake external services. – luboskrnac Jan 04 '17 at 14:49
  • The annotation is @MockBean, see https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html#boot-features-testing-spring-boot-applications-mocking-beans – notclive May 17 '17 at 22:56
  • This is a good solution, but my problem cannot be solved this way because I am doing AOP security-related testing. – EliuX Dec 27 '17 at 01:13
  • If you use AOP proxies, I recommend looking into my other answer, where I linked Github repository. That repository contains example how to mock AOP proxied Spring bean. – luboskrnac Dec 27 '17 at 09:13
  • This solution is hard to apply, when the mocked bean is passed as a parameter, and you don't know exactly, what methods will be called. For example, if you need to mock java.time.Clock to Clock.fixed() instance – WeGa Oct 11 '21 at 06:42
  • @WeGa, don't understand your comment. I suggest to create separate question with exact example and somebody would answer directly your example. I am sure it is possible with `MockedBean` annotation. – luboskrnac Oct 11 '21 at 11:37
  • @luboskrnac, actually, it's a statement, not a question. With MockBean annotation you replace bean with mock object which behaviour is undefined unless you explicitly provide it with `Mockito.when(mockBean.doSmth).then_`. In my example above, Clock is an object with rich api, in test, I want for it to be fixed moment of time, and it should behave like that with every method it has. With MockBean it wouild be pain in the neck to implement – WeGa Oct 13 '21 at 13:07
56

1. You can use @Primary annotation:

@Configuration
public class MockRestTemplateConfiguration {

    @Bean
    @Primary
    public RestTemplate restTemplate() {
        return Mockito.mock(RestTemplate.class)
    }
}

BTW, I wrote blog post about faking Spring bean

2. But I would suggest to take a look at Spring RestTemplate testing support. This would be simple example:

  private MockRestServiceServer mockServer;

  @Autowired
  private RestTemplate restTemplate;

  @Autowired
  private UsersClient usersClient;

  @BeforeMethod
  public void init() {
    mockServer = MockRestServiceServer.createServer(restTemplate);
  }

  @Test
  public void testSingleGet() throws Exception {
    // GIVEN
    int testingIdentifier = 0;
    mockServer.expect(requestTo(USERS_URL + "/" + testingIdentifier))
      .andExpect(method(HttpMethod.GET))
      .andRespond(withSuccess(TEST_RECORD0, MediaType.APPLICATION_JSON));


    // WHEN
    User user = usersClient.getUser(testingIdentifier);

    // THEN
    mockServer.verify();
    assertEquals(user.getName(), USER0_NAME);
    assertEquals(user.getEmail(), USER0_EMAIL);
  }

More examples can be found in my Github repo here

markdsievers
  • 7,151
  • 11
  • 51
  • 83
luboskrnac
  • 23,973
  • 10
  • 81
  • 92
  • 2
    I tried the second suggestion and it works great. Thanks for the hint..I feel this is the "correct" way to go. – mvlupan Mar 02 '16 at 13:04
  • 2
    Thanks. Option 1 worked for me with a simple `@Primary` annotation on my TestMock Bean which took precedent over the Production one for `@Autowire` of fields in my Test classes. Apart from this `@Primary` annotation, no other change was required. – DaddyMoe Jan 02 '19 at 17:44
  • This is the way. When using `@Primary`, make sure to use `@Configuration` and not `@TestConfiguration`. Also `@Primary` seems to clash with `@LocalServerPort` in the same configuration class. – Stijn Van Bael Jan 26 '21 at 15:00
  • 1
    Note that when using `@Primary`, this doesn't prevent both beans from being created. That said, `@Primary` is not suitable when there is some `@PostConstruct` initialization code you don't want to execute. – Rens Verhage Apr 30 '21 at 16:02
  • 3
    This solution does not work when production code contains multiple such beans and one of them is already marked as `@Primary` (while others use @Qualifier). This solution essentially abuses the disambiguation mechanism for something it isn't intended for. – Markus Sep 27 '21 at 12:54
  • Marcus, This answer was workaround before Spring Boot introduced @MockedBean annotation. So this answer is outdated and you should follow accepted one. – luboskrnac Oct 05 '21 at 07:51
  • Worked for me with static inner class and `@TestConfiguration` – RoBeaToZ Mar 08 '22 at 10:40
40

The Problem in your configuration is that you are using @Configuration for your test configuration. This will replace your main configuration. Instead use @TestConfiguration which will append (override) your main configuration.

46.3.2 Detecting Test Configuration

If you want to customize the primary configuration, you can use a nested @TestConfiguration class. Unlike a nested @Configuration class, which would be used instead of your application’s primary configuration, a nested @TestConfiguration class is used in addition to your application’s primary configuration.

Example using SpringBoot:

Main class

@SpringBootApplication() // Will scan for @Components and @Configs in package tree
public class Main{
}

Main config

@Configuration
public void AppConfig() { 
    // Define any beans
}

Test config

@TestConfiguration
public void AppTestConfig(){
    // override beans for testing
} 

Test class

@RunWith(SpringRunner.class)
@Import(AppTestConfig.class)
@SpringBootTest
public void AppTest() {
    // use @MockBean if you like
}

Note: Be aware, that all Beans will be created, even those that you override. Use @Profile if you wish not to instantiate a @Configuration.

Torsten
  • 919
  • 9
  • 8
  • Disclaimer: this annotation is not supported in earlier Spring versions, it is available since Spring Boot 1.4.0 – eugen-fried Jan 08 '20 at 13:28
  • 13
    So what does "override" mean if even overridden beans will be created? – Ilya Serbis Oct 02 '20 at 00:40
  • I think `AppConfig`and `AppTestConfig` should be classes, not methods. E.g. ```@Configuration public class AppConfig { // Define any beans }``` – Tim Büthe Mar 31 '22 at 19:05
  • 3
    It works nicely, but if you are overriding beans, since Spring Boot 2.1 the bean overriding feature is disabled by default, and you need to use the property: "spring.main.allow-bean-definition-overriding=true" – coderazzi Apr 08 '22 at 09:04
24

With @Primary annotation, Bean overriding works with Spring Boot 1.5.X but fails with Spring Boot 2.1.X it throw error:

Invalid bean definition with name 'testBean' defined in sample..ConfigTest$SpringConfig:.. 
There is already .. defined in class path resource [TestConfig.class]] bound

Please add below properties= which will instruct Spring explicitly to allow overriding, it is self explainatory.

@SpringBootTest(properties = ["spring.main.allow-bean-definition-overriding=true"])

UPDATE: You can add the same property in application-test.yml (file name depend upon what test profile name you are tests with)

ynerdy
  • 564
  • 4
  • 7
23

@MockBean and bean overriding used by the OP are two complementary approaches.

You want to use @MockBean to create a mock and forget the real implementation : generally you do that for slice testing or integration testing that doesn't load some beans which class(es) you are testing depend on and that you don't want to test these beans in integration.
Spring makes them by default null, you will mock the minimal behavior for them to fulfill your test.

@WebMvcTest requires very often that strategy as you don't want to test the whole layers and @SpringBootTest may also require that if you specify only a subset of your beans configuration in the test configuration.

On the other hand, sometimes you want to perform an integration test with as many real components as possible, so you don't want to use @MockBean but you want to override slightly a behavior, a dependency or define a new scope for a bean, in this case, the approach to follow is bean overriding :

@SpringBootTest({"spring.main.allow-bean-definition-overriding=true"})
@Import(FooTest.OverrideBean.class)
public class FooTest{    

    @Test
    public void getFoo() throws Exception {
        // ...     
    }

    @TestConfiguration
    public static class OverrideBean {    

        // change the bean scope to SINGLETON
        @Bean
        @Scope(ConfigurableBeanFactory.SINGLETON)
        public Bar bar() {
             return new Bar();
        }

        // use a stub for a bean 
        @Bean
        public FooBar BarFoo() {
             return new BarFooStub();
        }

        // use a stub for the dependency of a bean 
        @Bean
        public FooBar fooBar() {
             return new FooBar(new StubDependency());
        }

    }
}
davidxxx
  • 125,838
  • 23
  • 214
  • 215
20

Getting a little deeper into it, see my second answer.

I solved the Problem using

@SpringBootTest(classes = {AppConfiguration.class, AppTestConfiguration.class})

instead of

@Import({ AppConfiguration.class, AppTestConfiguration.class });

In my case the Test is not in the same package as the App. So I need to specify the AppConfiguration.class (or the App.class) explicit. If you use the same package in the test, than I guess you could just write

@SpringBootTest(classes = AppTestConfiguration.class)

instead of (not working)

@Import(AppTestConfiguration.class );

It is pretty wired to see that this is so different. Maybe some one can explain this. I could not find any good answers until now. You might think, @Import(...) is not picked up if @SpringBootTestsis present, but in the log the overriding bean shows up. But just the wrong way around.

By the way, using @TestConfiguration instead @Configuration also makes no difference.

Marty Pitt
  • 28,822
  • 36
  • 122
  • 195
Torsten
  • 919
  • 9
  • 8
5

I´ve declared an inner configuration class within my test because I wanted to overwrite just a single method

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class FileNotificationWebhookTest{

    public static class FileNotificationWebhookTestConfiguration {
        @Bean
        @Primary
        public FileJobRequestConverter fileJobRequestConverter() {
            return new FileJobRequestConverter() {
                @Override
                protected File resolveWindowsPath(String path) {
                    return new File(path);
                }
            };
        }
    }
}

However,

Declaring the configuration in @SpringBootTest did not work:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,classes = {FileNotificationWebhookTest.FileNotificationWebhookTestConfiguration.class})

or annotating the test configuration with @Configuration did not work:

@Configuration
public static class FileNotificationWebhookTestConfiguration {

}

and was leading to

Caused by: org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean.

What did work for me ( contrary to some other posts here) was using @Import

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Import(FileNotificationWebhookTest.FileNotificationWebhookTestConfiguration.class)
class FileNotificationWebhookTest {

}

Using Spring: 5.3.3 with Spring-Boot-Starter: 2.4.2

Marian Klühspies
  • 15,824
  • 16
  • 93
  • 136
4

This is super weird.

In my case, (Spring Boot 2.6.7), I could simply @Import MyTestConfiguration containing a custom @Primary @Bean into my @SpringBootTest, and everything worked.

Right until I needed to explicitly name my bean. Then I suddenly had to resort to

@SpringBootTest(
    properties = ["spring.main.allow-bean-definition-overriding=true"],
    classes = [MyTestConfig::class],
)
Joachim Lous
  • 1,316
  • 1
  • 14
  • 22
3

@MockBean creates Mockito mock instead of production build.

If you do not want to use Mockito, but provide a replacement in some other way (i.e. by disabling some features of bean with feature toggles), I suggest using combination of @TestConfiguration (since Spring Boot 1.4.0) and @Primary annotation.

@TestConfiguration will load your default context and apply your @TestConfiguration piece in addition to it. Adding @Primary will force your mocked RestTemplate to be injected to it's dependents.

See simplified example below:

@SpringBootTest
public class ServiceTest {

    @TestConfiguration
    static class AdditionalCfg {
        @Primary
        @Bean
        RestTemplate rt() {
            return new RestTemplate() {
                @Override
                public String exec() {
                    return "Test rest template";
                }
            };
        }
    }

    @Autowired
    MyService myService;

    @Test
    void contextLoads() {
       assertThat(myService.invoke()).isEqualTo("Test rest template");
    }
}

Kirill
  • 6,762
  • 4
  • 51
  • 81
1

The simplest solution I found was to set this property in (test) application.properties:

spring.main.allow-bean-definition-overriding=true

This will enable overriding of beans.

Next, create a configuration class in test, and annotate your bean with:

@Bean
@Primary

This way, this bean will override your usual bean when running tests.

0

Check this answer along with others provided in that thread. It's about overriding bean in Spring Boot 2.X, where this option was disabled by default. It also has some ideas about how to use Bean Definition DSL if you decided to take that path.

yuranos
  • 8,799
  • 9
  • 56
  • 65