Junit测试 - mockMVC

什么是Mock?

在面向对象的程序设计中,模拟对象(英语:mock object)是以可控的方式模拟真实对象行为的假对象。在编程过程中,通常通过模拟一些输入数据,来验证程序是否达到预期结果。

使用mock工具可以直接模拟http请求,不用直接产生网络的请求环境,简化了测试流程。

MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。

Spring MVC的测试往往看似比较复杂。其实他的不同在于,他需要一个ServletContext来模拟我们的请求和响应。但是Spring也针对Spring MVC 提供了请求和响应的模拟测试接口,以方便我们的单元测试覆盖面不只是service,dao层。

 

为什么使用Mock对象?

使用模拟对象,可以模拟复杂的、真实的对象行为。如果在单元测试中无法使用真实对象,可采用模拟对象进行替代。

在以下情况可以采用模拟对象来替代真实对象:

  • 真实对象的行为是不确定的(例如,当前的时间或温度);
  • 真实对象很难搭建起来;
  • 真实对象的行为很难触发(例如,网络错误);
  • 真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化);
  • 真实的对象是用户界面,或包括用户界面在内;
  • 真实的对象使用了回调机制;
  • 真实对象可能还不存在;
  • 真实对象可能包含不能用作测试(而不是为实际工作)的信息和方法。

测试流程:

1 mockMvc调用perform,调用controller的业务处理逻辑

2 perform返回ResultActions,返回操作结果,通过ResultActions,提供了统一的验证方式。

3 使用StatusResultMatchers对请求结果进行验证

4 使用ContentResultMatchers对请求返回的内容进行验证

MockMvc

MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。

接口MockMvcBuilder,提供一个唯一的build方法,用来构造MockMvc。主要有两个实现:StandaloneMockMvcBuilder和DefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和集成Web环境测试(并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。MockMvcBuilders提供了对应的创建方法standaloneSetup方法和webAppContextSetup方法,在使用时直接调用即可。

举例: Junit 测试Springboot +RestFul风格API

1)导入启动器,创建SpringBoot项目中默认引入的spring-boot-starter-test间接引入了spring-test,因此无需再额外引入jar包。

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

2)具体实例

