SpringMVC 整合 Mockito单元测试

不积跬步无以至千里,不积小流无以成江海

一、背景

  1. 在日常开发编码中,经常会遇到这样的场景:开发接口的过程中,有部分数据依赖同事开发的接口,需要等到同事的接口开发完成以后,才能开始我们的功能开发。这样依赖同事,有时候会影响到项目的上线节奏,所以我们在寻找有没能够解决这种场景的工具。
  2. 或者说,我们在分析完业务需求后,设计验证大的业务流程的代码的时候,也可以用到此类工具。首先把整体的大的处理逻辑完成,把细枝末节的处理打上标记 ToDo。
  3. Mockito 可以帮助我们在编写单元测试类的时候,调用指定接口返回指定的数据信息。通过模拟数据,替代真实数据,这样我们不需要等到同事的接口开发完成后,再开始我们的工作,做到“并行开发”。

二、实施过程

1. 引入依赖

1
2
3
4
5
6
7
<!-- 引入mockito 依赖 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>

2. CURD Spring MVC 业务代码(略)

3. 基于 Mockito 的单元测试基类

  1. 一般使用项目中已经写好的测试基类即可。
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
@RunWith(SpringJUnit4ClassRunner.class)//表示整合JUnit4进行测试
@ContextConfiguration(locations={
"classpath:spring/applicationContext.xml",
"classpath:spring/applicationContext-persistent.xml"})//加载spring配置文件
public class BaseSpringTest {
private Gson gson = new GsonBuilder().setPrettyPrinting().create();

/**
* 控制台打印展示对象 Json 字符串信息
*/
public void printlnJson(Object object) {
System.out.println(gson.toJson(object));
}

@Autowired
private UserService userService;

/**
* 设置上下文登录用户数据
*/
public LoginUser mockLoginUser(String userId){
LoginUser loginUser = new LoginUser();

UserBaseV userBaseV = userService.queryOne(userId);

User user = new User();

BeanUtil.copyProperties(user, userBaseV);
// 绑定指定用户
UserUtil.currentUser.set(user);

PreconditionsUtil.checkNotNull(userBaseV, "查询失败, 未查询到对应的用户数据");

loginUser.setId(userBaseV.getId());
loginUser.setLoginName(userBaseV.getUsername());
loginUser.setDealerId(userBaseV.getDealerId());
loginUser.setOrgId(userBaseV.getOrganizationId());
loginUser.setUserName(userBaseV.getName());

return loginUser;
}
}

4. Service 对应的 Mockito 测试类

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
import com.google.common.collect.Lists;
import com.google.gson.Gson;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

/**
* demo
*/
public class MockitoBusinessTypeTest extends BaseMockitoSpringTest {
@InjectMocks
private BusinessTypeService businessTypeService = mock(BusinessTypeService.class);
@Mock
private IvrDealerSettingService ivrDealerSettingService;

/**
* 上边使用 InjectMocks 注释的类,里面如果有依赖到的 service 可以通过下面的方法,
* Mock 注解,会自动为上边那个类注入下面这个service
*/
@Mock
private LOVService lovService;

@Autowired
private SeatService seatService;

@Before
public void initMock(){
MockitoAnnotations.initMocks(this);
doReturn(Lists.newArrayList()).when(businessTypeService).getSatisfactionBusinessType();
doReturn("hello world").when(ivrDealerSettingService).getSkillGroupByDealerAndIvrKey("a", "b", "c");

BusinessTypeV businessTypeResult = new BusinessTypeV();
businessTypeResult.setCode("11");
doReturn(businessTypeResult).when(businessTypeService).getUserDefaultBusinessType("system");
}

@Test
public void queryTest(){
List<BusinessTypeLOV> list = businessTypeService.getSatisfactionBusinessType();
String ivrResult = ivrDealerSettingService.getSkillGroupByDealerAndIvrKey("a", "b", "c");
System.out.println(new Gson().toJson(list));
System.out.println(ivrResult);
}

/**
* mock with autowired
*/
@Test
public void seatQueryTest(){
String businessType = businessTypeService.getUserDefaultBusinessType("system").getCode();
// 结果是可以正常执行的
printlnJson(seatService.selectByBusinessType(businessType));
}
}

5. Controller 对应的 Mockito 测试类

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
/**
* Controller 测试类
*/
@RunWith(MockitoJUnitRunner.class)
public class MockitoBusinessTypeControllerTest {

private MockMvc mockMvc;

@InjectMocks
private BusinessTypeController businessTypeController;
@Mock
private BusinessTypeService businessTypeService;
@Mock
private ClusterMessageListener clusterMessageListener;

@Before
public void initMock() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(businessTypeController).build();
}


@Test
public void getBusinessInfoTest() throws Exception {
// mock Service 方法行为
BusinessTypeV entity = new BusinessTypeV();
entity.setCode("11");
when(businessTypeService.queryOne(Mockito.any())).thenReturn(entity);

// 模拟接口调用
mockMvc.perform(get("/rest/businessType/all/getBusinessInfo")
.param("businessType", "11")
)
.andDo(print())
.andExpect(status().isOk());
}
}

6. 注解的说明

首先是Spring的几个Annotate:

  1. RunWith(SpringJUnit4ClassRunner.class): 表示使用Spring Test组件进行单元测试;
  2. WebAppConfiguration: 使用这个Annotate会在跑单元测试的时候真实的启一个web服务,然后开始调用Controller的Rest API,待单元测试跑完之后再将web服务停掉;
  3. ContextConfiguration: 指定Bean的配置文件信息,可以有多种方式,这个例子使用的是文件路径形式,如果有多个配置文件,可以将括号中的信息配置为一个字符串数组来表示;

然后是Mockito的Annotate:

  1. Mock: 如果该对象需要mock,则加上此Annotate;
  2. InjectMocks: 使mock对象的使用类可以注入mock对象,在上面这个例子中,mock对象是UserService,使用了UserService的是UserController,所以在Controller加上该Annotate;

Setup方法:

  1. MockitoAnnotations.initMocks(this): 将打上Mockito标签的对象起作用,使得Mock的类被Mock,使用了Mock对象的类自动与Mock对象关联。
  2. mockMvc: 细心的朋友应该注意到了这个对象,这个对象是Controller单元测试的关键,它的初始化也是在setup方法里面。

测试用例:

  1. mockMvc.perform: 发起一个http请求。
  2. post(url): 表示一个post请求,url对应的是Controller中被测方法的Rest url。
  3. param(key, value): 表示一个request parameter,方法参数是key和value。
  4. andDo(print()): 表示打印出request和response的详细信息,便于调试。
  5. andExpect(status().isOk()): 表示期望返回的Response Status是200。
  6. andExpect(content().string(is(expectstring)): 表示期望返回的Response Body内容是期望的字符串。

三、额外

Java 单元测试 PowerMock
文章对 PowerMock 做了非常详尽的讲解,非常推荐。
PowerMock也是依赖 Mockito,所以我认为这两个框架用起来的差别应该不大。