Mock測試你的Spring MVC接口

1. 前言

在Java開發中接觸的開發者大多數不太注重對接口的測試,結果在聯調對接中出現各類問題。也有的使用Postman等工具進行測試,雖然在使用上沒有什麼問題,若是接口增長了權限測試起來就比較噁心了。因此建議在單元測試中測試接口,保證在交付前先自測接口的健壯性。今天就來分享一下胖哥在開發中是如何對Spring MVC接口進行測試的。java

在開始前請務必確認添加了Spring Boot Test相關的組件,在最新的版本中應該包含如下依賴:git

<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>

本文是在Spring Boot 2.3.4.RELEASE下進行的。github

2. 單獨測試控制層

若是咱們只須要對控制層接口(Controller)進行測試,且該接口不依賴@Service@Component等註解聲明的Spring Bean時,能夠藉助@WebMvcTest來啓用只針對Web控制層的測試,例如web

@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());
    }

}

這種方式要快的多,它只加載了應用程序的一小部分。可是若是你涉及到服務層這種方式是不湊效的,咱們就須要另外一種方式了。spring

3. 總體測試

大多數Spring Boot下的接口測試是總體而又全面的測試,涉及到控制層、服務層、持久層等方方面面,因此須要加載比較完整的Spring Boot上下文。這時咱們能夠這樣作,聲明一個抽象的測試基類:json

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;
    // 其它公共依賴和處理方法 
}

只有當@AutoConfigureMockMvc存在時MockMvc纔會被注入Spring IoC。api

而後針對具體的控制層進行以下測試代碼的編寫:服務器

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測試

集成測試時,但願可以經過輸入URL對Controller進行測試,若是經過啓動服務器,創建http client進行測試,這樣會使得測試變得很麻煩,好比,啓動速度慢,測試驗證不方便,依賴網絡環境等,爲了能夠對Controller進行測試就引入了MockMvc網絡

MockMvc實現了對Http請求的模擬,可以直接使用網絡的形式,轉換到Controller的調用,這樣可使得測試速度快、不依賴網絡環境,並且提供了一套驗證的工具,這樣可使得請求的驗證統一併且很方便。接下來咱們來一步步構造一個測試的模擬請求,假設咱們存在一個下面這樣的接口:mvc

@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;
    }
}

參數設定爲name=felord.cn&age=18,那麼對應的HTTP報文是這樣的:

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

能夠預見的返回值爲:

{
    "test": "bar",
    "version": "v1",
    "username": "felord.cn"
}

事實上對接口的測試能夠分爲如下幾步。

構建請求

構建請求由MockMvcRequestBuilders負責,他提供了請求方法(Method),請求頭(Header),請求體(Body),參數(Parameters),會話(Session)等全部請求的屬性構建。/foo/user接口的請求能夠轉換爲:

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

執行Mock請求

而後由MockMvc執行Mock請求:

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

對結果進行處理

請求結果被封裝到ResultActions對象中,它封裝了多種讓咱們對Mock請求結果進行處理的方法。

對結果進行預期指望

ResultActions#andExpect(ResultMatcher matcher)方法負責對響應的結果的進行預期指望,看看是否符合測試的指望值。參數ResultMatcher負責從響應對象中提取咱們須要指望的部位進行預期比對。

假如咱們指望接口/foo/user返回的是JSON,而且HTTP狀態爲200,同時響應體包含了version=v1的值,咱們應該這麼聲明:

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

JsonPath是一個強大的JSON解析類庫,請經過其項目倉庫https://github.com/json-path/JsonPath瞭解。

對響應進行處理

ResultActions#andDo(ResultHandler handler)方法負責對整個請求/響應進行打印或者log輸出、流輸出,由MockMvcResultHandlers工具類提供這些方法。咱們能夠經過以上三種途徑來查看請求響應的細節。

例如/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 = []

獲取返回結果

若是你但願進一步處理響應的結果,也能夠經過ResultActions#andReturn()拿到MvcResult 類型的結果進行進一步的處理。

完整的測試過程

一般andExpect是咱們必然會選擇的,而andDoandReturn在某些場景下會有用,它們兩個是可選的。咱們把上面的連在一塊兒。

@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());
            
}

這種流式的接口單元測試從語義上看也是比較好理解的,你可使用各類斷言、正例、反例測試你的接口,最終讓你的接口更加健壯。

5. 總結

一旦你熟練了這種方式,你編寫的接口將更加具備權威性而不會再漏洞百出,甚至有時候你也可使用Mock來設計接口,使之更加貼合業務。因此CRUD不是徹底沒有技術含量,高質量高效率的CRUD每每須要這種工程化的單元測試來支撐。好了今天的分享就到這裏,我是:碼農小胖哥,多多關注,多多支持。

關注公衆號:Felordcn 獲取更多資訊

我的博客:https://felord.cn

相關文章
相關標籤/搜索