封面来源:本文封面来源于网络,如有侵权,请联系删除。
本文参考:汪文君 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 实战】查看。
简单来说
PowerMock 不是重复发明的轮子,它是对其他轮子的拓展延伸(类似于 MyBatis 与 MyBatis-Plus);
PowerMock 完成了其他 Mock 框架不能完成的工作;
尽量减少 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 > <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 public class User {}
创建一个 UserDao
类,并模拟数据库失效:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 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 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); 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 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 @RunWith(PowerMockRunner.class) @PrepareForTest(UserService.class) public class LocalUserServiceTest { @Test public void testQueryUserCount () { try { UserService userService = new UserService (); UserDao userDao = PowerMockito.mock(UserDao.class); 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 局部变量时,请在测试类上使用注解:
@PrepareForTest
的使用场景
重点! 重点! 重点!
当使用 PowerMockito.whenNew()
方法时,必须加注解 @PrepareForTest
和 @RunWith
。注解 @PrepareForTest
里写的类是需要 mock 的 new 对象代码所在的类;
当需要 mock final 方法的时候,必须加注解 @PrepareForTest
和 @RunWith
。注解 @PrepareForTest
里写的类是 final
方法所在的类;
当需要 mock 静态方法的时候,必须加注解 @PrepareForTest
和 @RunWith
。注解 @PrepareForTest
里写的类是静态方法所在的类。
当需要 mock 私有方法的时候,只是需要加注解 @PrepareForTest
,注解里写的类是私有方法所在的类。
当需要 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 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 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 @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 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 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 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 @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 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 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 @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 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 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 @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 (); 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 public class UserDao { public String queryByName (String username) { throw new UnsupportedOperationException (); } }
创建一个名为 UserService
的类:
1 2 3 4 5 6 7 8 9 10 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 @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 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 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 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); }
测试能够通过,控制台打印出:
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 ()); 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
可以设置 private
、static
、final
域的值,并且不用添加到 @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 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 public class WhiteBoxTest { @Test public void testGetInternalState () { ServiceHolder holder = new ServiceHolder (); final Object obj = new Object (); holder.addService(obj); Set<String> objectSet = Whitebox.getInternalState(holder, "objectSet" ); assertEquals(1 , objectSet.size()); assertSame(obj, objectSet.iterator().next()); 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 @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 (); Whitebox.setInternalState(animal, "name" , "name" ); assertEquals("name" , animal.getName()); Whitebox.setInternalState(animal, "TestName" ); assertEquals("TestName" , animal.getName()); }
同样,属性值的设置也有两种方式,第二种方式也是首选方式,它对于代码的重构更好,只不过如果在 Animal
类中有多个 String
类型的属性时,也不得不采用方式一。
相同的属性名
现在有一个名为 Cat
的实体类,它继承了 Animal
类:
1 2 3 4 5 6 7 8 9 10 11 12 @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); 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 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 @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 @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 @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 方法。
如果想要修改 school
中 universityName
的值,就可以使用 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 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); 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
,这时候就不用增加启动参数了。