1. Introduction

Most of the developers contacted in Java development do not pay much attention to the testing of the interface, resulting in various problems in the joint tuning docking. Some use tools such as Postman for testing, although there are no problems in use, if the interface added permissions to the test will be more disgusting. Therefore, it is recommended to test the interface in unit tests to ensure the robustness of the interface before delivery. Today, I will share how He tested the Spring MVC interface during his development.

Be sure to add Spring Boot Test related components before you start. The following dependencies should be included in the latest version:

<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>
Copy the code

This article was conducted under Spring Boot 2.3.4.RELEASE.

2. Test the control layer separately

If we only need to test the Controller interface, and the interface does not rely on Spring beans declared with @Service, @Component, etc., annotations, we can use @webMvcTest to enable web-only testing, for example

@WebMvcTest
class CustomSpringInjectApplicationTests {
    @Autowired
    MockMvc mockMvc;

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

This approach is much faster and only loads a small portion of the application. But if you get to the service layer this approach doesn’t work, we need another approach.

3. Holistic testing

Most Spring Boot interface tests are holistic and comprehensive, involving the control layer, service layer, persistence layer, etc., so you need to load a relatively complete Spring Boot context. We can declare an abstract test base class:

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;


/** * test base class, *@author felord.cn
 */
@SpringBootTest
@AutoConfigureMockMvc
abstract class CustomSpringInjectApplicationTests {
    /** * The Mock mvc. */
    @Autowired
    MockMvc mockMvc;
    // Other public dependencies and handling methods
}
Copy the code

MockMvc is injected into Spring IoC only if @AutoConfiguRemockMVC exists.

Then for the specific control layer for the following test code writing:

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.*;

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

4. MockMvc test

During the integration test, we hope to test the Controller by inputting the URL. If we test the Controller by starting the server and establishing the HTTP client, it will make the test very troublesome. For example, the startup speed is slow, the test verification is inconvenient, and the Controller depends on the network environment. MockMvc was introduced in order to test the Controller.

MockMvc implements the simulation of Http requests and can directly use the form of the network to convert to the call of the Controller, which makes the test fast and independent of the network environment. Moreover, it provides a set of validation tools, which makes the verification of requests unified and convenient. Let’s construct a test mock request step by step, assuming we have an interface that looks like this:

@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
        returnmap; }}Copy the code

If the parameter is set to name=felord.cn&age=18, the corresponding HTTP packet looks like this:

GET /foo/user? name=felord.cn&age=18 HTTP / 1.1
Host: localhost:8888
Api-Version: v1
Copy the code

The expected return value is:

{
    "test": "bar"."version": "v1"."username": "felord.cn"
}	
Copy the code

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

Build request

Build requests are handled by MockMvcRequestBuilders, who provide request Method, request Header, request Body, Parameters, Session, and all request attributes to build. /foo/user interface requests can be converted to:

MockMvcRequestBuilders.get("/foo/user")
                .param("name"."felord.cn")
                .param("age"."18")
                .header("Api-Version"."v1")
Copy the code

Executing Mock requests

MockMvc then executes the Mock request:

mockMvc.perform(MockMvcRequestBuilders.get("/foo/user")
                .param("name"."felord.cn")
                .param("age"."18")
                .header("Api-Version"."v1"))
Copy the code

The results are processed

The result of the request is wrapped in a ResultActions object, which encapsulates a variety of methods that let us process the result of the Mock request.

Anticipate the outcome

The ResultActions#andExpect(ResultMatcher matcher) method is responsible for the expected results of the response to see if they meet the expected results of the test. The parameter ResultMatcher is responsible for extracting the desired parts from the response object for expected comparison.

If we expect the interface /foo/user to return JSON, the HTTP status is 200, and the response body contains a value of version=v1, we should declare this:

   ResultMatcher.matchAll(MockMvcResultMatchers.status().isOk(),
                MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON),
                MockMvcResultMatchers.jsonPath("$.version", Is.is("v1")));
Copy the code

JsonPath is a powerful JSON parsing library available through its project repository github.com/json-path/J…

Process the response

The ResultActions#andDo(ResultHandler handler) method is responsible for printing or logging or streaming the entire request/response, provided by the MockMvcResultHandlers utility class. There are three ways to view the details of the request response.

For example, /foo/user:

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 = []
Copy the code

Get the returned result

If you want to further process the result of the response, you can also use ResultActions#andReturn() to get the result of type MvcResult for further processing.

Complete testing process

In general, andExpect is the inevitable choice, while andDo and andReturn are optional for certain scenarios. Let’s join the top one together.

@Autowired
MockMvc mockMvc;

@SneakyThrows
@Test
void contextLoads(a) {

     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());
            
}
Copy the code

This type of streaming interface unit testing is semantically understandable, and you can test your interface with assertions, positive and negative examples, ultimately making your interface more robust.

5. To summarize

Once you get used to it, you’ll write interfaces that are more authoritative and less buggy, and sometimes you can even Mock them to make them more business-friendly. So CRUD is not entirely technical, and high-quality, efficient CRUD often requires engineered unit testing. Ok, today’s share is over here, I am: small fat brother, a lot of attention, a lot of support.

Follow our public id: Felordcn for more information

Personal blog: https://felord.cn