PowerMock的使用

Posted by Simon Dong on 2018-12-21

0x00 术语

  • UT: Unit Test,单元测试
  • SIT: System Integration Test,集成测试
  • UAT: User Acceptance Test,用户验收测试

0x01 什么是单元测试

单元测试是用于保证在特定的条件或场景的输入下,函数(或方法)的输出符合需求或设计的功能要求,其主要目的是提高代码质量和可维护性。单元测试带来的好处主要有:

  • 适应变更:在需求变更或重构代码后,确保已有的模块仍然正确工作。如果变更导致了错误发生,单元测试能够快速定位错误
  • 简化集成:单元测试在一定程度上消除了单元代码的不可靠性,通过测试单个代码构造来保证集成后的软件可靠性。
  • 文档记录:单元测试在一定程度上体现了需求功能,并可以直观的展现函数(方法)是如何被调用。
  • 表达设计:单元测试数据集的构造和使用,能在一定程度上表达该函数对数据集处理的设计思想。

单元测试的方法主要有:

  1. 条件判断法:注重对方法中的逻辑条件的检查和测试,目的是测试代码中的条件错误。
  2. 数据划分法:注重对有效或无效数据集输入的测试,目的是测试代码对无效数据集的处理是否健壮。
  3. 边界值法:是数据划分法的特例,注重测试代码对于边界值和边界值两侧的输入是否能够正确处理。一般来说,边界值有“最小值”、略高于最小值、正常值、略低于最高值、最高值等几种取值方式。

单元测试编写的基本准则:可参考https://petroware.no/unittesting.html , 重要的有

  • 对测试进行评估(Measure the tests) : 检查测试用例的代码覆盖率
  • 立即修复失败的测试用例(Fix failing tests immediately):团队协同和持续集成的要求
  • 由简入繁(Start off simple):对于一个类或方法的测试,应先搭建测试骨架,然后一步步填充不同场景或数据集进行条件类型的测试。
  • 保持测试的独立性(Keep test independent):测试用例之间应相互独立,不可相互依赖。对于被测试类可能在其内部维护状态变量时,应特别注意这一点。在测试用例执行过程中,被测类的内部变量可能因测试而改变。
  • 合理命名测试用例(Name tests properly):将测试类命名为\test,测试方法命名为test\或\
  • 覆盖边界值(Cover boundary cases):确保边界值都被测试
  • 提供负向测试(Provide negative tests):负向测试是指提供有问题的输入,来验证代码的可靠性和是否能正确处理错误输入。
  • 为每个Bug编写测试用例(Write tests to reproduce bugs):在修复Bug前,为每个Bug编写能重现该Bug的测试用例,以确保在Bug修复后,测试用例能正确通过。

0x02 Mock

mock是什么

mock是模拟对象,用于模拟真实对象的行为。

mock测试的含义就是在测试过程中,对于某些不容易构造或者与环境相关的对象,模拟其输入输出以便测试的测试方法。mock对象就是真实对在测试期间的替代品。

例如:当测试类A时,类B需要读取数据库中的数据,类C是一个具有复杂逻辑的类,那么此时我们创建类B和类C的Mock对象进行测试

Mock的关键点

Mock对象: 模拟对象的概念就是我们想要创建一个可以替代实际对象的对象,这个模拟对象要可以通过特定参数调用特定的方法,并且能返回预期结果。

Stub(桩):桩指的是用来替换具体功能的程序段。桩程序可以用来模拟已有程序的行为或是对未完成开发程序的一种临时替代。

设置预期: 通过设置预期明确 Mock 对象执行时会发生什么,比如返回特定的值、抛出一个异常、触发一个事件等,又或者调用一定的次数。

比如上图的类B有如下代码

1
2
3
4
5
public class ClassB{
public int getOrder(int id){
return DbReader.readOrder(id);
}
}

使用PowerMock的代码

1
2
3
4
//Mock对象
ClassB mockB = mock(ClassB.class);
//Stub
when(mockB.getOrder(anyInt())).thenReturn(3);

Mock的好处

  • TDD(测试驱动开发): 可以根据需求提前创建测试
  • 隔离模块: 在软件开发中,为了避免代码的紧耦合和提高可扩展性,一般都会采用分层架构进行设计,单一模块仅关注单一功能的实现,Mock可以模拟本模块依赖的其它的模块。
  • 解耦资源限制:在某些代码需要访问外部资源(如数据库、文件系统、网络)等情况下,Mock可以模拟这些操作从而解除对外部资源的依赖。

