封面来源:本文封面来源于网络,如有侵权,请联系删除。

本文参考:汪文君 2016 PowerMock 实战

本文涉及的代码:mofan212/unit-test-framework-study

官方地址:PowerMock GitHub

  • 本文依赖 JDK 17
  • 截止 2023-11-09,PowerMock 已有三年未更新,其支持的 Mockito 版本十分过时,同时 Mockito 不断推出的新特性也逐渐替代了 PowerMock
  • 不再推荐使用 PowerMock,而是拥抱 Mockito

1. 什么是 PowerMock

PowerMock 是一个单元测试模拟框架,它是在其它单元测试模拟框架的基础上做出的扩展。通过提供定制的类加载器以及一些字节码篡改技巧的应用,PowerMock 现了对静态方法、构造方法、私有方法以及 final 方法的模拟支持,对静态初始化过程的移除等强大的功能。因为 PowerMock 在扩展功能时完全采用和被扩展的框架相同的 API,,熟悉 PowerMock 所支持的模拟框架的开发者会发现 PowerMock 非常容易上手。PowerMock 的目的就是在当前已经被大家所熟悉的接口上通过添加极少的方法和注释来实现额外的功能。

PowerMock 现已支持许多单元测试框架,比如:Mockito、EasyMock、TestNG 等等。

本文使用的 PowerMock 主要建立在对 Mockito 的拓展,更多 Mockito 知识,可在本站搜索【Mockito 实战】查看。

简单来说

  1. PowerMock 不是重复发明的轮子,它是对其他轮子的拓展延伸(类似于 MyBatis 与 MyBatis-Plus);
  2. PowerMock 完成了其他 Mock 框架不能完成的工作;
  3. 尽量减少 PowerMock 的是使用,应该多考虑是不是代码的设计问题。当然,如果非得使用,还是该用就用。

2. PowerMock 的基本用法

2.1 项目准备

导入依赖

在本文中,将使用 Mockito 与 PowerMock 相结合,因此导入 Mockito 与 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
40
41
<dependencies>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>

<!-- JDK 17 需要进行以下配置 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.time=ALL-UNNAMED
--add-opens java.base/java.time.format=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
</argLine>
</configuration>
</plugin>
</plugins>
</build>

项目搭建

创建一个空的 User 实体类:

1
2
3
4
5
/**
* @author mofan 2020/12/22
*/
public class User {
}

创建一个 UserDao 类,并模拟数据库失效:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author mofan 2020/12/22
*/
public class UserDao {

public int getCount() {
throw new UnsupportedOperationException();
}

public void insertUser(User user) {
throw new UnsupportedOperationException();
}
}

最后创建一个 UserService,调用 UserDao 中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author mofan 2020/12/22
*/
public class UserService {

private UserDao userDao;

public UserService(UserDao userDao) {
this.userDao = userDao;
}

public int queryUserCount() {
return userDao.getCount();
}

public void saveUser(User user) {
userDao.insertUser(user);
}
}

2.2 基本使用

编写一个测试类,对前面编写的 UserService 进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author mofan 2020/12/22
*/
public class UserServiceTest {

private UserService userService;

@Before
public void setup() {
userService = new UserService(new UserDao());
}

@Test
public void testQueryUserCountWithJunit() throws Exception{
int count = userService.queryUserCount();
assertEquals(0, count);
}

@Test
public void testSaveUserWithJunit() throws Exception{
userService.saveUser(new User());
}
}

由于 UserDao 中的方法都是直接抛出了 UnsupportedOperationException 异常,所以上述代码测试肯定是不能通过的,并且控制台也会出现异常 UnsupportedOperationException

解决方法也很简单,只需要捕获并断言就可以了:

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
public class UserServiceTest {

private UserService userService;

@Before
public void setup() {
userService = new UserService(new UserDao());
}

@Test
public void testQueryUserCountWithJunit() {
try {
int count = userService.queryUserCount();
Assert.fail("should not process to here");
} catch (Exception e) {
Assert.assertTrue(e instanceof UnsupportedOperationException);
}

}

@Test
public void testSaveUserWithJunit() {
try {
userService.saveUser(new User());
Assert.fail("should not process to here");
} catch (Exception e) {
Assert.assertTrue(e instanceof UnsupportedOperationException);
}
}
}

使用 Mock

那如果是要使用 mock 对象的方式来进行测试,应该怎么做呢?

1
2
3
4
5
6
7
8
9
10
11
12
@Mock
private UserDao userDao;

@Test
public void testQueryUserCountWithMockito() {
MockitoAnnotations.initMocks(this);
UserService userService = new UserService(userDao);
Mockito.when(userDao.getCount()).thenReturn(10);

int result = userService.queryUserCount();
Assert.assertEquals(10, result);
}

上面这种是 Mockito 的方式,有关 Mockito 其他内容,可在本站搜索【Mockito 实战】查看。

那如果是 PowerMock 呢?

1
2
3
4
5
6
7
8
9
@Test
public void testQueryUserCountWithPowerMock() {
UserDao userDao = PowerMockito.mock(UserDao.class);
PowerMockito.when(userDao.getCount()).thenReturn(10);
UserService userService = new UserService(userDao);

int result = userService.queryUserCount();
Assert.assertEquals(10, result);
}

针对无返回值方法的测试,可以这么写:

1
2
3
4
5
6
7
8
9
10
@Test
public void testSaveUserWithPowerMock() {
UserDao userDao = PowerMockito.mock(UserDao.class);
User user = new User();
PowerMockito.doNothing().when(userDao).insertUser(user);
UserService userService = new UserService(userDao);
userService.saveUser(user);
// 验证 insertUser() 方法被调用一次
Mockito.verify(userDao).insertUser(user);
}

如果学习过 Mockito 的话,会发现 PowerMock 和 Mockito 的语法差不多。

这也很正常,毕竟 PowerMock 相当于是对 Mockito 进行了增强。

那 PowerMock 对 Mockito 到底进行了哪些增强呢?

3. PowerMock 的增强

3.1 局部变量

使用 PowerMock 甚至可以 mock 局部变量,怎么做呢?

先对 UserService 改造一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author mofan 2020/12/22
*/
public class UserService {
public int queryUserCount() {
UserDao userDao = new UserDao();
return userDao.getCount();
}

public void saveUser(User user) {
UserDao userDao = new UserDao();
userDao.insertUser(user);
}
}

UserDao 不变!

使用 Mockito 无法 mock 局部变量,但是 PowerMock 就可以。

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
/**
* @author mofan 2020/12/23
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest(UserService.class)
public class LocalUserServiceTest {

@Test
public void testQueryUserCount() {
try {
UserService userService = new UserService();
UserDao userDao = PowerMockito.mock(UserDao.class);
// 使用无参构造函数 new 一个 UserDao 时,总是会返回 mock 出的 userDao
PowerMockito.whenNew(UserDao.class).withNoArguments().thenReturn(userDao);
PowerMockito.doReturn(10).when(userDao).getCount();

int result = userService.queryUserCount();
Assert.assertEquals(10, result);
} catch (Exception e) {
Assert.fail();
}
}

@Test
public void testSaveUser() {
try {
User user = new User();
UserService userService = new UserService();
UserDao userDao = PowerMockito.mock(UserDao.class);

PowerMockito.whenNew(UserDao.class).withAnyArguments().thenReturn(userDao);
PowerMockito.doNothing().when(userDao).insertUser(user);

userService.saveUser(user);
Mockito.verify(userDao, Mockito.times(1)).insertUser(user);
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行上述代码,测试可以通过!

需要注意的是,使用 PowerMock mock 局部变量时,请在测试类上使用注解:

  • @RunWith(PowerMockRunner.class)

  • @PrepareForTest(xxxx):填写需要 mock 的 new 对象代码所在的类。该注解告诉 PowerMock 将列出的类在字节码级别上进行操作。

@PrepareForTest 的使用场景

重点! 重点! 重点!

  1. 当使用 PowerMockito.whenNew() 方法时,必须加注解 @PrepareForTest@RunWith。注解 @PrepareForTest 里写的类是需要 mock 的 new 对象代码所在的类;
  2. 当需要 mock final 方法的时候,必须加注解 @PrepareForTest@RunWith。注解 @PrepareForTest 里写的类是 final 方法所在的类;
  3. 当需要 mock 静态方法的时候,必须加注解 @PrepareForTest@RunWith。注解 @PrepareForTest 里写的类是静态方法所在的类。
  4. 当需要 mock 私有方法的时候,只是需要加注解 @PrepareForTest,注解里写的类是私有方法所在的类。
  5. 当需要 mock 系统类的静态方法的时候,必须加注解 @PrepareForTest@RunWith。注解里写的类是需要调用系统方法所在的类。

3.2 静态方法

使用 PowerMock 甚至还可以 mock 静态方法,这在其他 mock 框架中是无法想象的(Mockito 3.4.1 及其以上版本已经支持)!

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author mofan 2020/12/22
*/
public class UserDao {

public static int getCount() {
throw new UnsupportedOperationException();
}

public static void insertUser(User user) {
throw new UnsupportedOperationException();
}
}

UserService 的改造:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author mofan 2020/12/22
*/
public class UserService {
public int queryUserCount() {
return UserDao.getCount();
}

public void saveUser(User user) {
UserDao.insertUser(user);
}
}

测试代码:

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
/**
* @author mofan 2020/12/23
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest(UserDao.class)
public class StaticUserServiceTest {
@Test
public void testQueryUserCount() {
PowerMockito.mockStatic(UserDao.class);
PowerMockito.when(UserDao.getCount()).thenReturn(10);

UserService userService = new UserService();
int result = userService.queryUserCount();
Assert.assertEquals(10, result);
}

@Test
public void testSaveUser() {
PowerMockito.mockStatic(UserDao.class);
User user = new User();
PowerMockito.doNothing().when(UserDao.class);

UserService userService = new UserService();
userService.saveUser(user);
}
}

3.3 final 修饰的类

使用 PowerMock 甚至还可以 mock final 修饰的类,这在其他 mock 框架中是无法想象的!

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author mofan 2020/12/22
*/
public final class UserDao {

public int getCount() {
throw new UnsupportedOperationException();
}

public void insertUser(User user) {
throw new UnsupportedOperationException();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author mofan 2020/12/22
*/
public class UserService {

private UserDao userDao;

public UserService(UserDao userDao) {
this.userDao = userDao;
}

public int queryUserCount() {
return userDao.getCount();
}

public void saveUser(User user) {
userDao.insertUser(user);
}
}

先使用 Mockito 来试试,看看测试能否通过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author mofan 2020/12/23
*/
public class FinalUserServiceTest {

@Mock
private UserDao userDao;

@Test
public void testQueryUserCountWithMockito() {
MockitoAnnotations.initMocks(this);
UserService userService = new UserService(userDao);
Mockito.when(userDao.getCount()).thenReturn(10);
int result = userService.queryUserCount();
Assert.assertEquals(10, result);
}
}

上述测试方法运行后没有通过,控制台打印出:

org.mockito.exceptions.base.MockitoException: 
Cannot mock/spy class indi.mofan.mockfinal.dao.UserDao
Mockito cannot mock/spy because :
 - final class

Mockito 不能 mock final 修饰的类,那在用 PowerMock 试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author mofan 2020/12/23
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest(UserDao.class)
public class FinalUserServiceTest {

@Mock
private UserDao userDao;

@Test
public void testQueryUserCountWithPowerMock() {
UserDao userDao = PowerMockito.mock(UserDao.class);
PowerMockito.when(userDao.getCount()).thenReturn(10);
UserService userService = new UserService(userDao);
int result = userService.queryUserCount();
Assert.assertEquals(10, result);
}
}

注意注解 @RunWith@PrepareForTest 的使用!

4. Verify 的使用

项目准备

在项目中创建一个名为 verify 的包,包里创建一个名为 UserDao 的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author mofan 2020/12/24
*/
public class UserDao {
public int getCount() {
throw new UnsupportedOperationException();
}

public void insertUser(User user) {
throw new UnsupportedOperationException();
}

public void updateUser(User user) {
throw new UnsupportedOperationException();
}
}

创建一个名为 UserService 的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author mofan 2020/12/24
*/
public class UserService {

public void saveOrUpdate(User user) {
UserDao userDao = new UserDao();

if (userDao.getCount() > 0) {
userDao.updateUser(user);
} else {
userDao.insertUser(user);
}
}
}

测试过程

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
/**
* @author mofan 2020/12/24
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest(UserService.class)
public class UserServiceVerifyTest {

@Test
public void testSaveOrUpdateWillUseNewJoiner() throws Exception {
User user = PowerMockito.mock(User.class);
UserDao userDao = PowerMockito.mock(UserDao.class);

PowerMockito.whenNew(UserDao.class).withNoArguments().thenReturn(userDao);
PowerMockito.when(userDao.getCount()).thenReturn(0);

UserService userService = new UserService();
userService.saveOrUpdate(user);

Mockito.verify(userDao).insertUser(user);
Mockito.verify(userDao, Mockito.never()).updateUser(user);
}

@Test
public void testSaveOrUpdateWillUseUpdateOriginal() throws Exception {
User user = PowerMockito.mock(User.class);
UserDao userDao = PowerMockito.mock(UserDao.class);

PowerMockito.whenNew(UserDao.class).withNoArguments().thenReturn(userDao);
PowerMockito.when(userDao.getCount()).thenReturn(1);

UserService userService = new UserService();
userService.saveOrUpdate(user);

Mockito.verify(userDao, Mockito.never()).insertUser(user);
Mockito.verify(userDao).updateUser(user);
}
}

这里使用的 verify() 是 Mockito 中的方法,verify() 还有其他的用法和作用。

有关 Mockito 和 Mockito 中 verify() 的其他内容,可在本站搜索【Mockito 实战】查看。

5. Mock 构造方法

项目准备

在项目中创建一个名为 construction 的包,包里创建一个名为 UserDao 的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author mofan 2020/12/24
*/
public class UserDao {
private String username;
private String password;

public UserDao(String username, String password) {
this.username = username;
this.password = password;
}

public void insert() {
throw new UnsupportedOperationException();
}
}

创建一个名为 UserService 的类:

1
2
3
4
5
6
7
8
9
10
/**
* @author mofan 2020/12/24
*/
public class UserService {

public void save(String username, String password) {
UserDao userDao = new UserDao(username, password);
userDao.insert();
}
}

测试过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author mofan 2020/12/24
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest(UserService.class)
public class UserServiceConstructionTest {

@Test
public void testSave() throws Exception {
UserDao userDao = PowerMockito.mock(UserDao.class);
String username = "mofan";
String password = "123456";

PowerMockito.whenNew(UserDao.class)
.withArguments(username, password).thenReturn(userDao);
PowerMockito.doNothing().when(userDao).insert();

UserService userService = new UserService();
// 如果 save() 的参数不是 username 和 password,测试将不会通过
userService.save(username, password);
Mockito.verify(userDao).insert();
}
}

mock 构造方法的方式有三种:

  • withNoArguments()
  • withAnyArguments()
  • withArguments()

前文中已经列举了两种,因此在这只列举一种。

6. Arguments Matcher

项目准备

在项目中创建一个名为 matcher 的包,包里创建一个名为 UserDao 的类:

1
2
3
4
5
6
7
8
9
/**
* @author mofan 2020/12/24
*/
public class UserDao {

public String queryByName(String username) {
throw new UnsupportedOperationException();
}
}

创建一个名为 UserService 的类:

1
2
3
4
5
6
7
8
9
10
/**
* @author mofan 2020/12/24
*/
public class UserService {

public String find(String name) {
UserDao userDao = new UserDao();
return userDao.queryByName(name);
}
}

测试过程

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
/**
* @author mofan 2020/12/24
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest(UserService.class)
public class UserServiceMatcherTest {

@Ignore
@Test
public void testFind() throws Exception {
UserDao userDao = PowerMockito.mock(UserDao.class);
PowerMockito.whenNew(UserDao.class).withAnyArguments().thenReturn(userDao);

PowerMockito.when(userDao.queryByName("mofan")).thenReturn("默烦");
UserService userService = new UserService();
String result = userService.find("mofan");
Assert.assertEquals("默烦", result);

// 没有匹配的参数,因此测试不会通过
String yang = userService.find("yang");
Assert.assertEquals("默烦", yang);
}

@Test
public void testFindWithMatcher() throws Exception {
UserDao userDao = PowerMockito.mock(UserDao.class);
PowerMockito.whenNew(UserDao.class).withAnyArguments().thenReturn(userDao);

PowerMockito.when(userDao.queryByName(Mockito.argThat(new MyArgumentMatcher()))).thenReturn("默烦");
UserService userService = new UserService();

Assert.assertEquals("默烦", userService.find("mofan"));
Assert.assertEquals("默烦", userService.find("yang"));
}

static class MyArgumentMatcher implements ArgumentMatcher<String> {
public boolean matches(String s) {
return switch (s) {
case "mofan", "yang" -> true;
default -> false;
};
}
}

}

这里使用的参数匹配器其实是 Mockito 中的知识,

有关 Mockito 中 Arguments Matcher 的其他内容,可在本站搜索【Mockito 实战】查看。

7. Answer 接口

在前文中,通常是一个参数返回一个结果,如果想实现不同的参数返回不同的结果,应该怎么做呢?

虽然使用 Mock 也能做到,但是非常不方便。

这一点在 Mockito 中也说明了,可在本站搜索【Mockito 实战】并在这篇文章中搜索 thenAnswer 查看。

在此不用 thenAnswer(),而是使用 then()

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
@Test
public void testFindWithAnswer() throws Exception {
UserDao userDao = PowerMockito.mock(UserDao.class);
PowerMockito.whenNew(UserDao.class).withAnyArguments().thenReturn(userDao);

PowerMockito.when(userDao.queryByName(Mockito.anyString())).then(invocation -> {
String arg = (String) invocation.getArguments()[0];
return switch (arg) {
case "mofan" -> "I am mofan.";
case "yang" -> "I am Yang.";
default -> throw new RuntimeException("Not support " + arg);
};
});

UserService userService = new UserService();
Assert.assertEquals("I am mofan.", userService.find("mofan"));
Assert.assertEquals("I am Yang.", userService.find("yang"));

try {
String anyValue = userService.find("anyValue");
Assert.fail("never process to here is right.");
} catch (Exception e) {
Assert.assertTrue(e instanceof RuntimeException);
}
}

8. Spy

mock 出来的对象是代理对象,调用这个对象的方法并不是原对象的方法;而 spy 出来的对象也是代理对象,但是调用这个对象的方法会执行原方法。

具体内容可在本站搜索【Mockito 实战】并在这篇文章中搜索 Mockito Spying 查看。

举个 🌰,现有这样一个 UserService 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author mofan 2020/12/24
*/
public class UserService {

public void foo() {
log();
}

private void log() {
System.out.println("I am console log.");
}
}

先 mock 一个 UserService 对象并调用 foo() 方法:

1
2
3
4
5
6
7
8
9
10
11
/**
* @author cheny 2020/12/24
*/
public class UserServiceSpyTest {

@Test
public void testFoo() {
UserService userService = PowerMockito.mock(UserService.class);
userService.foo();
}
}

运行这个测试方法后,测试可以通过,但是并不会在控制台上打印出任何字样。

再试试 spy 一个 UserService 对象并调用 foo() 方法:

1
2
3
4
5
@Test
public void testFoo() {
UserService userService = PowerMockito.spy(new UserService());
userService.foo();
}

运行这个测试方法后,测试可以通过,控制台打印出:

I am console log.

证明前面说的并没有错!

那这有什么用呢?

使用 spy 可以实现当不满足 Stubbing 时,执行真实方法。再举个 🌰 :

修改一下 UserService

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author mofan 2020/12/24
*/
public class UserService {

public void foo(String arg) {
log();
}

private void log() {
System.out.println("I am console log.");
}
}

再修改测试方法:

1
2
3
4
5
6
7
8
9
10
@Test
public void testFoo() {
UserService userService = PowerMockito.spy(new UserService());
System.out.println(userService);
String arg = "hello";
PowerMockito.doNothing().when(userService).foo(arg);

userService.foo(arg);
// userService.foo("world");
}

测试能够通过,控制台打印出:

indi.mofan.spy.UserService$MockitoMock$580761216@6a6cb05c

并不会使用真实方法,如果放开最后一句的注释,控制台打印出:

indi.mofan.spy.UserService$MockitoMock$1761583206@6a6cb05c
I am console log.

那这又有什么用呢?

8.1 私有方法的 Mock

再修改一下 UserService

1
2
3
4
5
6
7
8
9
10
public class UserService {

public boolean exist(String username) {
return checkExist(username);
}

private boolean checkExist(String username) {
throw new UnsupportedOperationException();
}
}

编写测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testCheck() throws Exception {
UserService userService = PowerMockito.spy(new UserService());

/*
* 下述 when() 方法参数解析:
* 第一个参数: 对象
* 第二个参数: 对象的方法
* 第三个及以后的参数: 方法的参数列表
*/
PowerMockito.doReturn(true).when(userService, "checkExist", "mofan");
Assert.assertTrue(userService.exist("mofan"));

try {
userService.exist("any");
Assert.fail();
} catch (Exception e) {
Assert.assertTrue(e instanceof UnsupportedOperationException);
}
}

小总结

  • 如果 spy 对象方法的调用不满足 Stubbing,将会执行真实的方法,否则就按照 Stubbing 执行。

  • 注意 when() 方法的使用。如果指定的对象方法的参数列表和 when() 方法中指定的参数列表不同,运行测试方法后将会报错:

    org.powermock.reflect.exceptions.MethodNotFoundException:
    

9. Whitebox 的使用

9.1 Whitebox 的介绍

在 PowerMock 中提供了一个名为 Whitebox 的工具类,使用这个工具类,可以绕过封装,注入或查看对象的私有属性以及执行对象中的某个方法(包括私有方法)。

使用 Whitebox 可以设置 privatestaticfinal 域的值,并且不用添加到 @PrepareForTest 注解中。

当然,获取或修改一个非 public 字段不是一个好主意,但这是通过测试覆盖代码以供将来重构的唯一方法。

常见的一些方法

设置某个对象中某个字段的值:

1
Whitebox.setInternalState(Object object, String fieldname, Object value)

设置某个类中静态字段的值:

1
Whitebox.setInternalState(Class  clazz, String fieldname, Object value)

获取某个对象中某个字段的值:

1
Whitebox.getInternalState(Object obj, String fieldName)

执行某个对象中的某个方法(包括私有方法):

1
Whitebox.invokeMethod(Object instance, String methodToExecute, Object... arguments)

执行某个类中的静态方法(包括私有静态方法):

1
Whitebox.invokeMethod(Class<?> clazz, String methodToExecute, Object... arguments)

9.2 内部属性值的设置和获取

属性值的获取

假设现在有这样第一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author mofan
* @date 2021/4/12 17:01
*/
public class ServiceHolder {

private final Set<Object> objectSet = new HashSet<>();

public void addService(Object service) {
objectSet.add(service);
}

public void removeService(Object service) {
objectSet.remove(service);
}
}

然后调用 addService() 方法向 Set 中添加元素,但添加元素后怎么知道此时 Set 内部元素情况呢?

这个时候可以使用 Whitebox.getInternalState() 获取属性值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @author mofan
* @date 2021/4/12 14:18
*/
public class WhiteBoxTest {

@Test
public void testGetInternalState() {
ServiceHolder holder = new ServiceHolder();
final Object obj = new Object();

holder.addService(obj);

// 方式一:获取 holder 中 objectSet 属性值
Set<String> objectSet = Whitebox.getInternalState(holder, "objectSet");
assertEquals(1, objectSet.size());
assertSame(obj, objectSet.iterator().next());

// 方式二:在 PowerMock 1.0 及其以上的版本,还可以这样获取属性值
Set<String> set = Whitebox.getInternalState(holder, Set.class);
assertEquals(1, set.size());
assertSame(obj, set.iterator().next());
}
}

第二种获取内部属性状态的方法更加安全,也是首选方式,但是如果在对象中有多个类型一样的属性,那就不得不使用方式一。

属性值的设置

假设现在有这样一个实体类:

1
2
3
4
5
6
7
8
/**
* @author mofan
* @date 2021/4/12 17:22
*/
@Getter
public class Animal {
private String name;
}

在这个实体类,只有一个私有属性 name,并且还没有提供相应的 Setter 方法,那怎么为 name 属性设置一个值呢?

可以使用 Whitebox.setInternalState() 方法来为内部属性赋值。

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testSetInternalState() {
Animal animal = new Animal();
// 方式一:为 animal 中的 name 属性设置值
Whitebox.setInternalState(animal, "name", "name");
assertEquals("name", animal.getName());

// 方式二:在 PowerMock 1.0 及其以上的版本,还可以这样设置属性值
Whitebox.setInternalState(animal, "TestName");
assertEquals("TestName", animal.getName());
}

同样,属性值的设置也有两种方式,第二种方式也是首选方式,它对于代码的重构更好,只不过如果在 Animal 类中有多个 String 类型的属性时,也不得不采用方式一。

相同的属性名

现在有一个名为 Cat 的实体类,它继承了 Animal 类:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author mofan
* @date 2021/4/12 17:23
*/
@Getter
public class Cat extends Animal{
private String name;

public String getSuperName() {
return super.getName();
}
}

并且还可以看到,子类和父类中都存在类型为 String 且名为 name 的属性。

那有没有什么办法可以准确地获取或设置值呢?

当然是可以的,只需要使用重载的方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testFieldWithTheSameName() {
Cat cat = new Cat();
Whitebox.setInternalState(cat, "name", "nameOfAnimal", Animal.class);
/*
* 第二种赋值的方式:
* Whitebox.setInternalState(cat, String.class, "nameOfAnimal", Animal.class);
*/
String name1 = Whitebox.<String>getInternalState(cat, "name", Animal.class);
String name2 = Whitebox.getInternalState(cat, String.class, Animal.class);
assertEquals("nameOfAnimal", name1);
assertEquals("nameOfAnimal", name2);
assertEquals("nameOfAnimal", cat.getSuperName());
assertNull(cat.getName());

Whitebox.setInternalState(cat, "TestName");
String name = Whitebox.getInternalState(cat, "name");
assertEquals("TestName", name);
assertEquals("TestName", cat.getName());
}

9.3 方法的执行

还可以使用 Whitebox.invokeMethod() 来执行方法(当然也包括私有方法)。

比如有这样一个类:

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
/**
* @author mofan
* @date 2021/4/12 18:07
*/
public class InvokeMethod {

private int sum(int a, int b) {
return a + b;
}

private int method(int a) {
return 2 * a;
}

private int method(Integer a) {
return 3 * a;
}

private double subtract(double a, double b) {
return a - b;
}

private static int multiplyMethod(int a, int b) {
return a * b;
}
}

然后可以使用 Whitebox 工具类来执行这个类中的一些方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testInvokeMethod() {
InvokeMethod method = new InvokeMethod();
try {
int sum1 = Whitebox.invokeMethod(method, "sum", 1, 2);
assertEquals(3, sum1);
int result1 = Whitebox.invokeMethod(method, "method", new Class<?>[]{int.class}, 2);
assertEquals(4, result1);
int result2 = Whitebox.invokeMethod(method, "method", new Class<?>[]{Integer.class}, 3);
assertEquals(9, result2);
int product = Whitebox.invokeMethod(InvokeMethod.class, "multiplyMethod", 2, 3);
assertEquals(6, product);
double diff = Whitebox.invokeMethod(method, 6.5, 3.1);
assertEquals(3.4, diff, 0.0);
} catch (Exception e) {
fail();
}
}

同样,在高版本的 PowerMock 中,可以不指定具体的方法名称来使用 Whitebox 执行方法,但必须保证方法的参数列表在所在类中唯一。

9.4 使用私有构造方法

利用 Whitebox 还可以利用私有构造方法来实例化一个类。

假设有这样一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @author mofan
* @date 2021/4/13 14:24
*/
@Getter
public class PrivateConstructor {
private int state;
private String code;

private PrivateConstructor(int state) {
this.state = state;
}

private PrivateConstructor(Integer state) {
this.state = state;
}

private PrivateConstructor(String code) {
this.code = code;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testPrivateConstructor() {
try {
PrivateConstructor constructor = Whitebox.invokeConstructor(PrivateConstructor.class, "Test");
assertEquals("Test", constructor.getCode());

PrivateConstructor privateConstructor1 =
Whitebox.invokeConstructor(PrivateConstructor.class, new Class<?>[]{int.class},
Collections.singletonList(5).toArray());
assertEquals(5, privateConstructor1.getState());
PrivateConstructor privateConstructor2 =
Whitebox.invokeConstructor(PrivateConstructor.class, new Class<?>[]{Integer.class},
Collections.singletonList(6).toArray());
assertEquals(6, privateConstructor2.getState());
} catch (Exception e) {
fail();
}
}

9.5 简单的使用案例

案例一

首先有这样两个实体类:

1
2
3
4
5
6
7
8
9
10
11
/**
* @author mofan
* @date 2021/4/12 14:50
*/
@Getter
@Setter
public class University {
private String universityName;
private String location;
private String code;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author mofan
* @date 2021/4/12 14:41
*/
@Getter
public class Student {
private String studentName;
private String gender;
private Integer age;
private University school;

public Student(String studentName, String gender, Integer age) {
University university = new University();
university.setUniversityName("xxx大学");
this.school = university;
this.studentName = studentName;
this.gender = gender;
this.age = age;
}
}

可以看到在 Student 类中有一个 University 类型的私有成员变量 school,它在构造方法中被初始化,并且在 Student 中并没有提供与 school 相关的 Setter 方法。

如果想要修改 schooluniversityName 的值,就可以使用 Whitebox 完成:

1
2
3
4
5
6
7
8
9
@Test
public void testInternalState() {
Student student = new Student("mofan", "gender", 19);
assertEquals("xxx大学", student.getSchool().getUniversityName());

University school = Whitebox.<University>getInternalState(student, "school");
Whitebox.setInternalState(school, "universityName", "TestUniversityName");
assertEquals("TestUniversityName", student.getSchool().getUniversityName());
}

案例二

假设有这样一个类,在这个类中还有一个静态嵌套类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author mofan
* @date 2021/4/12 15:13
*/
public class TestClass {
private static class InnerTestClass {
private String name;

public InnerTestClass(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void run() {
System.out.println("执行了run...");
throw new UnsupportedOperationException();
}
}
}

此时应该怎么测试 InnerTestClass 中的 run() 方法呢?

由于 InnerTestClass 是一个 private 的内部类,因此是没办法像下面这样 mock,因为这个内部类是不可见的:

1
TestClass.InnerTestClass clazz = mock(TestClass.InnerTestClass.class);

此时可以通过反射获取到 InnerTestClass 的构造方法,然后生成一个对象进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testInnerClass() throws Exception {
Class<Object> clazz = Whitebox.getInnerClassType(TestClass.class, "InnerTestClass");
Constructor<Object> constructor = Whitebox.getConstructor(clazz, String.class);
// 为 constructor 指定一个 String 类型的参数
Object instance = constructor.newInstance("TestName");
try {
Whitebox.invokeMethod(instance, "run");
fail();
} catch (Exception e) {
assertTrue(e instanceof UnsupportedOperationException);
}
}

运行上述测试方法后,测试通过,控制台打印出:

执行了run...

10. 整合 Spring 可能遇到的问题

10.1 ClassLoader 问题

使用 PowerMock 时,出现了以下错误:

Failed to load ApplicationContext

这是由于 ClassLoader 冲突,将对应冲突的包添加到测试类上注解 @PowerMockIgnore 的列表中即可。

比如:

1
@PowerMockIgnore({"javax.xml.*","com.sun.org.apache.xerces.*","javax.net.ssl.*","javax.management.*","org.xml.*","org.w3c.*"})

10.2 验证器冲突

使用 PowerMock 和继承时,可能会出现以下错误:

java.lang.VerifyError: Inconsistent stackmap frames at branch

这是因为 PowerMock 为支持对构造函数的测试,借助 Javassist 实现对字节码的操作。

从 Java 6 开始引入的 Stack Map Frames 特性与 Javassist 不兼容,在 Java 6 中该 Stack Map Frames 是可选的,但是到了 Java 7,Stack Map Frames 已经是默认使用的,由于不兼容问题导致了该异常。

在 Java 8 中可以增加启动参数 -Xverify:none 来解决,或者也可以升级 Javassist 版本到 3.25.0-GA,这时候就不用增加启动参数了。