声明一点:对于软件质量的保证,单元测试真的非常有必要,在CMMI-5标准也是对单元测试有明确要求的。
有的公司的对开发要求严谨的话,开发流程可能执行的是测试驱动开发(TDD
),即根据拿到需求,开发人员先写预期单元测试用例,再根据满足测试用例的要求来开发业务代码。
在不启动项目的情况下,对Spring Web项目,需要一些Servlet
相关的模拟对象,比如:MockMvc,MockHttpSession,MockHttpServletRequest,MockHttpServletResponse
等,还需要WebAppConfiguration
。
示例代码
引入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency>
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency>
|
Controller
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
| import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping;
import com.service.impl.DemoServiceImpl;
@Controller public class NormalAndRestController { @Autowired DemoServiceImpl demoServiceImpl; @RequestMapping("/normal") public String testPage(Model model) { model.addAttribute("msg", demoServiceImpl.saySomething()); return "page"; }
@RequestMapping(value = "/testRest", produces = "text/plain;charset=UTF-8") @ResponseBody public String testRest() { return demoServiceImpl.saySomething(); } }
|
Service
1 2 3 4 5 6 7 8 9 10
| import org.springframework.stereotype.Service;
@Service public class DemoServiceImpl { public String saySomething() { return "Hello World"; } }
|
单元测试代码
Maven管理的Spring项目,单元测试代码放在src/test/java
目录里
@RunWith(SpringJUnit4ClassRunner.class)
:让测试运行于 Spring-test 测试环境。
@WebAppConfiguration
:指定加载的ApplicationContext
是个WebAppConfiguration
。
@ContextConfiguration
:加载配置类或XML配置文件。
@Transactional
:用于事务回滚,将测试数据删除掉,保证数据库数据干净。
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext;
import com.config.SpringMvcConfig; import com.service.impl.DemoServiceImpl;
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringMvcConfig.class) @WebAppConfiguration("/src/main/webapp") public class TestController { private MockMvc mockMvc; @Autowired private DemoServiceImpl demoServiceImpl; @Autowired WebApplicationContext webApplicationContext; @Autowired MockHttpSession session; @Autowired MockHttpServletRequest request; @Autowired MockHttpServletResponse response;
@Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build(); }
@Test public void testNormalController() throws Exception { mockMvc.perform(get("/normal")) .andExpect(status().isOk()) .andExpect(view().name("page")) .andExpect(forwardedUrl("/WEB-INF/jsp/page.jsp")) .andExpect(model().attribute("msg", demoServiceImpl.saySomething())); } @Test public void testRestController() throws Exception { mockMvc.perform(get("/testRest")) .andExpect(status().isOk()) .andExpect(content().contentType("text/plain;charset=UTF-8")) .andExpect(content().string(demoServiceImpl.saySomething())); } }
|
page.jsp页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="${pageContext.request.contextPath}/static/js/jquery-1.11.0.js"></script>
<title>Test Page</title> </head> <body> <pre>Welcome to Spring MVC World</pre> </body> </html>
|
MockMvc原理分析
MockMvc执行测试步骤:
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
| @Test public void testNormalController() throws Exception { mockMvc.perform(get("/normal")) .andExpect(status().isOk()) .andExpect(view().name("page")) .andExpect(forwardedUrl("/WEB-INF/jsp/page.jsp")) .andExpect(model().attribute("msg", demoServiceImpl.saySomething())); RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/normal"); ResultActions resultActions = mockMvc.perform(requestBuilder);
ResultMatcher isOK = MockMvcResultMatchers.status().isOk(); ResultMatcher viewName = MockMvcResultMatchers.view().name("page"); ResultMatcher viewURL = MockMvcResultMatchers.forwardedUrl("/WEB-INF/jsp/page.jsp"); ResultMatcher attribute = MockMvcResultMatchers.model().attribute("msg", demoServiceImpl.saySomething()); resultActions.andExpect(isOK) .andExpect(viewName) .andExpect(viewURL) .andExpect(attribute); }
|
测试之前执行初始化
MockMvcBuilder
是用来构造 MockMvc
的构造器,其主要有两个实现:StandaloneMockMvcBuilder
和DefaultMockMvcBuilder
,StandaloneMockMvcBuilder
继承了 DefaultMockMvcBuilder
。
直接使用静态工厂 MockMvcBuilders
创建即可。
MockMvcBuilders.webAppContextSetup(WebApplicationContext context):指定 WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的 MockMvc
。
原理分析: MockMvcBuilders 构造器是重点,webAppContextSetup()
方法是通过DefaultMockMvcBuilder
构造方法返回一个包含WebApplicationContext
的对象。
1 2 3
| public static DefaultMockMvcBuilder webAppContextSetup(WebApplicationContext context) { return new DefaultMockMvcBuilder(context); }
|
DefaultMockMvcBuilder
继承了AbstractMockMvcBuilder
,提供了一API。
- addFilters(Filter… filters)/addFilter(Filter filter, String… urlPatterns):添加javax.servlet.Filter过滤器。
- defaultRequest(RequestBuilder requestBuilder):默认的
RequestBuilder
,每次执行时会合并到自定义的 RequestBuilder中,即提供公共请求数据的。
- alwaysExpect(ResultMatcher resultMatcher):定义全局的结果验证器,即每次执行请求时都进行验证的规则。
- alwaysDo(ResultHandler resultHandler):定义全局结果处理器,即每次请求时都进行结果处理。
- dispatchOptions:
DispatcherServlet
是否分发 OPTIONS
请求方法到控制器。
其它见上方代码注释:2,3,4。
ResultActions.andDo
添加一个结果处理器,表示要对结果做点什么事情,比如此处使用 MockMvcResultHandlers.print()
输出整个响应结果信息。
ResultActions.andReturn
表示执行完成后返回相应的结果。