Java的Mock框架

  • EasyMock是早期比较流行的Mock测试框架,它提供对接口的模拟,能够通过录制、回放、检查三步来完成测试过程,可以验证方法的调用、次数和顺序,可以令Mock对象返回指定的值或抛出异常。
  • mockito是在EasyMock之后流程的mock工具,相对EasyMock来说,mockito的API更简洁,学习曲线低,测试代码的可读性强,并且mockito增加了局部模拟doCallRealMethod()spy()
    • mock: 生成的类中所有的方法都是被mock的方法,通过when...thenReturn指定返回值
    • spy: 生成的类中所有的方法都是实际的方法(执行过程中运行方法中的代码),但返回的值由when...thenReturn指定
  • mockito2mockito的升级级,提供了更简洁的API语法
    • mock底层引擎由CGLIB更改为ByteBuddy
    • Java8兼容
  • PowerMock在是EasyMockmockito基础上扩展的,因此PowerMock有两个可选的版本,分别是基于EasyMock和基于mockitoPowerMock解决了对staticfinalprivate等方法的mock。

以下是Java各种Mock框架的特性比较

Java 各种Mock框架的特性比较

0x03 配置

Maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.7.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.7.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>

0x04 基础使用

一般步骤

  1. 使用PowerMockito.mock(Class<T> clazz)创建需要被mock的对象实例。一般来说,mock的对象是被测类中所依赖的其它类的实例。
  2. 对Mock的对象,使用when...thenReturn或者doNothing().when(...).<method>(...)或者doThrow(Throwable).when(...).<method>(...)创建桩(Stub)。
  3. 在单元测试中,使用Junit.Assert中相应的方法验证返回值是否与期望一致,或者使用PowerMockito.verify(<mockObject>, times(int)).<method>(...)验证Mock对象中相应方法被调用的次数。
  4. 如果一个对象,仅需mock它的部分方法,而其它方法希望与其实对象的行为一致,可以使用PowerMockito.spy(Class<T> clazz)代替PowerMockito.mock(Class<T> clazz),需要mock的方法通过第2步的when...thenReturn之类的方式。
  5. 若需要在运行过程中修改mock对象的私有属性值,可以使用:
    • mockito 1.0: Whitebox.setInternalState(mockObject, "fieldName", fieldValue)
    • mockito 2.0:FieldSetter.setField(mockObject, originFieldValue, newFieldValue)

方法参数通配符

​ 在测试过程,不需要关注Mock对象的方法输入时,可以使用anyIntanyStringany(Class<T> clazz)等方法通配符,可参考org.mockito.Matchers类中相关的方法。

​ 若Mock的方法中,仅关注其中某几个参数的输入值时,可使用eq(<parameterValue>)作为通配符。

PowerMock结合Junit的骨架代码

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
@RunWith(PowerMockRunner.class)
@PrepareForTest({StaticBean.class})
public class ExampleTest {

@Mock
private mockBean mockBean;

@Spy
private SpyBean spyBean;

@InjectMocks
private TestBean testBean;

@BeforeClass
public static void beforeClass() {

}

@Before
public void before() {

}

@Test
public void test() {

}

@After
public void after() {

}


@AfterClass
public static void afterClass() {

}
}
  • @RunWith: 是Junit提供的扩展接口,将具体的测试类代理给RunWith指定的运行器执行。
  • @PrepareForTest: 对final classinner class或指定的class中有静态方法或native方法进行mock准备。这个注解可同时用于类和方法。与@PrepareOnlyThisForTest的区别在于@PrepareForTest所mock的包括父类所含有的上述类型方法或类。
  • @Mock: 对相应的类进行Mock操作
  • @Spy: 对相应的类进行Spy操作
  • @InjectMocks: 在被测试的对象中注入@Mock@Spy的生成的对象(如果被测试的对象有相应的属性)
  • @BeforeClass: 这是Junit的注解,相当于类中static代码块,在测试类初始化的时候执行且仅执行一次
  • @Before: 在执行每个@Test标注的方法之前,执行本方法
  • @After:在执行第个@Test标注的方法之后,执行本方法
  • @AfterClass:在所有@Test方法执行后,执行本方法且仅执行一次,相当于测试类的析构方法。

一个Junit单元测试在运行过程的顺序:

