对Spring MVC做单元测试

简单介绍在工作用到的对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

参考: