基于Spring web 做单元测试

基于Spring web 做单元测试

0824

Mock

  • mock对象就是在调试期间用来作为真实对象的替代品。
  • mock测试就是在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试。

添加依赖

1
2
//Mockito-all测试框架
compile group: 'org.mockito', name: 'mockito-all', version: '2.0.2-beta'
  • 需要在@Before注解的setUp()中进行初始化(下面这个是个测试类的基类)
1
2
3
4
5
6
7
public abstract class MockitoBasedTest {
@Before
public void setUp() throws Exception {
// 初始化测试用例类中由Mockito的注解标注的所有模拟对象
MockitoAnnotations.initMocks(this);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void toIndex() throws Exception {
List<String> list = Mockito.mock(List.class);
//设置方法预期返回值 【√】
Mockito.when(list.get(0)).thenReturn("helloworld");
//设置方法的预期返回值 不推荐不如上一种方式 可读性不高 【X】
Mockito.doReturn("secondhello").when(list).get(1);
String str = list.get(0);
//验证方法调用(是否调用了get(0))
Mockito.verify(list).get(0);
//junit测试
Assert.assertEquals(str, "helloworld");
}

创建mock对象不能对finalAnonymous(匿名类)primitive(基本数据类型如int、double等和包装类)类进行mock。

如果mock的话,会给你一个红灯:

1
2
3
4
5
6
org.mockito.exceptions.base.MockitoException:
Cannot mock/spy class java.lang.Integer
Mockito cannot mock/spy following:
- final classes
- anonymous classes
- primitive types
  • 设置方法设定返回异常
1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void toIndex() throws Exception {
//创建mock对象,参数可以是类,也可以是接口
List<String> list = Mockito.mock(List.class);
//设置方法设定返回异常
Mockito.when(list.get(1)).thenThrow(new RuntimeException("这是类型的错误"));
String result = list.get(1);
//junit测试
Assert.assertEquals("helloworld", result);
}
  • 没有返回值的void方法与其设定(支持迭代风格,第一次调用donothing,第二次dothrow抛出runtime异常)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void toIndex() throws Exception {
//创建mock对象,参数可以是类,也可以是接口
List<String> list = Mockito.mock(List.class);
Mockito.doNothing().doThrow(new RuntimeException("void exception")).when(list).clear();
list.clear();
System.out.println("第一次调用完");
list.clear();
System.out.println("第二次调用完");
// Mockito.times(N) 调用N次
Mockito.verify(list,Mockito.times(2)).clear();
}

参数匹配器(Argument Matcher)

  • Matchers类内加你有很多参数匹配器 anyInt、anyString、anyMap…..Mockito类继承于Matchers,Stubbing时使用内建参数匹配器
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void toIndex() throws Exception {
//创建mock对象,参数可以是类,也可以是接口
List<String> list = Mockito.mock(List.class);
Mockito.when(list.get(Mockito.anyInt())).thenReturn("hello", "world");
String result = list.get(0) + list.get(1);
Mockito.verify(list, Mockito.times(2)).get(Mockito.anyInt());
Assert.assertEquals("helloworld", result);
}

注意:如果使用参数匹配器,那么所有的参数都要使用参数匹配器,不管是stubbing还是verify的时候都一样。

Intellij Idea 的 converage

真的是好强大

Spring与单元测试

添加依赖

1
2
3
4
5
6
7
8
// https://mvnrepository.com/artifact/junit/junit
compile group: 'junit', name: 'junit', version: '4.12'
// https://mvnrepository.com/artifact/org.mockito/mockito-core
compile group: 'org.mockito', name: 'mockito-core', version: '2.0.106-beta'
//https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test
compile group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '1.4.0.RELEASE'
  • 如果你要用到一些Spring自带的注解,比如@Autowired的话,最好是在测试类的基类中,加入如下注解,这样会使得测试时先将SpringBoot运行起来。

注意:

@SpringgApplicationConfigtation注解在SpringBoot1.4版本以后不建议使用

使用SpringBootTest注解替代@SpringgApplicationConfigtation

1
2
3
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = Application.class)
  • 接下来需要在@Before注解的setUp()中进行初始化
1
2
3
4
5
6
7
8
9
10
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = Application.class)
public abstract class MockitoBasedTest {
@Before
public void setUp() throws Exception {
// 初始化测试用例类中由Mockito的注解标注的所有模拟对象
MockitoAnnotations.initMocks(this);
}
}