@BeforeClass ->@Before -> @Test -> @After -> @AfterClass

其中,每一个@Test运行时,有

@Before -> @Test -> @After

一般来说,在@Before中,准备通用的Mock桩数据和一些资源,而在@After中可以重置桩数据或释放资源。

0x05 测试示例

我们将以典型SSM项目作为示例代码说明单元测试如何编写

Employee

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Employee {
private String username;

public Employee(String username) {
this.username = username;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}
}

EmployeeDao

1
2
3
4
5
6
7
8
9
@Repository
public interface EmployeeDao {

Employee getEmployeeById(String id) ;

List<Employee> getAll();

void save(Employee employee);
}

SapDao

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
@Repository
public class SapDao {

@Autowired
private JdbcTemplate jdbcTemplate;

@Transactional(readOnly = true, rollbackFor = Exception.class)
public Integer countAccountsById(String id) {
String sql = "SELECT count(*) FROM sap WHERE id=?";
return countAccountRealMehtod(sql, id);
}

public Integer countAccountRealMehtod(String sql, String id) {
Object[] parameters = new Object[1];
parameters[0] = id;
Integer count = jdbcTemplate.queryForObject(sql, parameters, new RowMapper<Integer>() {
@Override
public Integer mapRow(ResultSet rs, int rowNum) throws SQLException {
while (rs.next()) {
return rs.getInt(1);
}
return 0;
}
});
return count;
}
}

EmployeeService

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
@Service
public class EmployeeService {

@Autowired
private EmployeeDao employeeDao;

@Autowired
private SapDao sapDao;

public Employee getEmployeeById(String id) {
if (id == null || id.trim().isEmpty()) {
throw new IllegalArgumentException("Illegal employee id");
}
return employeeDao.getEmployeeById(id);
}

public List<Employee> getAll() {
return employeeDao.getAll();
}

public void save(Employee employee) {
employeeDao.save(employee);
}

public boolean isSapAccountExist(String id) {
return sapDao.countAccountsById(id) > 0;
}

public String getEmployeeAsJson(String id) {
Employee employee = this.getEmployeeById(id);
return JsonUtils.toJson(employee);
}
}

JsonUtils

1
2
3
4
5
6
7
8
9
public final class JsonUtils {

public static String toJson(Employee employee) throws IllegalFormatException {
if (employee == null) {
throw new RuntimeException("null employee");
}
return employee.toString();
}
}

通常情况下,SSM的应用的存储层(DAO层)一般都是以接口形式存在,不需要进行测试。

EmployeeService的单元测试

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import static org.powermock.api.mockito.PowerMockito.doNothing;
import static org.powermock.api.mockito.PowerMockito.doReturn;
import static org.powermock.api.mockito.PowerMockito.doThrow;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;

/**
* 示例测试类
* JsonUtils是final class,并调用了其static method
* 因此需要使用@PreparForTest声明
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest({JsonUtils.class})
public class EmployeeServiceTest {

/**
* Mock对象
* 所有的访问都代理至PowerMock内部
* 所有的方法在默认情况下都返回null
*/
@Mock
private EmployeeDao employeeDao;

/**
* Spy对象
* Spy的对象是真实对象
* 只有doReturn...when所代理的方法会返回期望值
* 其实方法返回真实运行结果
*/
@Spy
private SapDao sapDao = new SapDao();

/**
* 被测试类
* 当类中有Mock对象或Spy对象所对应的引用时,自动注入
*/
@InjectMocks
private EmployeeService employeeService;

/**
* @Before方法
* 在每个Test运行之前运行
*/
@Before
public void before() {
//预备测试用数据
List<Employee> employees = new ArrayList<>();
employees.add(new Employee("User01"));

//Stub(桩),调用Mock或Spy对象时,期望的返回值
when(employeeDao.getAll()).thenReturn(employees);
when(employeeDao.getEmployeeById(anyString())).thenReturn(null);
}


@Test
public void testGetEmployeeById() {
//Stub,覆盖@Before中的对应Stub,
// 多个thenReturn按照employeeDao.getEmployeeById()的调用顺序返回,
when(employeeDao.getEmployeeById(anyString()))
.thenReturn(new Employee("User02"))
.thenReturn(null);

//返回第一个thenReturn的结果
Employee employee = employeeService.getEmployeeById("02");
assertEquals("User02", employee.getUsername());
//返回第二个thenReturn的结果
assertNull(employeeService.getEmployeeById("02"));
//无其它thenReturn,返回最后一个ThenReturn结果(第二个thenReturn)
assertNull(employeeService.getEmployeeById("02"));
}

