Friday 10 April 2015

[Spring] How to test RestController using MockMvc ?

If you work in a software company you probably write integration tests which deploy your product into application server and then test functionalities exposed by web services. Integration tests ran by continuous integration server are very useful. Especially when the whole team develops some kind of functionality. After each commit you can easily check whether everything works fine and continue the development. Unfortunately it takes time... Typical workflow may look like this one: 1. Developer commits the code. 2. The code is being built on CI server. 3. Test environment has to be upgraded (war deployment). 4. The tests. It strongly depends on how big your project is but I'm sure you will have to wait for a while. Perfect tests should work as fast as unit tests. You should be able to run whole test suite and see results after seconds. And here comes Spring. Each rest controller can be conveniently tested using MockMvc. In order to do that you need to add spring-test into your dependencies' section:
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-test</artifactId>
  <scope>test</scope>
</dependency>
Let's say I want to test following endpoint:
@RestController
public class WeatherEndpoint {
  @Inject
  private WeatherService weatherService;
  
  @RequestMapping(value = "/weather/{city}",
                    method = RequestMethod.GET,
                    produces = "application/json"
  @ResponseBody
  public Weather currentWeather(@PathVariable("city") String city) {
    return weatherService.getCurrentWeatherIn(city);                                    }
}
It's just an example so my WeatherService returns fixed value:
private class WeatherService {
  public Weather getCurrentWeatherIn(String city) {
    return new Weather("20.3");
  }
}

@Bean
public WeatherService weatherService() {
  return new WeatherService();
}
We need to prepare test skeleton which will be able to: 1. Run the test using spring runner. 2. Run application context. 3. Expose instance of MockMvc class.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AppConfig.class})
@WebAppConfiguration
public class WeatherEndpointTest {
    private MockMvc mockMvc;

    @Inject
    private WebApplicationContext context;

    @Before
    public void contextSetup() {
        this.mockMvc = mockMvc();
    }

    private MockMvc mockMvc() {
        return MockMvcBuilders.<StandaloneMockMvcBuilder>webAppContextSetup(context).build();
    }
}
Now I'm going to explain each fragment of this template. @RunWith(SpringJUnit4ClassRunner.class) - indicates that Spring runner will be used. Unfortunately JUnit test can use only one runner so forget about Mockito runner. @ContextConfiguration(classes = {AppConfig.class}) - the runner will setup spring context using configuration classes listed in this annotation. In this example it will use only AppConfig. private MockMvc mockMvc - instance of MockMvc. This object allows to perform operations on rest endpoints. @Inject private WebApplicationContext context - context has to be injected in order to build mockMvc contextSetup() and mockMvc() - before each test mockMvc will be rebuilt Basically that's all. You can now start writing tests. Note that you can inject beans which have been added to spring container. In next episode I will explain how to do that and how to mock some of them in order to make tests isolated. currentWeather() returns Weather object which is a POJO class that contains only one field - (String) temperature. I've added jackson as dependency so the instance of Weather is being mapped to json. Here's our test:
    @Test
    public void shouldReturnInfoAboutWeather() throws Exception {
        // given
        String city = "Wroclaw";

        // when // then
        mockMvc.perform(get("/weather/" + city))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("temperature").value("20.3"));
    }
If you run the test you will see something like: INFO: Mapped "{[/weather/{city}],methods=[GET],params=[],headers=[],consumes=[],produces=[application/json],custom=[]}" onto public gt.dev.mockmvc.WeatherEndpoint.currentWeather(java.lang.String) which means that the context has been run and /weather endpoint exposed. In this particular example MockMvc performs GET on the endpoint and checks the result. In this case I expect http status to be OK (200) and media type json. Additionally I've added:
  <dependency>
   <groupId>com.jayway.jsonpath</groupId>
   <artifactId>json-path</artifactId>
   <scope>test</scope>
  </dependency>
so I can test the result mapped to json. Here I check whether temperature is equal to 20.3 (fixed value). Actually json-path is a really cool tool. You can assert that your json response is valid using very readable methods. The whole test below:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AppConfig.class})
@WebAppConfiguration
public class WeatherEndpointTest {
    private MockMvc mockMvc;

    @Inject
    private WebApplicationContext context;

    @Before
    public void contextSetup() {
        this.mockMvc = mockMvc();
    }

    @Test
    public void shouldReturnInfoAboutWeather() throws Exception {
        // given
        String city = "Wroclaw";

        // when // then
        mockMvc.perform(get("/weather/" + city))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("temperature").value("20.3"));
    }

    private MockMvc mockMvc() {
        return MockMvcBuilders.<StandaloneMockMvcBuilder>webAppContextSetup(context).build();
    }
}
I strongly recommend to experiment with mockMvc. You should definitely check what can be passed to andExpect() method. That's all folks. Next time I'm going to show how to mock services in sping container in order to make integration tests isolated.

No comments:

Post a Comment