package com.inventec.studentManagement.controller;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentControllerTest {

	@Autowired
	StudentController studentController;

	private MockMvc mvc;

	@Before
	public void setUp() {
		mvc = MockMvcBuilders.standaloneSetup(studentController).build();
	}

	/**
	 * @throws Exception
	 * 
	 *                   1.测试所有学生,有数据,正向测试,测试成功
	 */
	@Test
	public void selectStudentsTest() throws Exception {
		RequestBuilder request;
	
      /*
     * 1、mockMvc.perform执行一个请求。
     * 2、MockMvcRequestBuilders.get("XXX")构造一个请求。
     * 3、ResultActions.param添加请求传值
     * 4、ResultActions.accept(MediaType.TEXT_HTML_VALUE))设置返回类型
     * 5、ResultActions.andExpect添加执行完成后的断言。
     * 6、ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情
     *   比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。
     * 7、ResultActions.andReturn表示执行完成后返回相应的结果。
     */

	
		/*方法:GET
		URL:http://localhost:8080/students
		请求参数:无*/


		// 1、get查询所有学生

		request = get("/students/") 
          // 设置返回值类型为utf-8,否则默认为ISO-8859-1
            .accept(MediaType.APPLICATION_JSON_UTF8_VALUE);
		mvc.perform(request).andExpect(status().isOk()).andExpect(content().string(not("")))
				.andExpect(content().string(not("[]")));

	}

	/**
	 * @throws Exception
	 * 
	 *                   1.测试所有学生,无数据,正向测试,测试成功
	 */
	@Test
	public void selectStudentsNotStudentTest() throws Exception {
		RequestBuilder request;
		
		/*方法:GET
		URL:http://localhost:8080/students
		请求参数:无*/

		// 1、get查询所有学生,无数据。
		request = get("/students/");
		mvc.perform(request).andExpect(status().isOk())
				.andExpect(content().string(equalTo("{\"state\":1,\"message\":null,\"data\":[]}")));

	}

	/**
	 * @throws Exception
	 * 
	 *                   2. 测试根据学号查询某个学生  测试成功
	 */
	@Test
	public void selectStudentTest() throws Exception {
		RequestBuilder request;
		
		/*方法:GET
		URL:http://localhost:8080/students/”student_sno”
		请求参数: String student_sno(15位任意数字或字母)*/


		// 1、get根据正确学号查询某个学生

		request = get("/students/773456789012345");
		mvc.perform(request).andExpect(status().isOk()).andExpect(content().string(equalTo(
				"{\"state\":1,\"message\":null,\"data\":{\"student_sno\":\"773456789012345\",\"student_sname\":\"rose\",\"student_sex\":1,\"student_age\":18,\"student_time\":null}}")));

		

	}
	
	
	

	/**
	 * @throws Exception
	 * 
	 *                   3. 测试 新增一个学生信息, 正向测试,失败
	 * 
	 */
	@Test
	public void addStudentTest() throws Exception {
		RequestBuilder request;
		
		/*方法:POST
		URL:http://localhost:8080/students
		请求参数:JSON
		 {	
		String student_sno;(学生编号) 	 必填(15位数字或者字母)   
		String student_sname;(学生姓名)	必填
		int student_sex;   (学生性别)         必填
		int student_age;   (学生年龄)       必填
		String student_time;(创建时间)  	选填(yyy-MM-SS DD:MM:SS)
		       }
		*/
		
		// 学号 新增一个学生信息,无时间,测试成功。
		request = post("/students/").contentType(MediaType.APPLICATION_JSON)
				.content("{\"student_sno\":\"773456789012345\",\"student_sname\":\"rose\",\"student_sex\":1,\"student_age\":18}");
		mvc.perform(request)
				.andExpect(content().string(equalTo("{\"state\":1,\"message\":\"success\",\"data\":null}")));
		
		
	}

	
	/**
	 * @throws Exception
	 * 
	 *                   4.修改一个学生信息,反向测试,测试部分成功,部分失败。
	 */
	@Test
	public void updateStudentReverseTest() throws Exception {
		RequestBuilder request;

		/*String student_sno 必填 (15位任意数字或字母)
		JSON
		 {	
		String student_sno;(学生编号) 	选填(15位数字或者字母)   
		String student_sname;(学生姓名) 	选填
		int student_sex; (学生性别)      选填
		int student_age;   (学生年龄)    选填
		String student_time;(创建时间)  	选填(yyy-MM-SS DD:MM:SS)
		       }
		*/

		// put 修改id长度,   不通过测试,   数据修改成功,数据库没有更新数据。     测试失败。
		request = patch("/students/123456789012345").contentType(MediaType.APPLICATION_JSON).content(
				"{\"student_sno\":12534,\"student_sname\":\"tom\",\"student_sex\":\"0\",\"student_age\":18}");
		mvc.perform(request)
				.andExpect(content().string(equalTo("{\"state\":1,\"message\":\"success\",\"data\":null}")));
		
		
		

		// put 修改不存在的id,   通过测试
		/* request = patch("/students/903456789012345").contentType(MediaType.APPLICATION_JSON)
				.content("{\"student_sno\":12534,\"student_sname\":\"tom\",\"student_sex\":\"0\",\"student_age\":18}");
		mvc.perform(request).andExpect(content().string(equalTo("{\"state\":0,\"message\":\"fail\",\"data\":null}")));

		*/

		
	}

	/**
	 * @throws Exception 5.删除一个学生信息,正向删除 通过测试
	 */
	@Test
	public void deleteStudentTest() throws Exception {
		RequestBuilder request;
		
		/*方法:DELETE
		URL:http://localhost:8080/students/"student_sno"
		请求参数:String student_sno 必填 (15位任意数字或字母)*/

		// 删除成功的情况
		
		/*
		request = delete("/students/663456789012345");
		mvc.perform(request)
				.andExpect(content().string(equalTo("{\"state\":1,\"message\":\"success\",\"data\":null}")));
		*/
		
		
	}

	/**
	 * @throws Exception 5.删除一个学生信息,逆向测试, 全部通过测试
	 */
	@Test
	public void deleteStudentReverseTest() throws Exception {
		RequestBuilder request;
		
		/*方法:DELETE
		URL:http://localhost:8080/students/"student_sno"
		请求参数:String student_sno 必填 (15位任意数字或字母)*/


		//	请求参数:String student_sno 必填 (15位任意数字或字母)

		// 删除失败的情况,不存在的id删除
		request = delete("/students/403456789012345");
		mvc.perform(request).andExpect(content().string(equalTo("{\"state\":0,\"message\":\"fail\",\"data\":null}")));

		// 删除失败的情况,位数不够的id删除
		request = delete("/students/56789012345");
		mvc.perform(request).andExpect(content().string(equalTo("{\"state\":0,\"message\":\"fail\",\"data\":null}")));

		// 删除失败的情况,重复删除
		request = delete("/students/113456789012345");
		mvc.perform(request).andExpect(content().string(equalTo("{\"state\":0,\"message\":\"fail\",\"data\":null}")));

		// 删除失败的情况,重复删除
		request = delete("/students/113456789012345");
		mvc.perform(request).andExpect(content().string(equalTo("{\"state\":0,\"message\":\"fail\",\"data\":null}")));

		// 删除失败的情况,不存在的的id
		request = delete("/students/3456789012345aa");
		mvc.perform(request).andExpect(content().string(equalTo("{\"state\":0,\"message\":\"fail\",\"data\":null}")));

		// 删除失败的情况,不传id
		request = delete("/students/");
		mvc.perform(request).andExpect(status().is4xxClientError());

	}

}

 3)常用的例子