/**
* 期望被测方法抛出异常时,使用@Test(expected=Class<T implements Throwable>
*/
@Test(expected = IllegalArgumentException.class)
public void testGetEmployeeByIllegalId() {
employeeService.getEmployeeById(null);
}


@Test
public void testGetAll() {
List<Employee> allEmployees = employeeService.getAll();
assertEquals(1, allEmployees.size());
}

/**
* 使用doNothing()方法控制void返回值方法
* 使用verify方法验证Mock对象中内部方法的是否被调用,调用的次数是多少
* 使用verifyStatic可验证静态方法
*/
@Test
public void testSave() {
doNothing().when(employeeDao).save(any(Employee.class));
employeeService.save(new Employee("User03"));
verify(employeeDao, Mockito.times(1)).save(any(Employee.class));
}

/**
* 对Spy对象中的方法打桩,应使用doReturn...when...method的方式
*/
@Test
public void testSapAccountExisted() {
doReturn(1).when(sapDao).countAccountsById(eq("1"));
assertTrue(employeeService.isSapAccountExist("1"));
}

/**
* 静态方法的单元测试
*/
@Test
public void testGetEmployeeAsJson(){
//对静态方法的Mock和Stub
mockStatic(JsonUtils.class);
when(JsonUtils.toJson(any(Employee.class))).thenReturn("");

//getEmployeeAsJson首先调用EmployeeService.getEmployeeId,这一方法调用了EmployeeDao.getEmployeeId
//因此,EmployeeDao.getEmployeeId也需要Stub
when(employeeDao.getEmployeeById(anyString())).thenReturn(new Employee("User01"));

assertEquals("", employeeService.getEmployeeAsJson("1"));
}

/**
* 模拟Mock或Spy对象方法的异常抛出
*/
@Test(expected = RuntimeException.class)
public void testSapAccountExistedException(){
doThrow(new RuntimeException("throw via unit test")).when(sapDao).countAccountsById(anyString());

employeeService.isSapAccountExist("2");
}

/**
* 模拟静态方法的异常抛出
*/
@Test(expected = IllegalArgumentException.class)
public void testGetEmployeeAsJson3() throws Exception {
//模拟抛出异常
mockStatic(JsonUtils.class);
doThrow(new IllegalArgumentException("throw via unit test")).when(JsonUtils.class, "toJson", any(Employee.class));

when(employeeDao.getEmployeeById(anyString())).thenReturn(new Employee("User01"));

employeeService.getEmployeeAsJson("1");
}

/**
* 本方法本应在SapDaoTest类中,为了说明方便,特列于此
* 本测试展示 when...thenAnswer的用法,对于较复杂的返回模拟可以用此方法
* doAnswer模拟返回了一个RowMapper的应答。
*/
@Test
public void testSapDaoCountAccountRealMehtod(){
JdbcTemplate template = mock(JdbcTemplate.class);

//由于sapDao是一个Spy对象,其在初始化时未注入jdbcTemplate,同时它也未提供相应的
//Getter/Setter方法,因此需要使用Whitebox直接修改类内部变量的值
//Whitebox同时可用于Mock对象
Whitebox.setInternalState(sapDao, "jdbcTemplate", template);

when(template.queryForObject(anyString(), any(Object[].class), any(RowMapper.class))).thenAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
/**
* invocation是被模拟方法Mock相关参数
*/
Object[] args = invocation.getArguments();
RowMapper<Integer> rm = (RowMapper<Integer>) args[2];
ResultSet rs = mock(ResultSet.class);
when(rs.getInt(anyInt())).thenReturn(0);
//此处调用了SapDao中实现的mapRow方法
Integer value = rm.mapRow(rs, 1);
return value;
}
});
assertFalse(employeeService.isSapAccountExist("1"));
}
}

基本上,PowerMock常用的方式都已在此测试类中了。

0x06 Reference

  1. 单元测试

  2. Unit Testing Guidelines

  3. PowerMockito 快速上手记要

  4. Mock 模拟测试简介及 Mockito 使用入门

  5. 软件架构模式之分层架构

  6. java的mock测试框架

  7. JUnit4中@AfterClass @BeforeClass @After @Before的区别对比