1. Preface

Most of the developers contacted in Java development do not pay much attention to testing the interface, resulting in various problems in the docking. Some also use tools such as Postman for testing, although there is no problem in the use of the interface, if the interface increases the permissions testing is more disgusting. So it is recommended to test the interface in unit testing to ensure the robustness of the interface before delivery first self-test. Today we share how Spring MVC interfaces are tested in development.

Before you start make sure you add Spring Boot Test related components, which in the latest version should contain the following dependencies.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>

This article was done under Spring Boot 2.3.4.RELEASE.

2. Testing the control layer alone

If we need to test only the control layer interface (Controller) and that interface does not depend on Spring beans declared with annotations like @Service, @Component, etc., you can enable testing only for the web control layer with the help of @WebMvcTest, e.g.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@WebMvcTest
class CustomSpringInjectApplicationTests {
    @Autowired
    MockMvc mockMvc;

    @SneakyThrows
    @Test
    void contextLoads() {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/map"))
                .andExpect(ResultMatcher.matchAll(status().isOk(),
                        content().contentType(MediaType.APPLICATION_JSON),
                        jsonPath("$.test", Is.is("hello"))))
                .andDo(MockMvcResultHandlers.print());
    }

}

This way is much faster and it loads only a small part of the application. But if you are involved in the service layer this way is not working and we need another way.

3. holistic testing

Most interface tests under Spring Boot are holistic and comprehensive tests that involve all aspects of the control layer, service layer, persistence layer, etc., so you need to load a more complete Spring Boot context. We can do this by declaring an abstract test base class at this point.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package cn.felord.custom;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;


/**
 * 测试基类,
 * @author felord.cn
 */
@SpringBootTest
@AutoConfigureMockMvc
abstract class CustomSpringInjectApplicationTests {
    /**
     * The Mock mvc.
     */
    @Autowired
    MockMvc mockMvc;
    // 其它公共依赖和处理方法 
}

MockMvc will be injected into Spring IoC only if @AutoConfigureMockMvc is present.

Then the following test code is written for the specific control layer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package cn.felord.custom;

import lombok.SneakyThrows;
import org.hamcrest.core.Is;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * 测试FooController.
 *
 * @author felord.cn
 */
public class FooTests extends CustomSpringInjectApplicationTests {
    /**
     * /foo/map接口测试.
     */
    @SneakyThrows
    @Test
    void contextLoads() {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/map"))
                .andExpect(ResultMatcher.matchAll(status().isOk(),
                        content().contentType(MediaType.APPLICATION_JSON),
                        jsonPath("$.test", Is.is("bar"))))
                .andDo(MockMvcResultHandlers.print());
    }
}

4. MockMvc Testing

When integration testing, you want to be able to test the Controller by entering the URL, if you start the server and set up the http client to test, this will make the test very troublesome, for example, slow start, test verification is not convenient, dependent on the network environment, etc., so in order to be able to test the Controller, so the introduction of MockMvc.

MockMvc implements the simulation of Http requests, which can directly use the form of the network, converted to Controller calls, which can make the test fast and does not depend on the network environment, and provides a set of verification tools, which can make the request verification uniform and very convenient. Next, let’s construct a test mock request step by step, assuming we have an interface like the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/foo")
public class FooController {
    @Autowired
    private MyBean myBean;

    @GetMapping("/user")
    public Map<String, String> bar(@RequestHeader("Api-Version") String apiVersion, User user) {
        Map<String, String> map = new HashMap<>();
        map.put("test", myBean.bar());
        map.put("version", apiVersion);
        map.put("username", user.getName());
        //todo your business
        return map;
    }
}

The parameter is set to name=felord.cn&age=18, then the corresponding HTTP message will look like this.

1
2
3
GET /foo/user?name=felord.cn&age=18 HTTP/1.1
Host: localhost:8888
Api-Version: v1

The predictable return values are:

1
2
3
4
5
{
    "test": "bar",
    "version": "v1",
    "username": "felord.cn"
}    

In fact the testing of the interface can be divided into the following steps.

Building the request

Building requests is handled by MockMvcRequestBuilders, which provides all the request properties such as Method, Header, Body, Parameters and Session. The request for the /foo/user interface can be converted into.

1
2
3
4
MockMvcRequestBuilders.get("/foo/user")
                .param("name", "felord.cn")
                .param("age", "18")
                .header("Api-Version", "v1")

Execute the Mock request

The Mock request is then executed by MockMvc.

1
2
3
4
mockMvc.perform(MockMvcRequestBuilders.get("/foo/user")
                .param("name", "felord.cn")
                .param("age", "18")
                .header("Api-Version", "v1"))

Processing the results

The request results are encapsulated in the ResultActions object, which encapsulates a variety of methods that allow us to process the results of Mock requests.

Expectation of the result

The ResultActions#andExpect(ResultMatcher matcher) method is responsible for expecting the result of the response to see if it meets the expectations of the test. The parameter ResultMatcher is responsible for extracting the parts of the response object that we need to expect to be compared.

If we expect the interface /foo/user to return JSON with an HTTP status of 200 and a response body containing the value version=v1, we should declare this.

1
2
3
ResultMatcher.matchAll(MockMvcResultMatchers.status().isOk(),
                MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON),
                MockMvcResultMatchers.jsonPath("$.version", Is.is("v1")));

JsonPath is a powerful JSON parsing class library, learn about it via its project repository at https://github.com/json-path/JsonPath.

Processing of responses

The ResultActions#andDo(ResultHandler handler) method is responsible for printing or logging output, streaming output for the entire request/response, and is provided by the MockMvcResultHandlers tool class. We can see the details of the request response in all three ways.

For example the /foo/user interface.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /foo/user
       Parameters = {name=[felord.cn], age=[18]}
          Headers = [Api-Version:"v1"]
             Body = null
    Session Attrs = {}

Handler:
             Type = cn.felord.xbean.config.FooController
           Method = cn.felord.xbean.config.FooController#urlEncode(String, Params)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"test":"bar","version":"v1","username":"felord.cn"}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

Get the return result

If you wish to further process the results of the response, you can also get the results of type MvcResult for further processing via ResultActions#andReturn().

The complete testing process

Usually andExpect is the one we will necessarily choose, while andDo and andReturn will be useful in some scenarios, and both of them are optional. Let’s concatenate the above.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Autowired
MockMvc mockMvc;

@SneakyThrows
@Test
void contextLoads() {

     mockMvc.perform(MockMvcRequestBuilders.get("/foo/user")
            .param("name", "felord.cn")
            .param("age", "18")
            .header("Api-Version", "v1"))
            .andExpect(ResultMatcher.matchAll(status().isOk(),
                    content().contentType(MediaType.APPLICATION_JSON),
                    jsonPath("$.version", Is.is("v1"))))
            .andDo(MockMvcResultHandlers.print());

}

This kind of streaming interface unit testing is also semantically better understood. You can use various assertions, positive and negative examples to test your interface and ultimately make your interface more robust.

5. Summary

Once you become proficient in this approach, you will write interfaces that are more authoritative and less flawed, and you can even sometimes use Mock to design interfaces that are more relevant to your business. So CRUD isn’t completely untechnical. High-quality and efficient CRUD often requires such engineered unit tests to support it.

Reference https://felord.cn/mockmvc.html