最近用Spring Mvc框架做了几个小项目,但都没有做单元测试,最近想恶补一下这方面的东西,包括基于Spring的单元测试,自动化测试和JS单元测试。今天先讲一下基于Spring框架的单元测试,测试使用的是Spring自带的test组件,再结合Mockito一起编写测试案例,以下示例会包括Controller和Service,由于Repository是基于Spring JPA,没有自己的逻辑,所以这里就不涉及Repository的单元测试,以后有需要再介绍。
  
Controller
首先看一下Controller的代码(如下),代码比较简单,就是接收前端发过来的一些参数,通过这些参数直接调用Service的方法。  
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
   | import com.odde.mail.model.Result; import com.odde.mail.service.MailService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.jackson.map.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody;
  import static java.lang.String.format;
  @Controller @RequestMapping("/mail") public class MailController {     private static final Log log = LogFactory.getLog(MailController.class);     private final ObjectMapper mapper = new ObjectMapper();
      @Autowired     private MailService mailService;
      @RequestMapping(value = "/send", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")     public     @ResponseBody     String send(@RequestParam("recipients") String recipients,                 @RequestParam("subject") String subject,                 @RequestParam("content") String content) throws Exception {         log.debug("mail controller send start");         log.debug(format("recipients:%s", recipients));         log.debug(format("subject:%s", subject));         log.debug(format("content:%s", content));         Result mailResult = mailService.send(recipients, subject, content);         String result = mapper.writeValueAsString(mailResult);         log.debug(format("result:%s", result));         log.debug("mail controller send finish");         return result;     } }
   | 
   
再来看对应的单元测试:  
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
   | import com.odde.mail.model.Result; import com.odde.mail.service.MailService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; 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 static org.hamcrest.CoreMatchers.is; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 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;
  @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration("file:src/main/webapp/WEB-INF/mvc-dispatcher-servlet.xml") public class MailControllerTest {     private MockMvc mockMvc;
      @Mock     private MailService mailService;
      @InjectMocks     MailController mailController;
      @Before     public void setup() {         MockitoAnnotations.initMocks(this);         this.mockMvc = MockMvcBuilders.standaloneSetup(mailController).build();     }
      @Test     public void should_return_status_success_when_send_mail_success() throws Exception {         when(mailService.send("test@test.com", "test", "test")).thenReturn(new Result("成功"));
          mockMvc.perform(post("/mail/send")                 .param("recipients", "test@test.com")                 .param("subject", "test")                 .param("content", "test"))                 .andDo(print())                 .andExpect(status().isOk()).andExpect(content().string(is("{\"status\":\"" + result + "\"}")));
          verify(mailService).send("test@test.com", "test", "test");     }
   | 
   
首先是Spring的几个Annotate
- RunWith(SpringJUnit4ClassRunner.class): 表示使用Spring Test组件进行单元测试;
 
- WebAppConfiguration: 使用这个Annotate会在跑单元测试的时候真实的启一个web服务,然后开始调用Controller的Rest API,待单元测试跑完之后再将web服务停掉;
 
- ContextConfiguration: 指定Bean的配置文件信息,可以有多种方式,这个例子使用的是文件路径形式,如果有多个配置文件,可以将括号中的信息配置为一个字符串数组来表示;
 
然后是Mockito的Annotate
- Mock: 如果该对象需要mock,则加上此Annotate;
 
- InjectMocks: 使mock对象的使用类可以注入mock对象,在上面这个例子中,mock对象是MailService,使用了MailService的是MailController,所以在Controller加上该Annotate;
 
Setup方法
MockitoAnnotations.initMocks(this): 将打上Mockito标签的对象起作用,使得Mock的类被Mock,使用了Mock对象的类自动与Mock对象关联。 
mockMvc: 细心的朋友应该注意到了这个对象,这个对象是Controller单元测试的关键,它的初始化也是在setup方法里面。 
Test Case
- 首先mock了MailService的send方法,让其返回一个成功的Result对象。
 
mockMvc.perform: 发起一个http请求。 
post(url): 表示一个post请求,url对应的是Controller中被测方法的Rest url。 
param(key, value): 表示一个request parameter,方法参数是key和value。 
andDo(print()): 表示打印出request和response的详细信息,便于调试。 
andExpect(status().isOk()): 表示期望返回的Response Status是200。 
andExpect(content().string(is(expectstring)): 表示期望返回的Response Body内容是期望的字符串。 
使用print打印处理的信息类似下面显示的内容:
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
   | MockHttpServletRequest:          HTTP Method = POST          Request URI = /mail/send           Parameters = {recipients=[test@test.com], subject=[test], content=[test]}              Headers = {}
               Handler:                 Type = com.odde.mail.controller.MailController               Method = public java.lang.String com.odde.mail.controller.MailController.send(java.lang.String,java.lang.String,java.lang.String) throws java.lang.Exception
                 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=[text/plain;charset=UTF-8], Content-Length=[19]}         Content type = text/plain;charset=UTF-8                 Body = {"status":"成功"}        Forwarded URL = null       Redirected URL = null              Cookies = []
   | 
  
Service
照例我们先看一下Service的功能代码,代码也比较简单,就是调用Repository做一些增删改查的动作。  
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
   | import com.odde.mail.model.Recipient; import com.odde.mail.model.Result; import com.odde.mail.repo.RecipientRepository; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;
  import java.util.List;
  @Service public class RecipientService {
      @Autowired     private RecipientRepository recipientRepository;
      public Result add(String username, String email) {         Recipient recipient = recipientRepository.findByEmail(email);         Result result;         if (recipient == null) {             recipientRepository.save(new Recipient(username, email));             result = new Result("成功");         } else {             result = new Result("失败");         }         return result;     } }
   | 
  
再来看对应的测试代码:  
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
   | import com.odde.mail.model.Recipient; import com.odde.mail.repo.RecipientRepository; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
  import java.util.List;
  import static java.util.Arrays.asList; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when;
  @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("file:src/main/webapp/WEB-INF/mvc-dispatcher-servlet.xml") public class RecipientServiceTest {
      @Mock     private RecipientRepository recipientRepository;
      @InjectMocks     private RecipientService recipientService;
      @Before     public void setup() {         MockitoAnnotations.initMocks(this);     }
      @Test     public void should_return_success_when_add_recipient_not_exist() throws Exception {         when(recipientRepository.findByEmail(anyString())).thenReturn(null);         when(recipientRepository.save(any(Recipient.class))).thenReturn(null);
          assertThat(recipientService.add("Tom", "test@test.com").getStatus(), is("成功"));         verify(recipientRepository).findByEmail(anyString());         verify(recipientRepository).save(any(Recipient.class));     }
   | 
  
Service的单元测试就比较简单了,大部分内容都在Controller里面讲过,不同的地方就是Controller是使用mockMvc对象来模拟Controler的被测方法,而在Service的单元测试中则是直接调用Service的方法(比如上面例子中的findByEmail和add)。
Reponsitory
最后再说一下Reponsitory的单元测试,刚才讲过这里不涉及这块的介绍,因为Reponsitory没有具体的实现代码,基本上调用的是Spring JPA的功能。  
1 2 3 4 5 6 7
   | import com.odde.mail.model.Recipient; import org.springframework.data.jpa.repository.JpaRepository;
  public interface RecipientRepository extends JpaRepository<Recipient, Long> {
      public Recipient findByEmail(String email); }
   | 
  
如果你的项目里面有自定义的Reponsitory具体实现,则需要做单元测试,这个可以上网自行搜索相关资料。