1. 前言
在Java開發中接觸的開發者大多數不太注重對介面的測試,結果在聯調對接中出現各種問題。也有的使用Postman等工具進行測試,雖然在使用上沒有什麼問題,如果介面增加了許可權測試起來就比較噁心了。所以建議在單元測試中測試介面,保證在交付前先自測介面的健壯性。今天就來分享一下胖哥在開發中是如何對Spring MVC介面進行測試的。
在開始前請務必確認新增了Spring Boot Test相關的元件,在最新的版本中應該包含以下依賴:
<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下進行的。
2. 單獨測試控制層
如果我們只需要對控制層介面(Controller)進行測試,且該介面不依賴@Service
、@Component
等註解宣告的Spring Bean時,可以藉助@WebMvcTest
來啟用只針對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());
}
}
這種方式要快的多,它只載入了應用程式的一小部分。但是如果你涉及到服務層這種方式是不湊效的,我們就需要另一種方式了。
3. 整體測試
大多數Spring Boot下的介面測試是整體而又全面的測試,涉及到控制層、服務層、持久層等方方面面,所以需要載入比較完整的Spring Boot上下文。這時我們可以這樣做,宣告一個抽象的測試基類:
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。
然後針對具體的控制層進行如下測試程式碼的編寫:
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的呼叫,這樣可以使得測試速度快、不依賴網路環境,而且提供了一套驗證的工具,這樣可以使得請求的驗證統一而且很方便。接下來我們來一步步構造一個測試的模擬請求,假設我們存在一個下面這樣的介面:
@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
是我們必然會選擇的,而andDo
和andReturn
在某些場景下會有用,它們兩個是可選的。我們把上面的連在一起。
@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 獲取更多資訊