Search code examples
javaspring-bootmockitointegration-testing

Understanding @MockBean usage With 'RestTemplate restTemplate'


The behavior that I am seeing is a NullPointerException in the second test when the mocked restTemplate called. That pointed to a problem in the resetting of the mock. What surprised me is the fix ( that made both tests pass).

Modifying the code from @MockBean private RestTemplate restTemplate; to @MockBean(reset = MockReset.NONE) private RestTemplate restTemplate; fixed the issue. A few questions here:

  1. Why didn't the default @MockBean behavior of MockReset.RESET work?
  2. Is there something wrong with how I have set up my test such that default MockReset.RESET was failing?
  3. Is there something wrong with the test config class?

Hopefully, I've provided enough context to answer the question.

I've created a simplified example of what I'm seeing: Test configuration:

@Profile("test")
@Configuration
public class TestConfiguration {

    @Bean
    @Primary
    public ObjectNode getWeatherService(RestTemplate restTemplate) {
        return new WeatherServiceImpl(restTemplate);
    }

}

The test:

@SpringBootTest
@ActiveProfiles("test")
@AutoConfigureMockMvc
class SamTest {
    @Autowired private MockMvc mockMvc;
    @MockBean private RestTemplate restTemplate;
    /*
    Works:
    @MockBean(reset = MockReset.NONE) private RestTemplate restTemplate;
    Fails:
    @MockBean(reset = MockReset.BEFORE) private RestTemplate restTemplate;
    @MockBean(reset = MockReset.AFTER) private RestTemplate restTemplate;
     */
    @Test
    public void testOne() throws Exception {
        Mockito.when(restTemplate.getForEntity("http://some.weather.api", ObjectNode.class))
                .thenReturn(new ResponseEntity("{\"weather\" : \"rainy\"}", HttpStatus.OK));

        // Makes call to standard @RestController with a @GetMapping
        // Call to external API is contained in @Service class.
        // Currently controller just passes through the json from the underlying service call.
        this.mockMvc.perform(
                get("/weather/check").
                        contentType(MediaType.APPLICATION_JSON_VALUE)).
                andExpect(status().isOk());
    }

    @Test
    public void testTwo() throws Exception {
        Mockito.when(restTemplate.getForEntity("http://some.weather.api", ObjectNode.class))
                .thenReturn(new ResponseEntity("{\"error\" : \"bandwidth\"}", HttpStatus.BANDWIDTH_LIMIT_EXCEEDED));

        this.mockMvc.perform(
                get("/weather/check").
                        contentType(MediaType.APPLICATION_JSON_VALUE)).
                andExpect(status().is5xxServerError());
    }
}

The service:

@Service
public class WeatherServiceImpl implements WeatherService {
    private final RestTemplate restTemplate;

    @Autowired
    public WeatherServiceImpl(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    public ObjectNode retrieve(URI uri) {
        ResponseEntity<ObjectNode> response = restTemplate.getForEntity(uri, ObjectNode.class);
        return response.getBody();
    }

}

Solution

  • There is a misunderstanding about the @MockBean default behaviour:

    Why didn't the default @MockBean behavior of MockReset.RESET work? Is there something wrong with how I have set up my test such that default MockReset.RESET was failing?

    From the MockBean.reset method documentation:

    The reset mode to apply to the mock bean. The default is MockReset.AFTER meaning that mocks are automatically reset after each test method is invoked.

    So your MockBean will be reset and unregistered from the application context after your first testcase execution and then your second testcase will find it null, while it will not happen in case of @MockBean(reset = MockReset.NONE) as you have done.