被测类中@Autowired注解,如何控制其中Repository返回值

1
2
3
4
5
6
7
8
9
10
public class GameHelper {
@Autowired
private PointRepository pointRepository;
public boolean checkLineItem(final Line line) {
Point fromPoint = pointRepository.findById(line.getFromPointId()); //如何控制这个repository的返回?
Point toPoint = pointRepository.findById(line.getToPointId());
return fromPoint.getID().equals(toPoint.getID());//简化了原函数
}
}
  • 对被测类中@Autowired的对象,用@Mocks标注;对被测类自己,用@InjectMocks标注。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GameHelperTest {
@Mock
private PointRepository pointRepository;
@InjectMocks
private GameHelper gamehelper; //pointRepository作为mock对象被注入到gamehelper中,gamehelper其他成员变量不变
public void testCheckLineItem() {
Line line = new Line(***);
when(pointRepository.findById(123L)).thenReturn(new Point(***));
when(pointRepository.findById(456L)).thenReturn(new Point(***));
assertTrue(gamehelper.checkLineItem(line));
}
...
}

官方解释:

@InjectMocks - injects mock or spy fields into tested object automatically.

  • @Mock标注的对象会自动注入到被@InjectMocks标注的对象中。
  • 在上例中,GameHelper中的成员变量pointRepository(的函数),就会被我们用在测试用例中改写过返回值的pointRepository对象替换掉。

另外,经测试,thenReturn返回的是对象引用而不是深复制了对象本身(所以可以减少写thenReturn()的次数)。

被测函数调用被测类其他函数,怎么控制返回值?

比如在CreateGameServiceImpl这个类中,有这样一段函数
public class CreateGameServiceImpl implements CreateGameService {

...//省略成员变量

public FullGame createGame(String name, Long creatorId, List<Point> points, List<Selection> selections, List<Line> lines) {
    Game gameItem = createBlankGame(name, creatorId);    //createBlankGame()为CreateGameServiceImpl中另一个函数

那么,如果我还没实现createBlackGame(),我在测试函数里应该怎么控制它呢?这次用2)中的方法@Mock + @InjectMocks就不行了,因为他们属于同一个类。

(这个问题@Xander 觉得应该实现了被调用的函数才好,但是既然mock的存在很多时候是为了在函数都没实现的情况下编写测试,因此我觉得继续研究。)

后来自己通过查阅官方的文档,解决办法是使用spy()命令,结合doReturn():
public class CreateGameServiceImplTest {
//这部分不需要改。省略其他成员变量
@Mock
private GameHelper gameHelper;

@InjectMocks
CreateGameServiceImpl serviceimpl;

@Test
public void testCreateGameStringLongListOfPointListOfSelectionListOfLine() {
    serviceimpl = spy(serviceimpl); //将serviceimpl部分mock化
    doReturn(***).when(serviceimpl).createBlankGame(a, b);  //这里必须用doReturn()而不能是when().thenReturn()
    ...
}

}

原因我们在最后解释。

首先我们来看文档中对于Spy()的解释:

You can create spies of real objects. When you use the spy then the methods are called (unless a method was stubbed).

Spying on real objects can be associated with “partial mocking” concept.(重点是,spy与”部分mock”相关。)

对于Spy,官方有个Sample:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List list = new LinkedList();
List spy = spy(list);
//optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);
//using the spy calls real methods
spy.add("one");
spy.add("two");
//prints "one" - 这个函数还是真实的
System.out.println(spy.get(0));
//100 is printed - size()函数被替换了
System.out.println(spy.size());
  • 通俗来讲,在我个人理解,Spy()可以使一个对象的一部分方法被用户替换。
  • 在我们的例子中,CreateGameServiceImpl中的函数createGame()调用了createBlankGame(),而后者可能是未实现的。
  • 但是此时CreateGameServiceImpl类的注解是@InjectMocks而不是@Mock,只能接收@Mock对象的注入,而自己的方法无法被mock(stub)。
  • 因此我们通过spy(),将CreateGameServiceImpl部分mock化,从而将createBlankGame()函数替换掉。
  • 不过这里如果遇到private的被调函数就没办法了。

在JUnit中集成Spring上下文的支持

