Spring的测试框架及MockMvc
快速入门
使用Spring的测试框架需要添加如下依赖:
<!--测试框架-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
<scope>test</scope>
</dependency>
测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={RootConfig.class, WebConfig.class}) //如果是Java Config配置的,指定相关的配置类
//@ContextConfiguration(locations = { "classpath:spring/applicationContext.xml" }) //如果是xml配置指定文件路径
@WebAppConfiguration
public class SimpleSpringTest {
@Test
public void print(){
System.out.println("print test");
}
}
- @RunWith:用于指定junit运行环境,是junit提供给其他框架测试环境接口扩展,为了便于使用spring的依赖注入,spring提供了org.springframework.test.context.junit4.SpringJUnit4ClassRunner作为Junit测试环境。
- @ContextConfiguration:导入配置文件。有XML和Java Config两种方式。从Spring3.0,@Configuration用于定义配置类,可替换xml配置文件。
- @WebAppConfiguration:加载Web应用程序上下文,否则在调用controller时会报:Caused by: java.lang.IllegalStateException: No ServletContext set。
SpringContainer.java
/**
* @Description 整个项目的程序入口
*/
public class SpringContainer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 根容器,用于获取Spring应用容器的配置文件
*
* @return
*/
@Override
protected Class<?>[] getRootConfigClasses() {
//return new Class[0];
return new Class[]{RootConfig.class};
}
/**
* Spring mvc容器,是根容器的子容器
*
* @return
*/
@Override
protected Class<?>[] getServletConfigClasses() {
//return new Class[0];
return new Class[]{WebConfig.class};
}
/**
* "/"表示由DispatcherServlet处理所有向该应用发起的请求。
*
* @return
*/
@Override
protected String[] getServletMappings() {
//return new String[0];
return new String[]{"/"};
}
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
characterEncodingFilter.setEncoding("UTF-8");
characterEncodingFilter.setForceEncoding(true);
return new Filter[]{characterEncodingFilter, characterEncodingFilter};
}
}
RootConfig.java
@Configuration
/**
* @EnableAspectJAutoProxy:开启AOP代理自动配置
* proxyTargetClass=true:表示使用CGLib动态代理技术织入增强,决定是基于接口的还是基于类的代理被创建。默认为false(JDK代理)
* 即<aop:aspectj-autoproxy proxy-target-class="true"/>
* */
@EnableAspectJAutoProxy(proxyTargetClass = true) //解决实现接口后,spring不能创建类实例的问题
@ComponentScan(
basePackages = {"com.spring"},
excludeFilters = {//设置不扫描的文件,这里会排除Controller
@ComponentScan.Filter(type = FilterType.ANNOTATION, value = RestController.class),
@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)
}
)
public class RootConfig {
}
WebConfig.java
/**
* @Description Spring MVC 配置
*/
@Configuration
@EnableWebMvc //启用Spring MVC组件
@ComponentScan("com.spring.controller") //配置扫描的ctrl层
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.viewResolver(viewResolver());
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**").addResourceLocations("classpath:/WEB-INF/images/");
}
/**
* Jackson配置
* 添加自定义转换器
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(mappingJackson2HttpMessageConverter());
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//访问/hello/index,直接跳转到/WEB-INF/pages/index.jsp页面
registry.addViewController("/hello/index2").setViewName("index");
}
/**
* Jackson配置
*
* @return
*/
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
//MediaType mediaType1 = new MediaType("application\\json");
//MediaType mediaType2 = new MediaType("charset=UTF-8");
//supportedMediaTypes.add(mediaType1);
//supportedMediaTypes.add(mediaType2);
supportedMediaTypes = MediaType.parseMediaTypes("application/json; charset=utf-8");
converter.setSupportedMediaTypes(supportedMediaTypes);
return converter;
}
/**
* 配置JSP视图解析器
*
* @return
*/
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/pages/");
resolver.setSuffix(".jsp");
//可以在JSP页面中通过${}访问beans
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}
}
注解解释
Junit注解
在junit中常用的注解有@Test、@Ignore、@BeforeClass、@AfterClass、@Before、@After、@Runwith、@Parameters。
@Test
在junit3中,是通过对测试类和测试方法的命名来确定是否是测试,且所有的测试类必须继承junit的测试基类。在junit4中,定义一个 测试方法变得简单很多,只需要在方法前加上@Test就行了。
注意:测试方法必须是public void,即公共、无返回数据。可以抛出异常。
@BeforeClass
当我们运行几个有关联的用例时,可能会在数据准备或其它前期准备中执行一些相同的命令,这个时候为了让代码更清晰,更少冗余,可以将公用的部分提取出来,放在一个方法里,并为这个
方法注解@BeforeClass。意思是在测试类里所有用例运行之前,运行一次这个方法。例如创建数据库连接、读取文件等。
注意:方法名可以任意,但必须是public static void,即公开、静态、无返回。这个方法只会运行一次。
@AfterClass
跟@BeforeClass对应,在测试类里所有用例运行之后,运行一次。用于处理一些测试后续工作,例如清理数据,恢复现场。
注意:同样必须是public static void,即公开、静态、无返回。这个方法只会运行一次。
@Before
与@BeforeClass的区别在于,@Before不止运行一次,它会在每个用例运行之前都运行一次。主要用于一些独立于用例之间的准备工作。比如两个用例都需要读取数据库里的用户A信息,但第一个用例会删除这个用户A,而第二个用例需要修改用户A。那么可以用@BeforeClass创建数据库连接。用@Before来插入一条用户A信息。
注意:必须是public void,不能为static。不止运行一次,根据用例数而定。
@After
与@Before对应。
@Runwith
首先要分清几个概念:测试方法、测试类、测试集、测试运行器。
其中测试方法就是用@Test注解的一些函数。测试类是包含一个或多个测试方法的一个**Test.java文件,测试集是一个suite,可能包含多个测试类。测试运行器则决定了用什么方式偏好去运行这些测试集/类/方法。
而@Runwith就是放在测试类名之前,用来确定这个类怎么运行的。也可以不标注,会使用默认运行器。
常见的运行器:
- @RunWith(Parameterized.class) 参数化运行器,配合@Parameters使用junit的参数化功能
- @RunWith(Suite.class)
- @SuiteClasses({ATest.class,BTest.class,CTest.class}) 测试集运行器配合使用测试集功能
- @RunWith(JUnit4.class) junit4的默认运行器
- @RunWith(JUnit38ClassRunner.class) 用于兼容junit3.8的运行器
一些其它运行器具备更多功能。例如@RunWith(SpringJUnit4ClassRunner.class)集成了spring的一些功能
@Parameters
用于使用参数化功能。
Spring Test注解
@ContextConfiguration
Spring整合JUnit4测试时,使用注解引入多个配置文件。
@ContextConfiguration(locations="../applicationContext.xml")
@ContextConfiguration(classes = SimpleConfiguration.class)
@WebAppConfiguration
@WebAppConfiguration是一个类级注释,它将加载特定于web的ApplicationContext,即WebApplicationContext,所用是模拟ServletContext。
可以用来测试web控制器。
@WebAppConfiguration必须与@ContextConfiguration结合使用。
@ActiveProfiles
在平时的开发中,通常开发一个开发库,测试一个测试库,生产一个生产库。
我们将数据库信息写在一个配置文件中,在部署的时候我们将配置文件改成对应的配置文件,这样改来改去非常麻烦。
在使用@Profile后,我们就可以定义3个配置文件dev、sit、pro其分别对应3个profile,在实际运行的时候只需给定一个参数,容器就会加载激活的配置文件,这样就简便了。
测试bean:
public class TestBean {
private String content;
public TestBean(String content) {
super();
this.content = content;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
测试配置:
@Configuration
public class TestConfig {
@Bean
@Profile("dev")
public TestBean devTestBean() {
return new TestBean("from development profile");
}
@Bean
@Profile("pro")
public TestBean proTestBean() {
return new TestBean("from production profile");
}
}
测试:
@RunWith(SpringJUnit4ClassRunner.class)
//在SpringJUnit4ClassRunner在JUnit环境下提供Spring TestContext Framework功能
@ContextConfiguration(classes={TestConfig.class}) //加载配置ApplicationContext,classes属性用来加载类
@ActiveProfiles("pro")//声明活动的profile
public class DemoBeanIntegrationTests {
@Autowired
private TestBean testBean;
@Test
public void prodBeanShouldInject() {
String expected="from production profile";
String actual=testBean.getContent();
Assert.assertEquals(expected, actual);
}
}
@ContextHierarchy
上下文体系,必须配合@ContextConfiguration注解使用。
@ContextHierarchy({
@ContextConfiguration(locations = {"/web/WEB-INF/spring.xml" }, name = "parent"),
@ContextConfiguration("/web/WEB-INF/spring-servlet.xml")
})
@TestExecutionListeners
用于指定在测试类执行之前,可以做的一些动作。
如DependencyInjectionTestExecutionListener.class可以对一测试类中的依赖进行注入,TransactionalTestExecutionListener.class用于对事务进行管理;这两个都是Spring自带的; 我们也可以实现自己的Listener类来完成我们自己的操作,只需要继承类org.springframework.test.context.support.AbstractTestExecutionListener就可以了,具体可以参照DependencyInjectionTestExecutionListener.class的实现。 Listener在实现类执行之前被执行、实现类的测试方法之前被执行,这与Listener的实现有关。
@BeforeTransaction 和 @AfterTransaction
使用这两个注解注解的方法定义了在一个事务性测试方法之前或之后执行的行为,且被注解的方法将运行在该事务性方法的事务之外
@DirtiesContext
表示每个测试方法执行完毕需关闭当前上下文并重建一个全新的上下文,即不缓存上下文。可应用到类或方法级别,但在JUnit 3.8中只能应用到方法级别。
@Repeat
表示被注解的方法应被重复执行多少次,使用如@Repeat(2)方式指定。
@Rollback
默认为true,用于替换@TransactionConfiguration中定义的defaultRollback指定的回滚行为。
@Timed
表示被注解的方法必须在多长时间内运行完毕,超时将抛出异常,使用如@Timed(millis=10)方式指定,单位为毫秒。注意此处指定的时间是如下方法执行时间之和:测试方法执行时间(或者任何测试方法重复执行时间之和)、@Before和@After注解的测试方法之前和之后执行的方法执行时间。而Junit 4中的@Test(timeout=2)指定的超时时间只是测试方法执行时间,不包括任何重复等。
@TransactionConfiguration
开启测试类的事务管理支持配置,并指定事务管理器和默认回滚行为。
MockMvc的使用
为何使用MockMvc
对模块进行集成测试时,希望能够通过输入URL对Controller进行测试,如果通过启动服务器,建立http client进行测试,这样会使得测试变得很麻烦,比如,启动速度慢,测试验证不方便,依赖网络环境等,所以为了可以对Controller进行测试,我们引入了MockMVC。
MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。
测试的逻辑
- MockMvcBuilder构造MockMvc;
- mockMvc调用perform,执行一个RequestBuilder请求,调用controller的业务处理逻辑;
- perform返回ResultActions,返回操作结果,通过ResultActions,提供了统一的验证方式;
- 使用StatusResultMatchers对请求结果进行验证;
- 使用ContentResultMatchers对请求返回的内容进行验证;
MockMvcBuilder
MockMvc是spring测试下的一个非常好用的类,它们的初始化需要在setUp中进行。
MockMvcBuilder是用来构造MockMvc的,其主要有两个实现:StandaloneMockMvcBuilder和DefaultMockMvcBuilder,前者继承了后者。
- MockMvcBuilders.webAppContextSetup(WebApplicationContext context):指定WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的MockMvc;
- MockMvcBuilders.standaloneSetup(Object... controllers):通过参数指定一组控制器,这样就不需要从上下文获取了,比如this.mockMvc = MockMvcBuilders.standaloneSetup(this.controller).build();
示例:
private MockMvc mockMvc;
@Autowired
private WebApplicationContext webApplicationContext;
@Before
public void setUp() throws Exception{
//MockMvcBuilders.webAppContextSetup(WebApplicationContext context):指定WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的MockMvc;
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();//建议使用这种
}
MockMvcRequestBuilders
从名字可以看出,RequestBuilder用来构建请求的,其提供了一个方法buildRequest(ServletContext servletContext)用于构建MockHttpServletRequest;其主要有两个子类MockHttpServletRequestBuilder和MockMultipartHttpServletRequestBuilder(文件上传使用),即用来Mock客户端请求需要的所有数据。
主要的API:
- MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables):根据uri模板和uri变量值得到一个GET请求方式的RequestBuilder,如果在controller的方法中method选择的是RequestMethod.GET,那在controllerTest中对应就要使用MockMvcRequestBuilders.get。
- post(String urlTemplate, Object... urlVariables):同get类似,但是是POST方法;
- put(String urlTemplate, Object... urlVariables):同get类似,但是是PUT方法;
- delete(String urlTemplate, Object... urlVariables) :同get类似,但是是DELETE方法;
- options(String urlTemplate, Object... urlVariables):同get类似,但是是OPTIONS方法;
//发送请求举例
//post请求
MockHttpServletRequestBuilder post = MockMvcRequestBuilders.post("/test");
//get请求
MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/test");
//post请求 带Cookie
MockHttpServletRequestBuilder post_cookie = MockMvcRequestBuilders.post("/test")
.cookie(new Cookie("cookieName","value"));//自己填键值对
//get请求 带Cookie
MockHttpServletRequestBuilder get_cookie = MockMvcRequestBuilders.post("/test")
.cookie(new Cookie("cookieName","value"));//自己填键值对
//post请求 带session
MockHttpServletRequestBuilder post_cookie_session = MockMvcRequestBuilders.post("/test")
.sessionAttr("sessionName","value");
//get请求 带session
MockHttpServletRequestBuilder get_cookie_session = MockMvcRequestBuilders.get("/test")
.sessionAttr("sessionName","value");
//post请求 带Cookie 带参
//另外也可适用场景 使用@ModelAttribute("formName")注解接收form表单对象
//例子:
// @PostMapping("/submitOrder")
// public ModelAndView submitOrder (@ModelAttribute("orderForm") ServiceProductOrder serviceProductOrder)
MockHttpServletRequestBuilder post_cookie_par = MockMvcRequestBuilders.post("/test")
.cookie(new Cookie("cookieName","value"))//自己填键值对
.param("userName","admin")//有@RequestParam注解优先对应,否则对应着表单的input标签的name属性的 值及value
.param("pass","admin");//用@ModelAttribute注解的直接对应表单内容
//get请求 带Cookie 带参方法一(用方法填充)
MockHttpServletRequestBuilder get_cookie_par_one = MockMvcRequestBuilders.post("/test")
.cookie(new Cookie("cookieName","value"))//自己填键值对
//有@RequestParam注解优先对应,否则对应着表单的input标签的name属性的 值及value
.param("userName","admin")
.param("pass","admin");
//get请求 带Cookie 带参方法二(url路径拼接)
MockHttpServletRequestBuilder get_cookie_par_two = MockMvcRequestBuilders.post("/test?userName=admin&pass=admin")
.param("pass","admin");
//post请求 带Cookie
//适用场景:使用@RequestBody注解接收对象
//例子:@PostMapping("/submitOrder")
// public ModelAndView submitOrder (@RequestBody ServiceProductOrder serviceProductOrder) {
Admin admin=new Admin();
admin.setLoginName("admin");
admin.setLoginPassword("admin");//填一些必要的参数等.
MockHttpServletRequestBuilder post_cookie_obj = MockMvcRequestBuilders.post("/test")
.cookie(new Cookie("cookieName","value"))//自己填键值对
.contentType(MediaType.APPLICATION_JSON).content(JSONObject.toJSONString(admin));//阿里巴巴的json序列化
//MultipartFile文件上传请求
String filename = "images/sy_02.png";//测试文件
InputStream inStream = getClass().getClassLoader().getResourceAsStream(filename);
MockMultipartFile mfile = new MockMultipartFile("file", "sy_02.png", "png", inStream);
MockMultipartHttpServletRequestBuilder file = MockMvcRequestBuilders.multipart("/file/upload").file(mfile);
ResultActions
调用MockMvc.perform(RequestBuilder requestBuilder)后将得到ResultActions,对ResultActions有以下三种处理:
- ResultActions.andExpect:添加执行完成后的断言。添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确;
- ResultActions.andDo:添加一个结果处理器,比如此处使用.andDo(MockMvcResultHandlers.print())输出整个响应结果信息,可以在调试的时候使用。
- ResultActions.andReturn:表示执行完成后返回相应的结果
ResultHandler用于对处理的结果进行相应处理的,比如输出整个请求/响应等信息方便调试,Spring mvc测试框架提供了MockMvcResultHandlers静态工厂方法,该工厂提供了ResultHandler print()返回一个输出MvcResult详细信息到控制台的ResultHandler实现。
示例如下:
String responseString = mockMvc.perform(
get("/categories/getAllCategory") //请求的url,请求的方法是get
.contentType(MediaType.APPLICATION_FORM_URLENCODED) //数据的格式
.param("pcode","root") //添加参数
).andExpect(status().isOk()) //返回的状态是200
.andDo(print()) //打印出请求和相应的内容
.andReturn().getResponse().getContentAsString(); //将相应的数据转换为字符串
System.out.println("--------返回的json = " + responseString);
ResultMatchers
ResultMatcher用来匹配执行完请求后的结果验证,其就一个match(MvcResult result)断言方法,如果匹配失败将抛出相应的异常,spring mvc测试框架提供了很多***ResultMatchers来满足测试需求。
MvcResult
即执行完控制器后得到的整个结果,并不仅仅是返回值,其包含了测试时需要的所有信息。
- MockHttpServletRequest getRequest():得到执行的请求;
- MockHttpServletResponse getResponse():得到执行后的响应;
- Object getHandler():得到执行的处理器,一般就是控制器;
- HandlerInterceptor[] getInterceptors():得到对处理器进行拦截的拦截器;
- ModelAndView getModelAndView():得到执行后的ModelAndView;
- Exception getResolvedException():得到HandlerExceptionResolver解析后的异常;
- FlashMap getFlashMap():得到FlashMap;
- Object getAsyncResult()/Object getAsyncResult(long timeout):得到异步执行的结果;
示例如下:
//执行请求 返回相应的MvcResult (mvc这个参数,看最上面的框架怎么注入的)
//request 就是MockHttpServletRequestBuilder类
MvcResult mvcResult = mvc.perform(request).andReturn();
//获取状态码
int status = mvcResult.getResponse().getStatus();//500状态码 302状态码 404状态码 200状态码等
//获取返回 @ResponseBody json字符串 : 进行反序列化处理即可
//注意: 500/400/302则是返回的HTML源码String类型
String contentAsString = mvcResult.getResponse().getContentAsString();
//返回ModelAndView 获取里面的页面路径
//代码: model.setViewName("/index");
ModelAndView mv = mvcResult.getModelAndView();
String url=mv.getViewName(); //得到/index
//返回ModelAndView 判断里面state参数
// model.addObject("state", 1);
ModelAndView mv = mvcResult.getModelAndView();
Map<String, Object> model = mv.getModel();
Integer state = (Integer) model.get("state");
//返回ModelAndView 判断里面的集合/map/对象
// PageInfo<Admin> list=new PageInfo<>();
// model.addObject("AdminList", list);
ModelAndView mv1 = mvcResult.getModelAndView();
Map<String, Object> model1 = mv1.getModel();
((PageInfo<Admin>) model1.get("AdminList")).getList().size();//得到list长度