步驟
整個測試過程非常有規律:
- 準備測試環境
- 通過MockMvc執行請求
3.1. 添加驗證斷言
3.2. 添加結果處理器
3.3. 得到MvcResult進行自定義斷言/進行下一步的異步請求 - 卸載測試環境
spring提供了mockMvc模塊,可以模擬web請求來對controller層進行單元測試
示例:MockMvc
MockMvc
Spring提供了mockMvc模塊,可以模擬web請求來對controller層進行單元測試
第一步:添加Maven依賴
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
第二步:統一返回結果相關
Result.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
// 請求響應狀態碼(200、400、500)
private int code;
// 請求結果描述信息
private String msg;
// 請求結果數據
private T data;
public Result(int code, String msg) {
this.code = code;
this.msg = msg;
}
public Result(ResultCode resultCode) {
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
}
}
ResultCode.java
public enum ResultCode {
// 成功
OK(200, "OK"),
// 服務器錯誤
INTERNAL_SERVER_ERROR(500, "Internal Server Error");
// 操作代碼
int code;
// 提示信息
String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
ResultUtil.java
public class ResultUtil {
/**
* 操作成功,返回具體的數據、結果碼和提示信息
* 用於數據查詢接口
* @param data
* @return
*/
public static Result success(Object data) {
Result<Object> result = new Result(ResultCode.OK);
result.setData(data);
return result;
}
/**
* 操作失敗,只返回結果碼和提示信息
*
* @param resultCode
* @return
*/
public static Result fail(ResultCode resultCode) {
return new Result(resultCode);
}
}
第三步:實體類
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class Dept {
private Integer deptno;
private String dname;
private String loc;
}
第四步:數據庫模擬類
public class DeptTable {
private static ArrayList<Dept> depts = new ArrayList<>();
static {
depts.add(new Dept(10, "ACCOUNTING", "CHICAGO"));
depts.add(new Dept(20, "RESEARCH", "DALLAS"));
depts.add(new Dept(30, "SALES", "CHICAGO"));
depts.add(new Dept(40, "OPERATIONS", "BOSTON"));
}
public static boolean insert(Dept dept) {
boolean res = depts.add(dept);
return res;
}
public static boolean update(Dept dept) {
Integer deptno = dept.getDeptno();
boolean flag = false;
for (int i = 0; i < depts.size(); i++) {
Dept temp = depts.get(i);
if (temp.getDeptno().equals(deptno)) {
depts.set(i, dept);
flag = true;
}
}
return flag;
}
public static boolean delete(Integer deptno) {
boolean flag = false;
for (int i = 0; i < depts.size(); i++) {
Dept dept = depts.get(i);
if (dept.getDeptno().equals(deptno)) {
depts.remove(i);
flag = true;
}
}
return flag;
}
public static Dept selectByDeptno(Integer deptno) {
for (int i = 0; i < depts.size(); i++) {
Dept dept = depts.get(i);
if (dept.getDeptno().equals(deptno)) {
return dept;
}
}
return null;
}
public static List<Dept> selectAll() {
return depts;
}
public static void output() {
for (Dept dept : depts) {
System.out.println(dept);
}
}
}
第五步:Controller接口一
@Slf4j
@RestController
@RequestMapping("/mock")
public class DeptController {
//增加Dept ,使用POST方法
@PostMapping(value = "/dept")
public Result saveDept(@RequestBody Dept dept) {
boolean res = DeptTable.insert(dept);
log.info("saveDept:{}", dept);
DeptTable.output();
if (res) {
return ResultUtil.success(dept);
} else {
return ResultUtil.fail(ResultCode.INTERNAL_SERVER_ERROR);
}
}
@GetMapping("/allDept")
public Result getUserList() {
List<Dept> depts = DeptTable.selectAll();
return ResultUtil.success(depts);
}
@GetMapping("allDept2")
public List<Dept> allDept2() {
List<Dept> depts = DeptTable.selectAll();
return depts;
}
@GetMapping("dept/{deptno}")
public Dept selectByDeptno(@PathVariable int deptno) {
Dept dept = DeptTable.selectByDeptno(deptno);
return dept;
}
@DeleteMapping("dept/{deptno}")
public Result deleteByDeptno(@PathVariable int deptno) {
boolean res = DeptTable.delete(deptno);
DeptTable.output();
if (res) {
return ResultUtil.success(deptno);
} else {
return ResultUtil.fail(ResultCode.INTERNAL_SERVER_ERROR);
}
}
//////////////////////////////////////////////////////////////////////////////
@GetMapping("fun1")
public String fun1(){
return "hello";
}
@GetMapping("dept2")
public Dept selectByDeptno2(int deptno) {
Dept dept = DeptTable.selectByDeptno(deptno);
return dept;
}
@PostMapping("fun4")
public Dept fun4(Dept dept) {
dept.setDeptno(8989);
return dept;
}
@PutMapping("fun5")
public Dept fun5(Dept dept) {
dept.setDeptno(8989);
return dept;
}
@PatchMapping("fun6")
public Dept fun6(Dept dept) {
dept.setDeptno(8989);
return dept;
}
}
第六步:容器環境下Mockito測試(★★★★)
Servlet容器注入時還可以讓Spring容器注入其它的對象,如果測試方法中還依賴有其它的對象,比如依賴Service,就需要使用這種方式了。
實現一
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class DeptController0Test { //Servlet容器環境下測試
@Autowired
private WebApplicationContext wac;
//mock對象:用來模擬網絡請求
private MockMvc mockMvc;
@Before
public void setup() {
//mock對象初始化,指定WebApplicationContext,將會從該上下文獲取相應的控制器並得到相應的MockMvc
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
//測試方法:運行測試方法時不需要啓動服務
@Test
public void saveDept() throws Exception {
System.out.println(mockMvc);
Dept dept = new Dept(10, "ACCOUNTING", "CHICAGO");
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(dept);
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.request(HttpMethod.POST, "/mock/dept")
.contentType(MediaType.APPLICATION_JSON)
.content(json);
MvcResult result = mockMvc.perform(builder)//執行一個RequestBuilder請求,會自動執行SpringMVC的流程並映射到相應的控制器執行處理
.andExpect(MockMvcResultMatchers.status().isOk()) //添加RequestMatcher驗證規則,驗證控制器執行完成後是否正確
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value(200))
.andExpect(MockMvcResultMatchers.jsonPath("$.msg").value("OK"))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.dname").value("ACCOUNTING"))
.andDo(print()) //添加ResultHandler結果處理器,比如調試時打印結果到控制檯
.andReturn();//最後返回相應的MvcResult,然後進行自定義驗證/進行下一步的異步處理
String res = result.getResponse().getContentAsString();
log.info(res);
}
@Test
public void allDept() throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/mock/allDept")
.contentType(MediaType.APPLICATION_JSON);
MvcResult result = mockMvc.perform(builder)
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.data[0].dname").value("ACCOUNTING"))
.andDo(print())
.andReturn();
String res = result.getResponse().getContentAsString();
log.info(res);
}
@Test
public void allDept2() throws Exception {
MockHttpServletRequestBuilder builder = get("/mock/allDept2")
.contentType(MediaType.APPLICATION_JSON);
String result = mockMvc.perform(builder)//構造一個請求
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(4))
.andReturn()
.getResponse()
.getContentAsString();
System.out.println("Result === " + result);
}
@Test
public void selectByDeptno() throws Exception {
mockMvc.perform(get("/mock/dept/10"))
.andExpect(status().isOk())
.andDo(print());
}
@Test
public void deleteByDeptno() throws Exception {//delete
mockMvc.perform(delete("/mock/dept/10"))
.andReturn();
}
}
方式二
Servlet容器環境測試代碼也可以簡寫爲:
@Slf4j
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@SpringBootTest
public class DeptController1Test { //Servlet容器環境下測試
@Autowired
private WebApplicationContext wac;
//mock對象:用來模擬網絡請求
@Resource
private MockMvc mockMvc;
@Test
public void fun1() throws Exception {
mockMvc.perform((get("/mock/fun1")))
.andExpect(MockMvcResultMatchers.content().string("hello"))
.andExpect(status().isOk()) //添加執行完成後的斷言
.andDo(print());//添加一個結果處理器,此處表示輸出整個響應的結果信息
}
@Test
public void selectByDeptno2() throws Exception {
mockMvc.perform((get("/mock/dept2")
.param("deptno", "30"))) //添加傳值請求
.andExpect(status().isOk()) //添加執行完成後的斷言
.andDo(print());//添加一個結果處理器,此處表示輸出整個響應的結果信息
}
@Test
public void fun4() throws Exception {//post
final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("dname", "sales");
params.add("loc", "china");
String mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/mock/fun4")
.params(params))
.andReturn()
.getResponse()
.getContentAsString();
System.out.println("Result === " + mvcResult);
}
@Test
public void fun5() throws Exception {//put
final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("dname", "sales");
params.add("loc", "china");
String mvcResult = mockMvc.perform(MockMvcRequestBuilders.put("/mock/fun5")
.params(params))
.andReturn()
.getResponse()
.getContentAsString();
System.out.println("Result === " + mvcResult);
}
@Test
public void fun6() throws Exception {//patch
final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("dname", "sales");
params.add("loc", "china");
String mvcResult = mockMvc.perform(MockMvcRequestBuilders.patch("/mock/fun6")
.params(params))
.andReturn()
.getResponse()
.getContentAsString();
System.out.println("Result === " + mvcResult);
}
}
第七步:Controller接口二
@Slf4j
@Controller
@RequestMapping("/mock2")
public class DeptController2 {
@GetMapping("/dept/{deptno}")
public ModelAndView selectByDeptno(@PathVariable("deptno") Integer deptno) {
Dept dept = DeptTable.selectByDeptno(deptno);
ModelAndView mav = new ModelAndView();
mav.setViewName("dept/detail");
mav.addObject("dept", dept);
return mav;
}
@PostMapping("/dept")
public String saveDept(Dept dept, RedirectAttributes redirect) {
boolean res = DeptTable.insert(dept);
redirect.addFlashAttribute("dept", res);
return "redirect:/dept/list/";
}
// 單文件上傳
@RequestMapping("/fileUpload")
@ResponseBody
public String fileUpload(@RequestParam("fileName") MultipartFile file) {
if (file.isEmpty()) {
return "file is empty";
}
String fileName = file.getOriginalFilename();
int size = (int) file.getSize();
System.out.println(fileName + "-->" + size);
String path = "E:/test";
File dest = new File(path + "/" + fileName);
if (!dest.getParentFile().exists()) { //判斷文件父目錄是否存在
dest.getParentFile().mkdir();
}
try {
file.transferTo(dest); //保存文件
return "true";
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
return "false";
}
}
}
第八步:Mockito上下文測試(★★★)
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class DeptController2Test {
//mock對象:用來模擬網絡請求
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new DeptController2())
.alwaysDo(print()) //全局配置
//默認每次執行請求後都做的動作
.alwaysExpect(MockMvcResultMatchers.status().isOk()) //默認每次執行後進行驗證的斷言
.build();
}
//測試普通控制器
@Test
public void selectByDeptno() throws Exception {
System.out.println(mockMvc);
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/mock2/dept/10"))
.andExpect(MockMvcResultMatchers.model().attributeExists("dept"))
.andExpect(MockMvcResultMatchers.forwardedUrl("dept/detail"))
.andDo(print())
.andReturn();
String res = result.getResponse().getContentAsString();
log.info(res);
}
//得到MvcResult自定義驗證
@Test
public void selectByDeptno2() throws Exception {
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/mock2/dept/{deptno}", 10))//執行請求
.andReturn(); //返回MvcResult
Assert.assertNotNull(result.getModelAndView().getModel().get("dept")); //自定義斷言
}
//驗證請求參數綁定到模型數據及Flash屬性
@Test
public void saveDept() throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/mock2/dept")
.param("deptno", "50")
.param("dname", "zhang")
.param("loc", "san");
mockMvc.perform(builder) //執行傳遞參數的POST請求(也可以post("/user?name=zhang"))
.andExpect(MockMvcResultMatchers.handler().handlerType(DeptController2.class)) //驗證執行的控制器類型
.andExpect(MockMvcResultMatchers.handler().methodName("saveDept")) //驗證執行的控制器方法名
.andExpect(MockMvcResultMatchers.model().hasNoErrors()) //驗證頁面沒有錯誤
.andExpect(MockMvcResultMatchers.flash().attributeExists("dept")) //驗證存在flash屬性
.andExpect(MockMvcResultMatchers.view().name("redirect:/dept/list/"))
.andDo(print()); //驗證視圖
}
//上傳測試
@Test
public void fileTest() throws Exception {
MockMultipartFile mmf = new MockMultipartFile("fileName", "aim.xlsx", "application/ms-excel", new FileInputStream(new File("E:/ABCD.xlsx")));
MockMultipartHttpServletRequestBuilder buider = MockMvcRequestBuilders.fileUpload("/mock2/fileUpload").
file(mmf);
ResultActions resultActions = mockMvc.perform(buider);
MvcResult mvcResult = resultActions.andDo(MockMvcResultHandlers.print())
.andReturn();
String result = mvcResult.getResponse().getContentAsString();
System.out.println("==========結果爲:==========\n" + result + "\n");
}
}
輕量級測試:僅僅測試Controller層
- Service
@Service
public class DeptService {
public String ff(){
System.out.println("--------------DeptService ff()----------------");
return "正確";
}
}
- DeptController
@RestController
@RequestMapping("/mock3")
public class DeptController3 {
@Resource
private DeptService deptService;
@GetMapping("/fun1")
public void fun1() {
System.out.println("Controller 層執行");
deptService.ff();
}
}
- 測試代碼
@Slf4j
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@WebMvcTest
public class DeptController3Test { //Servlet容器環境下測試
//mock對象:用來模擬網絡請求
@Resource
private MockMvc mockMvc;
@MockBean //僞造一個Service
private DeptService deptService;
@Test
public void fun1() throws Exception {
//打樁:設置當調用 deptService.ff()時的返回值爲"ok",而不用返回deptService.ff()方法的真正返回值"正確",而不用真正執行service中的方法
when(deptService.ff()).thenReturn("ok");
mockMvc.perform((get("/mock3/fun1")))
.andExpect(status().isOk()) //添加執行完成後的斷言
.andDo(print());//添加一個結果處理器,此處表示輸出整個響應的結果信息
}
}