1.测试普通控制器

mockMvc.perform(get("/user/{id}", 1)) //执行请求  
            .andExpect(model().attributeExists("user")) //验证存储模型数据  
            .andExpect(view().name("user/view")) //验证viewName  
            .andExpect(forwardedUrl("/WEB-INF/jsp/user/view.jsp"))//验证视图渲染时forward到的jsp  
            .andExpect(status().isOk())//验证状态码  
            .andDo(print()); //输出MvcResult到控制台

2.得到MvcResult自定义验证

MvcResult result = mockMvc.perform(get("/user/{id}", 1))//执行请求  
        .andReturn(); //返回MvcResult  
Assert.assertNotNull(result.getModelAndView().getModel().get("user")); //自定义断言

 3.验证请求参数绑定到模型数据及Flash属性

mockMvc.perform(post("/user").param("name", "zhang")) //执行传递参数的POST请求(也可以post("/user?name=zhang"))  
            .andExpect(handler().handlerType(UserController.class)) //验证执行的控制器类型  
            .andExpect(handler().methodName("create")) //验证执行的控制器方法名  
            .andExpect(model().hasNoErrors()) //验证页面没有错误  
            .andExpect(flash().attributeExists("success")) //验证存在flash属性  
            .andExpect(view().name("redirect:/user")); //验证视图

4.文件上传

byte[] bytes = new byte[] {1, 2};  
mockMvc.perform(fileUpload("/user/{id}/icon", 1L).file("icon", bytes)) //执行文件上传  
        .andExpect(model().attribute("icon", bytes)) //验证属性相等性  
        .andExpect(view().name("success")); //验证视图

5.JSON请求/响应验证

