简单介绍在工作用到的对Controller进行单元测试。其实,在编写单元测试的时候还是遇到了一些问题没有解决(基于公司封装的框架,不能用最新的包 (⊙﹏⊙)b)。先记录下主要的代码,其他问题慢慢解决。
所需要基本的依赖包
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>1.10.19</version> <scope>test</scope> </dependency> <dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path-assert</artifactId> <version>0.9.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> <version>1.3</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.0.3.RELEASE</version> <scope>test</scope> </dependency> |
当然还需要一些基本的Spring的包,就不列出了。
被测试的代码
Controller类:
@RestController @RequestMapping("/v0.1/statistics") public class StatisticsController{ @Autowired StatisticsService statisticsService; @RequestMapping(value = "/diaries", method = RequestMethod.GET) public Object getDiariesStatistics(@AuthenticationPrincipal UserInfo userInfo) { if (userInfo == null) { throw new BizException(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "缺少认证"); } UserStatistics userStatistics = statisticsService.getDiariesStatistics(userInfo.getUserId()); return entityToVO(userStatistics); } } |
Service类:
@Service
public class StatisticsService {
@Autowired
StatisticsRepository statisticsRepository;
public UserStatistics getDiariesStatistics(String userId) {
return statisticsRepository.findByUserId(userId);
}
}
因为只是要对Controller进行单元测试,就不列举StatisticsRepository的代码了;另因为演示,也就不列举UserStatistics代码,可以自己替换为相关的实体。
独立的Controller单元测试
这种是单纯地对Controller进行单元测试。这里会对Service进行mock处理,流程不会走到Service层。
import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.mockito.Matchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; public class StatisticsControllerTestWithStandalone { private MockMvc mockMvc; @Mock private StatisticsService statisticsService; @InjectMocks private StatisticsController statisticsController; private UserStatistics userStatistics; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); this.mockMvc = MockMvcBuilders.standaloneSetup(statisticsController).build(); userStatistics = new UserStatistics(); userStatistics.setUserId("330134"); Statistics statistics = new Statistics(); statistics.setDiaryCount(10); userStatistics.setStatistics(statistics); } @Test public void testGetDiariesStatistics() throws Exception { when(statisticsService.getDiariesStatistics(any(String.class))).thenReturn(userStatistics); mockMvc.perform(get("/v0.1/statistics/diaries").header("Authorization", "debug userId=330134")) .andDo(print()).andExpect(status().isOk()) .andExpect(content().contentType("application/json;charset=UTF-8")) .andExpect(jsonPath("diaryCount").value(10)); verify(statisticsService).getDiariesStatistics(any(String.class)); } } |
先来说明下代码:
@Mock :mock出一个对象
@InjectMocks :使mock对象的使用类可以注入mock对象。比如上面的例子中,我们要把StatisticsService注入到StatisticsController中,那么我们就要对StatisticsController进行InjectMocks,对StatisticsService进行mock
MockitoAnnotations.initMocks(this) : 将打上Mockito标签的对象起作用,使得Mock的类被Mock,使用了Mock对象的类自动与Mock对象关联。
通过 MockMvcBuilders.standaloneSetup 模拟一个Mvc测试环境,通过build得到一个MockMvc
MockMvc :测试时经常用到核心API,具体可以看官网文档
运行结果:
MockHttpServletRequest: HTTP Method = GET Request URI = /v0.1/statistics/diaries Parameters = {} Headers = {Authorization=[debug userId=330134]} Handler: Type = nd.sdp.imdiary.statistics.controller.StatisticsController Method = public java.lang.Object nd.sdp.imdiary.statistics.controller.StatisticsController.getDiariesStatistics(com.nd.gaea.rest.security.authens.UserInfo) Async: Was async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[application/json;charset=UTF-8]} Content type = application/json;charset=UTF-8 Body = {"diaryCount":10,"firstDiaryDate":null,"lastDiaryDate":null,"unfinishedDate":null} Forwarded URL = null Redirected URL = null Cookies = [] |
细心看以上代码会发现,我参数用到是 any(String.class) 。这是因为在独立测试该Controller的时候, @AuthenticationPrincipal UserInfo userInfo 怎么也获得不到对应的值。据说这个在最新的Spring Security中有解决方案(用WithSecurityContextTestExcecutionListener),而项目用到是3.2.3版本。
集成到Web的单元测试
有的时候我们需要对系统进行集成单元测试,那么我们就可以做如下操作:
import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.ContextConfiguration; 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 javax.servlet.Filter; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @ContextConfiguration(classes = {WebConfig.class, MongodbConfig.class}) @WebAppConfiguration @RunWith(SpringJUnit4ClassRunner.class) public class StatisticsControllerTest { protected MockMvc mockMvc; @Autowired private Filter springSecurityFilterChain; @Autowired private WebApplicationContext wac; @Before public void setUp() { MockitoAnnotations.initMocks(this); this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).addFilters(springSecurityFilterChain).build(); } @After public void teardown() throws Exception { SecurityContextHolder.clearContext(); } @Test public void testGetDiariesStatistics() throws Exception { mockMvc.perform(get("/v0.1/statistics/diaries").header("Authorization", "debug userId=330134")) .andDo(print()).andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)); } } |
以上的代码和单独的单元测试代码还是有一些不一样的。首先是多个几个类上的注解,然后就是少了对Service的mock,最后就是生成mockmvc的方式不一样了。
@ContextConfiguration() :指定Bean的配置文件信息。项目中用的是注解风格配置,WebConfig.class等。如果你用的是xml配置,可以把它替换为对应的xml配置
@WebAppConfiguration :在运行单元测试的时候会启动一个Web服务,所有的测试用例跑完以后停掉
@RunWith(SpringJUnit4ClassRunner.class) :示使用Spring Test组件进行单元测试
@Autowired WebApplicationContext wac :注入web环境的ApplicationContext容器
MockMvcBuilders.webAppContextSetup(this.wac) :模拟真实的Spring MVC环境
@Autowired Filter springSecurityFilterChain :获得SecurityContextPersistenceFilter