  • 使用JUnit 4.x提供的注解@RunWith,可以指定单元测试的“运行类”,运行类必须继承自 org.junit.runner.Runner 并实现 run 方法。
  • Spring Test 框架提供的运行类是 SpringJUnit4ClassRunner ,使用该类可以轻松的将SpringJUnit进行集成。该类的用法示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@RunWith ( SpringJUnit4ClassRunner.class ) //指定单元测试运行类
@SpringBootTest(classes = Application.class)
//很多情况下单元测试离不开事务,下面的注解指明使用的事务管理器
//如果defaultRollback为true,测试运行结束后,默认回滚事务,不影响数据库
@TransactionConfiguration ( transactionManager = "txManager", defaultRollback = true )
@Transactional //指定默认所有测试方法的事务特性
public class AccountServiceTest
{
@Inject
private SpringManagedBean bean; //任何Spring管理的Bean都可以注入到单元测试类
@BeforeClass
public static void setUpBeforeClass() throws Exception
{
}
@AfterClass
public static void tearDownAfterClass() throws Exception
{
}
@Before
public void setUp() throws Exception
{
}
@After
public void tearDown() throws Exception
{
}
@Repeat ( 10 )//重复测试10次
//该测试期望抛出IllegalArgumentException,测试超时1秒
@Test ( expected = IllegalArgumentException.class, timeout = 1000 )
@Rollback ( true )
//测试完毕后回滚
public void test()
{
}
}

Spring MVC的测试

以前的测试方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.sishuok.mvc.controller;
//省略import
public class UserControllerTest {
private UserController userController;
@Before
public void setUp() {
userController = new UserController();
//安装userCtroller依赖 比如userService
}
@Test
public void testView() {
MockHttpServletRequest req = new MockHttpServletRequest();
ModelAndView mv = userController.view(1L, req);
ModelAndViewAssert.assertViewName(mv, "user/view");
ModelAndViewAssert.assertModelAttributeAvailable(mv, "user");
}
}
  • 准备控制器:我们通过new方式创建一个,然后手工查找依赖注入进去(比如从spring容器获取/new的);
  • Mock Request:此处使用Spring提供的MockAPI模拟一个HttpServletRequest,其他的Servlet API也提供了相应的Mock类,具体请查看Javadoc;
  • 访问控制器方法:通过直接调用控制器方法进行访问,此处无法验证SpringMVC框架的类型转换、数据验证等是否正常;
  • ModelAndViewAssert:通过这个Assert API验证我们的返回值是否正常;

这种方式的缺点:

  • 如不能走Spring MVC完整流程(不能走Servlet的过滤器链、SpringMVC的类型转换、数据验证、数据绑定、拦截器等等),如果做基本的测试没问题,这种方式就是纯粹的单元测试,我们想要的功能其实是一种集成测试,不过后续部分不区分

安装测试环境