String requestBody = "{\"id\":1, \"name\":\"zhang\"}";  
    mockMvc.perform(post("/user")  
            .contentType(MediaType.APPLICATION_JSON).content(requestBody)  
            .accept(MediaType.APPLICATION_JSON)) //执行请求  
            .andExpect(content().contentType(MediaType.APPLICATION_JSON)) //验证响应contentType  
            .andExpect(jsonPath("$.id").value(1)); //使用Json path验证JSON 请参考http://goessner.net/articles/JsonPath/  
    String errorBody = "{id:1, name:zhang}";  
    MvcResult result = mockMvc.perform(post("/user")  
            .contentType(MediaType.APPLICATION_JSON).content(errorBody)  
            .accept(MediaType.APPLICATION_JSON)) //执行请求  
            .andExpect(status().isBadRequest()) //400错误请求  
            .andReturn();  
    Assert.assertTrue(HttpMessageNotReadableException.class.isAssignableFrom(result.getResolvedException().getClass()));//错误的请求内容体

6.异步测试

//Callable  
    MvcResult result = mockMvc.perform(get("/user/async1?id=1&name=zhang")) //执行请求  
            .andExpect(request().asyncStarted())  
            .andExpect(request().asyncResult(CoreMatchers.instanceOf(User.class))) //默认会等10秒超时  
            .andReturn();  
    mockMvc.perform(asyncDispatch(result))  
            .andExpect(status().isOk())  
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))  
            .andExpect(jsonPath("$.id").value(1));

 7.全局配置

mockMvc = webAppContextSetup(wac)  
            .defaultRequest(get("/user/1").requestAttr("default", true)) //默认请求 如果其是Mergeable类型的,会自动合并的哦mockMvc.perform中的RequestBuilder  
            .alwaysDo(print())  //默认每次执行请求后都做的动作  
            .alwaysExpect(request().attribute("default", true)) //默认每次执行后进行验证的断言  
            .build();  
    mockMvc.perform(get("/user/1"))  
            .andExpect(model().attributeExists("user"));

MockMvcResultHandlers.print()打印结果中body中文乱码的解决

ResultActions resultActions = mockMvc.perform(post(requestUrl)
                                            .accept(MediaType.APPLICATION_JSON)
                                            .content(requestParam));
        resultActions.andReturn().getResponse().setCharacterEncoding("UTF-8");
        //添加断言
        resultActions.andDo(print()).andExpect(status().isOk());

 

4)方法解释说明。

常用注解

  •   RunWith(SpringJUnit4ClassRunner.class): 表示使用Spring Test组件进行单元测试;
  •  WebAppConfiguration: 使用这个Annotate会在跑单元测试的时候真实的启一个web服务,然后开始调用Controller的Rest API,待单元测试跑完之后再将web服务停掉;作用是模拟ServletContext
  •  ContextConfiguration: 指定Bean的配置文件信息,可以有多种方式,这个例子使用的是文件路径形式,如果有多个配置文件,可以将括号中的信息配置为一个字符串数组来表示;

常用方法说明:

  • perform:执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理
  • get:声明发送一个get请求的方法。MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables):根据uri模板和uri变量值得到一个GET请求方式的。另外提供了其他的请求的方法,如:post、put、delete等。
  • param:添加request的参数,如上面发送请求的时候带上了了pcode = root的参数。假如使用需要发送json数据格式的时将不能使用这种方式,可见后面被@ResponseBody注解参数的解决方法
  • andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确(对返回的数据进行的判断);
  • andDo:添加ResultHandler结果处理器,比如调试时打印结果到控制台(对返回的数据进行的判断);
  • andReturn:最后返回相应的MvcResult;然后进行自定义验证/进行下一步的异步处理(对返回的数据进行的判断);

如果要进行远端测试:可以使用httpclient

  httpclient 测试RestFul风格接口详细教程    https://blog.csdn.net/huzecom/article/details/103589457

发布了51 篇原创文章 · 获赞 121 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/huzecom/article/details/103694319