23

Situation and Problem: In Spring Boot, how can I inject one or more mocked classes/beans into the application to do an integration test? There are a few answers on StackOverflow, but they are focused on the situation before Spring Boot 1.4 or are just not working for me.

The background is, that in the code bellow the implementation of Settings relies on third party servers and other external systems. The functionality of Settings is already tested in a unit test, so for a full integration test I want to mock out the dependency to these servers or system and just provide dummy values.

MockBean will ignore all existing bean definitions and provide a dummy object, but this object doesn't provide a method behavior in other classes that inject this class. Using the @Before way to set the behavior before a test doesn't influence the injected object or isn't injected in other application services like AuthenticationService.

My question: How can I inject my beans into the application context? My test:

package ch.swaechter.testapp;

import ch.swaechter.testapp.utils.settings.Settings;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.junit4.SpringRunner;

@TestConfiguration
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {MyApplication.class})
public class MyApplicationTests {

    @MockBean
    private Settings settings;

    @Before
    public void before() {
        Mockito.when(settings.getApplicationSecret()).thenReturn("Application Secret");
    }

    @Test
    public void contextLoads() {
        String applicationsecret = settings.getApplicationSecret();
        System.out.println("Application secret: " + applicationsecret);
    }
}

And bellow a service that should use the mocked class, but doesn't receive this mocked class:

package ch.swaechter.testapp;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AuthenticationServiceImpl implements AuthenticationService {

    private final Settings settings;

    @Autowired
    public AuthenticationServiceImpl(Settings settings) {
        this.settings = settings;
    }

    @Override
    public boolean loginUser(String token) {
        // Use the application secret to check the token signature
        // But here settings.getApplicationSecret() will return null (Instead of Application Secret as specified in the mock)!
        return false;
    }
}
c-x-berger
  • 991
  • 12
  • 30
swaechter
  • 1,357
  • 3
  • 22
  • 46
  • 1
    Move what ever you had into method of `@Before` as spring test wont call `@Bean`, if you need your bean to be called then annotate class with `@testconfiguration` and create mock services – rajadilipkolli Mar 07 '17 at 16:42
  • Thank you for the answer! This will work for the settings object in the contextLoads test (Value is "Application Secret"). But for all other components in the application that autowire Settings, a default mock object without a method definition is used. Any ideas to also inject the mocked object there? – swaechter Mar 07 '17 at 17:46

2 Answers2

36

Looks like you are using Settings object before you specify its mocked behavior. You have to run

Mockito.when(settings.getApplicationSecret()).thenReturn("Application Secret");

during configuration setup. For preventing that you can create special configuration class for test only.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {MyApplication.class, MyApplicationTest.TestConfig.class})
public class MyApplicationTest {

    private static final String SECRET = "Application Secret";

    @TestConfiguration
    public static class TestConfig {
        @Bean
        @Primary
        public Settings settingsBean(){
            Settings settings = Mockito.mock(Settings.class);
            Mockito.when(settings.getApplicationSecret()).thenReturn(SECRET);
            Mockito.doReturn(SECRET).when(settings).getApplicationSecret();
            return settings;
        }

    }

.....

}  

Also I would recommend you to use next notation for mocking:

Mockito.doReturn(SECRET).when(settings).getApplicationSecret();

It will not run settings::getApplicationSecret

Serg
  • 938
  • 8
  • 14
7

When you annotate a field with @MockBean, spring will create a mock of the annotated class and use it to autowire all beans of the application context.

You must not create the mock yourself with

 Settings settings = Mockito.mock(Settings.class);

this would create a second mock, leading to the described problem.

Solution :

@MockBean
private Settings settings;

@Before
public void before() {
Mockito.when(settings.getApplicationSecret()).thenReturn("Application Secret");
}

@Test
public void contextLoads() {
    String applicationsecret = settings.getApplicationSecret();
    System.out.println("Application secret: " + applicationsecret);
}
  • Sadly that doesn't solve my problem, so I just updated my question to reflect my problem in a better way. – swaechter Mar 12 '17 at 18:23