基于Spring web 做单元测试
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 { 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"); Mockito.doReturn("secondhello").when(list).get(1); String str = list.get(0); Mockito.verify(list).get(0); Assert.assertEquals(str, "helloworld"); }
|
创建mock对象不能对final
,Anonymous
(匿名类) ,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 { List<String> list = Mockito.mock(List.class); Mockito.when(list.get(1)).thenThrow(new RuntimeException("这是类型的错误")); String result = list.get(1); 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 { 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.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 { 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
| compile group: 'junit', name: 'junit', version: '4.12' compile group: 'org.mockito', name: 'mockito-core', version: '2.0.106-beta' compile group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '1.4.0.RELEASE'
|
- 如果你要用到一些Spring自带的注解,比如
@Autowired
的话,最好是在测试类的基类中,加入如下注解,这样会使得测试时先将SpringBoot运行起来。
注意:
@SpringgApplicationConfigtation
注解在SpringBoot的1.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 { 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()); 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; 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); when(spy.size()).thenReturn(100); spy.add("one"); spy.add("two"); System.out.println(spy.get(0)); 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 ,使用该类可以轻松的将Spring和JUnit进行集成。该类的用法示例如下:
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) @TransactionConfiguration ( transactionManager = "txManager", defaultRollback = true ) @Transactional public class AccountServiceTest { @Inject private SpringManagedBean 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 ) @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; public class UserControllerTest { private UserController userController; @Before public void setUp() { userController = new UserController(); } @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
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
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
方法 |
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")) .andExpect(forwardedUrl("/WEB-INF/jsp/user/view.jsp")) .andExpect(status().isOk()) .andDo(print());
|
测试普通控制器,但是URL错误,即404
1 2 3 4 5 6
| 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(); Assert.assertNotNull(result.getModelAndView().getModel().get("user"));
|
验证请求参数绑定到模型数据及Flash属性
1 2 3 4 5 6
| mockMvc.perform(post("/user").param("name", "zhang")) .andExpect(handler().handlerType(UserController.class)) .andExpect(handler().methodName("create")) .andExpect(model().hasNoErrors()) .andExpect(flash().attributeExists("success")) .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)) .andExpect(jsonPath("$.id").value(1)); 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()) .andReturn(); Assert.assertTrue(HttpMessageNotReadableException.class.isAssignableFrom(result.getResolvedException().getClass()));
|
XML请求/响应验证
- 测试时需要安装
spring oxm
和xstream
依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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)) .andExpect(xpath("/user/id/text()").string("1")); 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()) .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()) .andExpect(content().string(CoreMatchers.containsString("var"))); mockMvc.perform(get("/static/app1.js")) .andExpect(status().isNotFound());
|
异步测试
1 2 3 4 5 6 7 8 9 10
| MvcResult result = mockMvc.perform(get("/user/async1?id=1&name=zhang")) .andExpect(request().asyncStarted()) .andExpect(request().asyncResult(CoreMatchers.instanceOf(User.class))) .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
| result = mockMvc.perform(get("/user/async2?id=1&name=zhang")) .andExpect(request().asyncStarted()) .andExpect(request().asyncResult(CoreMatchers.instanceOf(User.class))) .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)) .alwaysDo(print()) .alwaysExpect(request().attribute("default", true)) .build(); mockMvc.perform(get("/user/1")) .andExpect(model().attributeExists("user"));
|
记住测试步骤,按照步骤操作,整个测试过程是非常容易理解的:
- 准备测试环境
- 通过MockMvc执行请求
- 添加验证断言
- 添加结果处理器
- 得到MvcResult进行自定义断言/进行下一步的异步请求
- 卸载测试环境
集成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
| @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") }) 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")); }
|
- mockMvc.perform 执行一个请求;
- MockMvcRequestBuilders.get(“/user/1”) 构造一个请求
- ResultActions.andExpect 添加执行完成后的断言
- ResultActions.andDo 添加一个结果处理器,表示要对结果做点什么事情,比如此处使用MockMvcResultHandlers.print() 输出整个响应结果信息。
- ResultActions.andReturn 表示执行完成后返回相应的结果。