  • spring mvc测试框架提供了两种方式,独立安装和集成Web环境测试(此种方式并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。

独立测试方式

1
2
3
4
5
6
7
public class UserControllerStandaloneSetupTest {
private MockMvc mockMvc;
@Before
public void setUp() {
UserController userController = new UserController();
mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
}
  • 首先自己创建相应的控制器,注入相应的依赖
  • 通过MockMvcBuilders.standaloneSetup模拟一个Mvc测试环境,通过build得到一个MockMvc
  • MockMvc:是我们以后测试时经常使用的API,后边介绍

测试API

  • Spring mvc测试框架提供了测试MVC需要的API,主要包括

    • Servlet/JSPMock
    • MockMvcBuilder
    • MockMvc
    • RequestBuilder
    • ResultMatcher
    • ResultHandler
    • MvcResult
  • 另外提供了几个静态工厂方法便于测试:

    • MockMvcBuilders
    • MockMvcRequestBuilder
    • MockMvcResultMatchers
    • MockMvcResultHandler
  • 在使用时请使用静态方法导入方便测试,如:

1
2
3
4
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
  • Servlet/JSP API Mock 提供了对Servlet 3 相应API的Mock,如:
    • MockServletContext
    • MockHttpServletRequest
    • MockHttpServletResponse
  • 具体请查看spring-test模块的org.springframework.mock.web包。

API如下:

MockMvcBuilder/MockMvcBuilders主要API:

  • MockMvcBuilder是用来构造MockMvc的构造器,其主要有两个实现:
    • StandaloneMockMvcBuilder
    • DefaultMockMvcBuilder
  • 分别对应之前的两种测试方式。对于我们来说直接使用静态工厂MockMvcBuilders创建即可:
方法 API说明
MockMvcBuilders.webAppContextSetup(WebApplicationContextcontext) 指定WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的MockMvc
MockMvcBuilders.standaloneSetup(Object... controllers) 通过参数指定一组控制器,这样就不需要从上下文获取了

DefaultMockMvcBuilder主要API:

方法 API说明
addFilters(Filter... filters)/addFilter(Filter filter, String... urlPatterns) 添加javax.servlet.Filter过滤器
defaultRequest(RequestBuilderrequestBuilder) 默认的RequestBuilder,每次执行时会合并到自定义的RequestBuilder中,即提供公共请求数据的;
alwaysExpect(ResultMatcher resultMatcher) 定义全局的结果验证器,即每次执行请求时都进行验证的规则;
alwaysDo(ResultHandler resultHandler) 定义全局结果处理器,即每次请求时都进行结果处理;
dispatchOptions DispatcherServlet是否分发OPTIONS请求方法到控制器;

StandaloneMockMvcBuilder继承了DefaultMockMvcBuilder

方法 API说明
setMessageConverters(HttpMessageConverter<?>…messageConverters) 设置HTTP消息转换器;
setValidator(Validator validator) ` 设置验证器;
setConversionService(FormattingConversionService conversionService) 设置转换服务;
addInterceptors(HandlerInterceptor…interceptors) 添加spring mvc拦截器;
addMappedInterceptors(String[]pathPatterns,HandlerInterceptor… interceptors) 添加spring mvc拦截器;
setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) 设置内容协商管理器;
setAsyncRequestTimeout(long timeout) 设置异步超时时间;
setCustomArgumentResolvers(HandlerMethodArgumentResolver… argumentResolvers) 设置自定义控制器方法参数解析器;
setCustomReturnValueHandlers(HandlerMethodReturnValueHandler… handlers) 设置自定义控制器方法返回值处理器;
setHandlerExceptionResolvers(List exceptionResolvers)/setHandlerExceptionResolvers(HandlerExceptionResolver… exceptionResolvers) 设置异常解析器;
setViewResolvers(ViewResolver…resolvers) 设置视图解析器;
setSingleView(View view) 设置单个视图,即视图解析时总是解析到这一个(仅适用于只有一个视图的情况);
setLocaleResolver(LocaleResolver localeResolver) 设置Local解析器;
setFlashMapManager(FlashMapManager flashMapManager) 设置FlashMapManager,如存储重定向数据;
setUseSuffixPatternMatch(boolean useSuffixPatternMatch)` 设置是否是后缀模式匹配,如“/user”是否匹配”/user.*”,默认真即匹配;
setUseTrailingSlashPatternMatch(boolean useTrailingSlashPatternMatch) 设置是否自动后缀路径模式匹配,如“/user”是否匹配“/user/”,默认真即匹配;
addPlaceHolderValue(String name, String value) 添加request mapping中的占位符替代;

MockMvc主要API:

  • 使用之前的MockMvcBuilder.build()得到构建好的MockMvc
  • 这个是mvc测试的核心API,对于该API的使用方式如下:
1
2
3
4
5
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/user/1"))
.andExpect(MockMvcResultMatchers.view().name("user/view"))
.andExpect(MockMvcResultMatchers.model().attributeExists("user"))
.andDo(MockMvcResultHandlers.print())
.andReturn();
方法 API说明
perform 执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理;
andExpect 添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确;
andDo 添加ResultHandler结果处理器,比如调试时打印结果到控制台;
andReturn 最后返回相应的MvcResult;然后进行自定义验证/进行下一步的异步处理;
还提供了以下API
setDefaultRequest 设置默认的RequestBuilder,用于在每次perform执行相应的RequestBuilder时自动把该默认的RequestBuilder合并到perform的RequestBuilder中;
setGlobalResultMatchers 设置全局的预期结果验证规则,如我们通过MockMvc测试多个控制器时,假设它们都想验证某个规则时,就可以使用这个;
setGlobalResultHandlers 设置全局的ResultHandler结果处理器;

RequestBuilder / MockMvcRequestBuilders

  • RequestBuilder用来构建请求的,其提供了一个方法buildRequest(ServletContextservletContext)用于构建MockHttpServletRequest 其主要有两个子类。

    • MockHttpServletRequestBuilder
    • MockMultipartHttpServletRequestBuilder(如文件上传使用)
  • 用来Mock客户端请求需要的所有数据。

MockMvcRequestBuilders主要API:

方法 API说明
MockHttpServletRequestBuilderget(StringurlTemplate,Object…urlVariables) 根据uri模板和uri变量值得到一个GET请求方式的MockHttpServletRequestBuilder;如get(“/user/{id}”, 1L);
MockHttpServletRequestBuilder post(String urlTemplate, Object… urlVariables) 同get类似,但是是POST方法;
MockHttpServletRequestBuilder put(String urlTemplate, Object… urlVariables) 同get类似,但是是PUT方法;
MockHttpServletRequestBuilder delete(String urlTemplate, Object… urlVariables) 同get类似,但是是DELETE方法;
MockHttpServletRequestBuilder options(String urlTemplate, Object…urlVariables) 同get类似,但是是OPTIONS方法;
MockHttpServletRequestBuilderrequest(HttpMethodhttpMethod,StringurlTemplate,Object…urlVariables) 提供自己的Http请求方法及uri模板和uri变量,如上API都是委托给这个API;
MockMultipartHttpServletRequestBuilderfileUpload(StringurlTemplate,Object…urlVariables) 提供文件上传方式的请求,得到MockMultipartHttpServletRequestBuilder;
RequestBuilder asyncDispatch(finalMvcResultmvcResult) 创建一个从启动异步处理的请求的MvcResult进行异步分派的RequestBuilder;

MockHttpServletRequestBuilder和MockMultipartHttpServletRequestBuilder API:

MockHttpServletRequestBuilder API:

方法 API说明
MockHttpServletRequestBuilderheader(Stringname,Object…values) 添加头信息
MockHttpServletRequestBuilderheaders(HttpHeaders httpHeaders) 添加头信息
MockHttpServletRequestBuilder contentType(MediaType mediaType) 指定请求的contentType头信息
MockHttpServletRequestBuilderaccept(MediaType…mediaTypes)/MockHttpServletRequestBuilderaccept(String…mediaTypes) 指定请求的Accept头信息
MockHttpServletRequestBuildercontent(byte[]content)/MockHttpServletRequestBuildercontent(Stringcontent) 指定请求Body体内容
MockHttpServletRequestBuilder cookie(Cookie… cookies) 指定请求的Cookie
MockHttpServletRequestBuilder locale(Locale locale 指定请求的Locale
MockHttpServletRequestBuilder characterEncoding(String encoding) 指定请求字符编码
MockHttpServletRequestBuilder requestAttr(String name, Object value) 设置请求属性数据
MockHttpServletRequestBuilder sessionAttr(String name, Object value) 置请求session属性数据;
MockHttpServletRequestBuilder sessionAttrs(Map sessionAttributes) 置请求session属性数据
MockHttpServletRequestBuilder flashAttr(String name, Object value) 指定请求的flash信息,比如重定向后的属性信息
MockHttpServletRequestBuilder flashAttrs(MapflashAttributes) 指定请求的flash信息,比如重定向后的属性信息
MockHttpServletRequestBuilder session(MockHttpSession session) 指定请求的Session
MockHttpServletRequestBuilder principal(Principal principal) 指定请求的Principal
MockHttpServletRequestBuildercontextPath(StringcontextPath) 指定请求的上下文路径,必须以“/”开头,且不能以“/”结尾
MockHttpServletRequestBuilder pathInfo(String pathInfo) 请求的路径信息,必须以“/”开头
MockHttpServletRequestBuilder secure(boolean secure) 请求是否使用安全通道
MockHttpServletRequestBuilderwith(RequestPostProcessorpostProcessor) 请求的后处理器,用于自定义一些请求处理的扩展点

MockMultipartHttpServletRequestBuilder继承自MockHttpServletRequestBuilder,又提供了如下API:

方法 API说明
MockMultipartHttpServletRequestBuilder file(String name, byte[] content) 指定要上传的文件
MockMultipartHttpServletRequestBuilder file(MockMultipartFile file) 指定要上传的文件

ResultActions

  • 调用MockMvc.perform(RequestBuilder requestBuilder)后将得到ResultActions,通过ResultActions完成如下三件事:
    • ResultActions andExpect(ResultMatcher matcher) :添加验证断言来判断执行请求后的结果是否是预期的;
    • ResultActions andDo(ResultHandlerhandler):添加结果处理器,用于对验证成功后执行的动作,如输出下请求/结果信息用于调试;
    • MvcResult andReturn()返回验证成功后的MvcResult;用于自定义验证/下一步的异步处理;

ResultMatcher / MockMvcResultMatchers

  • ResultMatcher用来匹配执行完请求后的结果验证,其就一个 match(MvcResultresult) 断言方法,如果匹配失败将抛出相应的异常;
  • spring mvc测试框架提供了很多ResultMatchers来满足测试需求。注意这些ResultMatchers并不是ResultMatcher的子类,而是返回ResultMatcher实例的。
  • Spring mvc测试框架为了测试方便提供了MockMvcResultMatchers静态工厂方法方便操作;

  • 具体的API如下:

方法 API说明
HandlerResultMatchers handler() 请求的Handler验证器,比如验证处理器类型/方法名;此处的Handler其实就是处理请求的控制器;
RequestResultMatchers request() 得到RequestResultMatchers验证器
ModelResultMatchers model() 得到模型验证器
ViewResultMatchers view() 得到视图验证器
FlashAttributeResultMatchers flash() 得到Flash属性验证
StatusResultMatchers status() 得到响应状态验证器
HeaderResultMatchers header() 得到响应Header验证器
CookieResultMatchers cookie() 得到响应Cookie验证器
ContentResultMatchers content() 得到响应内容验证器
JsonPathResultMatchers jsonPath(String expression, Object … args) 得到Json表达式验证器
ResultMatcher jsonPath(String expression, Matcher matcher) 得到Json表达式验证器
XpathResultMatchers xpath(String expression, Object… args) 得到Xpath表达式验证器
XpathResultMatchers xpath(String expression,Mapnamespaces,Object…args) 得到Xpath表达式验证器
ResultMatcher forwardedUrl(final String expectedUrl) 验证处理完请求后转发的url(绝对匹配)
ResultMatcher forwardedUrlPattern(final String urlPattern) 验证处理完请求后转发的url(Ant风格模式匹配,@since spring4)
ResultMatcher redirectedUrl(final String expectedUrl) 验证处理完请求后重定向的url(绝对匹配)
ResultMatcher redirectedUrlPattern(final StringexpectedUrl) 验证处理完请求后重定向的url(Ant风格模式匹配,@since spring4)
  • 得到相应的ResultMatchers后,接着再调用其相应的API得到ResultMatcher
  • ModelResultMatchers.attributeExists(final String...names)判断Model属性是否存在。具体请查看相应的API。

ResultHandler/MockMvcResultHandlers

  • ResultHandler用于对处理的结果进行相应处理的,比如输出整个请求/响应等信息方便调试
  • Springmvc测试框架提供了MockMvcResultHandlers静态工厂方法,该工厂提供了ResultHandlerprint()返回一个输出MvcResult详细信息到控制台的ResultHandler实现。

MvcResult

  • 即执行完控制器后得到的整个结果,并不仅仅是返回值,其包含了测试时需要的所有信息,如:
方法 API说明
MockHttpServletRequest getRequest(): 得到执行的请求;
MockHttpServletResponse getResponse(): 得到执行后的响应;
Object getHandler(): 得到执行的处理器,一般就是控制器;
HandlerInterceptor[] getInterceptors(): 得到对处理器进行拦截的拦截器;
ModelAndView getModelAndView(): 得到执行后的ModelAndView;
Exception getResolvedException(): 得到HandlerExceptionResolver解析后的异常;
FlashMap getFlashMap(): 得到FlashMap;
Object getAsyncResult() 得到异步执行的结果;
Object getAsyncResult(long timeout) 得到异步执行的结果;

测试示例

测试普通控制器

1
2
3
4
5
6
7
//测试普通控制器
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到控制台

测试普通控制器,但是URL错误,即404

1
2
3
4
5
6
//找不到控制器,404测试
MvcResult result = mockMvc.perform(get("/user2/{id}", 1)) //执行请求
.andDo(print())
.andExpect(status().isNotFound()) //验证控制器不存在
.andReturn();
Assert.assertNull(result.getModelAndView()); //自定义断言

得到MvcResult自定义验证

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

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

1
2
3
4
5
6
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")); //验证视图

验证请求参数验证失败出错

1
2
3
4
mockMvc.perform(post("/user").param("name", "admin")) //执行请求
.andExpect(model().hasErrors()) //验证模型有错误
.andExpect(model().attributeDoesNotExist("name")) //验证存在错误的属性
.andExpect(view().name("showCreateForm")); //验证视图
1
2
3
4
5
//文件上传
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")); //验证视图

JSON请求/响应验证

  • 测试时需要安装jackson Json和JsonPath依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()));//错误的请求内容体

XML请求/响应验证

  • 测试时需要安装spring oxmxstream依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//XML请求/响应
String requestBody = "<user><id>1</id><name>zhang</name></user>";
mockMvc.perform(post("/user")
.contentType(MediaType.APPLICATION_XML).content(requestBody)
.accept(MediaType.APPLICATION_XML)) //执行请求
.andDo(print())
.andExpect(content().contentType(MediaType.APPLICATION_XML)) //验证响应contentType
.andExpect(xpath("/user/id/text()").string("1")); //使用XPath表达式验证XML 请参考http://www.w3school.com.cn/xpath/
String errorBody = "<user><id>1</id><name>zhang</name>";
MvcResult result = mockMvc.perform(post("/user")
.contentType(MediaType.APPLICATION_XML).content(errorBody)
.accept(MediaType.APPLICATION_XML)) //执行请求
.andExpect(status().isBadRequest()) //400错误请求
.andReturn();
Assert.assertTrue(HttpMessageNotReadableException.class.isAssignableFrom(result.getResolvedException().getClass()));//错误的请求内容体

异常处理

1
2
3
4
5
6
//异常处理
MvcResult result = mockMvc.perform(get("/user/exception")) //执行请求
.andExpect(status().isInternalServerError()) //验证服务器内部错误
.andReturn();
Assert.assertTrue(IllegalArgumentException.class.isAssignableFrom(result.getResolvedException().getClass()));

静态资源

1
2
3
4
5
6
7
//静态资源
mockMvc.perform(get("/static/app.js")) //执行请求
.andExpect(status().isOk()) //验证状态码200
.andExpect(content().string(CoreMatchers.containsString("var")));//验证渲染后的视图内容包含var
mockMvc.perform(get("/static/app1.js")) //执行请求
.andExpect(status().isNotFound()); //验证状态码404

异步测试

1
2
3
4
5
6
7
8
9
10
//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));
1
2
3
4
5
6
7
8
9
10
//DeferredResult
result = mockMvc.perform(get("/user/async2?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));
  • 此处请在第一次请求时加上 andExpect(request().asyncResult(CoreMatchers.instanceOf(User.class))) 这样会等待结果返回/超时,无须自己设置线程等待了;此处注意request().asyncResult一定是在第一次请求发出;然后第二次通过asyncDispatch进行异步请求。

添加自定义过滤器

1
2
mockMvc = webAppContextSetup(wac).addFilter(new MyFilter(), "/*").build();
mockMvc.perform(get("/user/1")).andExpect(request().attribute("filter", true));

全局配置

1
2
3
4
5
6
7
8
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"));

记住测试步骤,按照步骤操作,整个测试过程是非常容易理解的:

  1. 准备测试环境
  2. 通过MockMvc执行请求
  3. 添加验证断言
  4. 添加结果处理器
  5. 得到MvcResult进行自定义断言/进行下一步的异步请求
  6. 卸载测试环境

集成Web环境方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//XML风格
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration(value = "src/main/webapp")
@ContextHierarchy({
@ContextConfiguration(name = "parent", locations = "classpath:spring-config.xml"),
@ContextConfiguration(name = "child", locations = "classpath:spring-mvc.xml")
})
//注解风格
//@RunWith(SpringJUnit4ClassRunner.class)
//@WebAppConfiguration(value = "src/main/webapp")
//@ContextHierarchy({
// @ContextConfiguration(name = "parent", classes = AppConfig.class),
// @ContextConfiguration(name = "child", classes = MvcConfig.class)
//})
public class UserControllerWebAppContextSetupTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
}
  • 通过@Autowired WebApplicationContextwac注入web环境的ApplicationContext容器;
  • 然后通过MockMvcBuilders.webAppContextSetup(wac).build()创建一个MockMvc进行测试;

开始测试

1
2
3
4
5
6
7
8
9
10
@Test
public void testView() throws Exception {
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/user/1"))
.andExpect(MockMvcResultMatchers.view().name("user/view"))
.andExpect(MockMvcResultMatchers.model().attributeExists("user"))
.andDo(MockMvcResultHandlers.print())
.andReturn();
Assert.assertNotNull(result.getModelAndView().getModel().get("user"));
}
  1. mockMvc.perform 执行一个请求;
  2. MockMvcRequestBuilders.get(“/user/1”) 构造一个请求
  3. ResultActions.andExpect 添加执行完成后的断言
  4. ResultActions.andDo 添加一个结果处理器,表示要对结果做点什么事情,比如此处使用MockMvcResultHandlers.print() 输出整个响应结果信息。
  5. ResultActions.andReturn 表示执行完成后返回相应的结果。
Contents
  1. 1. 基于Spring web 做单元测试
  2. 2. Mock
    1. 2.1. 添加依赖
    2. 2.2. 参数匹配器(Argument Matcher)
    3. 2.3. Intellij Idea 的 converage
  3. 3. Spring与单元测试
    1. 3.1. 添加依赖
  4. 4. 被测类中@Autowired注解,如何控制其中Repository返回值
    1. 4.1. 被测函数调用被测类其他函数,怎么控制返回值?
  5. 5. 在JUnit中集成Spring上下文的支持
  6. 6. Spring MVC的测试
    1. 6.1. 以前的测试方式
  7. 7. 安装测试环境
    1. 7.1. 独立测试方式
    2. 7.2. 测试API
    3. 7.3. API如下:
      1. 7.3.1. MockMvcBuilder/MockMvcBuilders主要API:
      2. 7.3.2. DefaultMockMvcBuilder主要API:
      3. 7.3.3. StandaloneMockMvcBuilder继承了DefaultMockMvcBuilder
    4. 7.4. MockMvc主要API:
    5. 7.5. RequestBuilder / MockMvcRequestBuilders
    6. 7.6. MockMvcRequestBuilders主要API:
    7. 7.7. MockHttpServletRequestBuilder和MockMultipartHttpServletRequestBuilder API:
      1. 7.7.1. MockHttpServletRequestBuilder API:
      2. 7.7.2. MockMultipartHttpServletRequestBuilder继承自MockHttpServletRequestBuilder,又提供了如下API:
      3. 7.7.3. ResultActions
    8. 7.8. ResultMatcher / MockMvcResultMatchers
      1. 7.8.1. ResultHandler/MockMvcResultHandlers
      2. 7.8.2. MvcResult
  8. 8. 测试示例
    1. 8.1. 测试普通控制器
    2. 8.2. 测试普通控制器,但是URL错误,即404
    3. 8.3. 得到MvcResult自定义验证
    4. 8.4. 验证请求参数绑定到模型数据及Flash属性
    5. 8.5. 验证请求参数验证失败出错
    6. 8.6. JSON请求/响应验证
    7. 8.7. XML请求/响应验证
    8. 8.8. 异常处理
    9. 8.9. 静态资源
    10. 8.10. 异步测试
    11. 8.11. 添加自定义过滤器
    12. 8.12. 全局配置
  9. 9. 记住测试步骤,按照步骤操作,整个测试过程是非常容易理解的:
    1. 9.1. 集成Web环境方式
    2. 9.2. 开始测试