013-Java单元测试之PowerMock
1、背景介绍
EasyMock 以及 Mockito 都因为可以极大地简化单元测试的书写过程而被许多人应用在自己的工作中,但是这 2 种 Mock 工具都不可以实现对静态函数、构造函数、私有函数、Final 函数以及系统函数的模拟,但是这些方法往往是我们在大型系统中需要的功能。PowerMock 是在 EasyMock 以及 Mockito 基础上的扩展,通过定制类加载器等技术,PowerMock 实现了之前提到的所有模拟功能,使其成为大型系统上单元测试中的必备工具。
Mockito 是一个针对 Java 的单元测试模拟框架,它与 EasyMock 和 jMock 很相似,都是为了简化单元测试过程中测试上下文 ( 或者称之为测试驱动函数以及桩函数 ) 的搭建而开发的工具。在有这些模拟框架之前,为了编写某一个函数的单元测试,程序员必须进行十分繁琐的初始化工作,以保证被测试函数中使用到的环境变量以及其他模块的接口能返回预期的值,有些时候为了单元测试的可行性,甚至需要牺牲被测代码本身的结构。单元测试模拟框架则极大的简化了单元测试的编写过程:在被测试代码需要调用某些接口的时候,直接模拟一个假的接口,并任意指定该接口的行为。这样就可以大大的提高单元测试的效率以及单元测试代码的可读性。
Mockito 的优点是通过在执行后校验哪些函数已经被调用,消除了对期望行为(expectations)的需要。
Mockito 也并不是完美的,它不提供对静态方法、构造方法、私有方法以及 Final 方法的模拟支持。而程序员时常都会发现自己有对以上这些方法的模拟需求,特别是当一个已有的软件系统摆在面前时。
PowerMock 也是一个单元测试模拟框架,它是在其它单元测试模拟框架的基础上做出的扩展。通过提供定制的类加载器以及一些字节码篡改技巧的应用,PowerMock 现了对静态方法、构造方法、私有方法以及 Final 方法的模拟支持,对静态初始化过程的移除等强大的功能。因为 PowerMock 在扩展功能时完全采用和被扩展的框架相同的 API, 熟悉 PowerMock 所支持的模拟框架的开发者会发现 PowerMock 非常容易上手。PowerMock 的目的就是在当前已经被大家所熟悉的接口上通过添加极少的方法和注释来实现额外的功能,目前,PowerMock 仅支持 EasyMock 和 Mockito。
2、Mock底层原理
①Mockito底层使用了动态代理,用到了CGLIB。因此需要被mock的对象,Mockito都会生成一个子类继承该类,这也就是为什么final类、private方法、static方法不可以被Mock的原因
②powermock的底层原理
首先看powermock的依赖,它有两个重要的依赖:javassist和objenesis。
javassist是一个修改java字节码的工具包,objenesis是一个绕过构造方法来实例化一个对象的工具包。由此看来,PowerMock的本质是通过修改字节码来实现对静态和final等方法的mock的
下面是PowerMock的简单实现原理:
当某个测试方法被注解@PrepareForTest标注以后,在运行测试用例时,会创建一个新的org.powermock.core.classloader.MockClassLoader实例,然后加载该测试用例使用到的类(系统类除外)。
PowerMock会根据你的mock要求,去修改写在注解@PrepareForTest里的class文件(当前测试类会自动加入注解中),以满足特殊的mock需求。例如:去除final方法的final标识,在静态方法的最前面加入自己的虚拟实现等。
如果需要mock的是系统类的final方法和静态方法,PowerMock不会直接修改系统类的class文件,而是修改调用系统类的class文件,以满足mock需求。
3、项目中引用PowerMock
可以在POM文件中进行如下配置来在项目中引入PowerMock辅助我们进行单元测试:
下面展示一些 内联代码片
。
<properties>
<powermock.version>1.5.6</powermock.version>
</properties>
<dependencies>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
4、重点API讲解
Mock
Powermockito.mock() 方 法 主 要 是 根 据 class 创 建 一 个 对 应 的 mock 对 象 ,powermock 的创建方式可不像 easymock 等使用 proxy 的方式创建,他是会在你运行的过程中动态的修改 class 字节码文件的形式来创建的。
spy
如果一个对象,只希望mock它的部分方法,而其他方法希望和真实对象的行为一致,可以使用spy。时,没有通过when设置过的方法,测试调用时,行为和真实对象一样
DoReturn…when…then
我们可以看到,每次当我们想给一个 mock 的对象进行某种行为的预期时,都会使用do…when…then…这样的语法,其实理解起来非常简单:做什么、在什么时候、然后返回什么。DoReturn不会进入mock方法的内部
when…thenReturn
其实理解起来非常简单:做什么、在什么时候、然后返回
什么。需要注意的是:mock的对象,所有没有调用when设置过的方法,在测试时调用,返回的都是对应返回类型的默认值。when…thenReturn会进入mock方法的内部
doNothing().when(…)…
调用后什么都不做的
doThrow(Throwable).when(…)…
调用后抛异常
Verify
当我们测试一个 void 方法的时候,根本没有办法去验证一个 mock 对象所执行后的结果,因此唯一的方法就是检查方法是否被调用,在后文中将还会专门来讲解。
@RunWith
显式的告诉 Junit 使用某个指定的 Runner 来运行 Test Case,我们使用了 PowerMockRunner 来运行我们的测试用例,如果不指定的话我们就默认使用的是 Junit 提供的 Runner
@PrepareForTest
PowerMock的Runner提前准备一个已经根据某种预期改变过的class,PowerMockito mock私有方法,静态方法和final方法的时候添加这个注解,可以作用在类和方法(某些情况下不起作用)上
通配符
可能存在这种情况:测试时对于传入的参数或者传出的参数并不关心,这时可以使用通配符:通过PowerMockito.anyInt()、PowerMockito.anyString、甚至PowerMockito.any(Class clazz)……来表示任意值,**需要注意的是,如果一个方法中,有一个地方使用了通配符,其他参数也都要使用通配符,对于特定的参数,不能直接指定,而需要使用PowerMockito.eq(someArg)来通配这个参数
5、常见操作
5.1、PowerMockito.mock
ElasticSearch.RestHighLevelClient为例
private RestHighLevelClient restHighLevelClient = mock(RestHighLevelClient.class);
5.2、PowerMockito.mockstatic
ElasticSearch.RestClient为例
mockStatic(RestClient.class);
when(RestClient.builder(httpHost)).thenReturn(restClientBuilder);
5.3、PowerMockito.whenNew
PowerMockito.whenNew,它还可以实现无参数withNoArguments、带参数withArguments(一个以及多个)
whenNew(RestHighLevelClient.class).withArguments(restClientBuilder).thenReturn(restHighLevelClient);
whenNew(HttpHost.class).withArguments(host, port, "http").thenReturn(httpHost);
whenNew(SearchSourceBuilder.class).withNoArguments().thenReturn(searchSourceBuilder);
6、实例代码
@RunWith(PowerMockRunner.class) :表明用 PowerMockerRunner来运行测试用例,否则无法使用PowerMock
PrepareForTest({UserController.class}):所有需要测试的类,列在此处,以逗号分隔
@PowerMockIgnore(“javax.management.*”):为了解决使用powermock后,提示classloader错误
依赖:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.6.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.6.5</version>
<scope>test</scope>
</dependency>
UserController
import org.springframework.beans.factory.annotation.Autowired;
public class UserController {
@Autowired
private UserService userService;
public boolean addUser(UserDto userDto) {
int added = userService.addUser(userDto);
if (added <= 0) {
return false;
} else {
return true;
}
}
public boolean delUser(int id) {
try {
userService.delUser(id);
return true;
} catch (Exception e) {
return false;
}
}
public void saveUser(UserDto userDto) {
userService.saveUser(userDto);
}
public int countUser() {
UserDto ud = new UserDto();
int count = 0;
if (ud.getId() > 0) {
count += 1;
}
return count;
}
public boolean modUser(UserDto userDto) {
int moded = userService.modUser(userDto);
return verifyMod(moded);
}
private boolean verifyMod(int moded) {
if (moded <= 0) {
return false;
} else {
return true;
}
}
}
UserService
public interface UserService {
int addUser(UserDto userDto);
int delUser(int id) throws Exception;
int modUser(UserDto userDto);
void saveUser(UserDto userDto);
}
UserDto
public class UserDto {
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
FileHelper
public class FileHelper {
public static String getName(String name) {
return "A_" + name;
}
}
测试类完整代码
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
import java.lang.reflect.Method;
// 相关注解
@RunWith(PowerMockRunner.class)
@PrepareForTest({UserController.class, FileHelper.class})
@PowerMockIgnore("javax.management.*")
public class UserControllerTest {
// @Autowired 属性的注入方式: 联合使用 @Mock 和 @InjectMocks
// 下面的方式,将会mock出来一个 user service对象,将将其注入到 UserController 的实例 uc 中去。
@Mock
private UserService userService;
@InjectMocks
private UserController uc;
/**
* mock普通方法
*
* @throws Exception
*/
@Test
public void testAddUser() throws Exception {
UserDto ud = new UserDto();
PowerMockito.when(userService.addUser(ud)).thenReturn(1);
// can not stub like this
// PowerMockito.doReturn(1).when(userService.addUser(ud));
boolean result = uc.addUser(ud);
Assert.assertEquals(result, true);
}
/**
* mock抛异常
*
* @throws Exception
*/
@Test
public void testDelUser() throws Exception {
int toDelete = 1;
// 如果 user service 中的 delUser() 方法抛出的是 checked exception,那么,thenThrow() 里需要抛出 Exception()或者其子类;
// 如果delUser() 方法抛出的是 unchecked exception,那么,thenThrow() 里需要抛出 RuntimeException()或其子类
PowerMockito.when(userService.delUser(toDelete)).thenThrow(new Exception("mock exception"));
boolean result = uc.delUser(toDelete);
Assert.assertEquals(result, false);
}
/**
* mock静态方法
*/
@Test
public void mockFileHelper() {
PowerMockito.mockStatic(FileHelper.class);
PowerMockito.when(FileHelper.getName("lucy")).thenReturn("lily");
Assert.assertEquals(FileHelper.getName("lucy"), "lily");
}
/**
* mock 返回值为 void 的方法
*
* @throws Exception
*/
@Test
public void testSaveUser() throws Exception {
UserDto userDto = new UserDto();
// way one:
PowerMockito.doNothing().when(userService, "saveUser", userDto);
// way two:
PowerMockito.doNothing().when(userService).saveUser(userDto);
uc.saveUser(userDto);
}
/**
* mock私有方法<br />
* 方法一<br />
* PS:该方法中,还介绍了 mock私有字段的值 的方法。
*
* @throws Exception
*/
@Test
public void testModUser() throws Exception {
UserDto ud = new UserDto();
int moded = 1;
PowerMockito.when(userService.modUser(ud)).thenReturn(moded);
UserController uc2 = PowerMockito.mock(UserController.class);
// 给没有 setter 方法的 私有字段 赋值。
Whitebox.setInternalState(uc2, "userService", userService);
// 因为要测试的是 modUser() 方法,
// 所以,当调用这个方法时,应该让它调用真实的方法,而非被mock掉的方法
PowerMockito.when(uc2.modUser(ud)).thenCallRealMethod();
// 在modUser()方法中会调用verifyMod()这个私有方法,所以,需要将mock掉
PowerMockito.when(uc2, "verifyMod", moded).thenReturn(true);
boolean result = uc2.modUser(ud);
Assert.assertEquals(result, true);
}
/**
* mock私有方法<br />
* 方法二
*
* @throws Exception
*/
@Test
public void testModUser2() throws Exception {
UserDto ud = new UserDto();
int moded = 1;
PowerMockito.when(userService.modUser(ud)).thenReturn(moded);
// 对uc进行监视
uc = PowerMockito.spy(uc);
// 当uc的verifyMod被执行时,将被mock掉
PowerMockito.when(uc, "verifyMod", moded).thenReturn(true);
boolean result = uc.modUser(ud);
Assert.assertEquals(result, true);
}
/**
* 测试私有方法(注意: 是测试,不是mock)<br />
* 方法一
*
* @throws Exception
*/
@Test
public void testVerifyMod() throws Exception {
// 获取Method对象,
Method method = PowerMockito.method(UserController.class, "verifyMod", int.class);
// 调用Method的invoke方法来执行
boolean result = (boolean) method.invoke(uc, 1);
Assert.assertEquals(result, true);
}
/**
* 测试私有方法(注意: 是测试,不是mock)<br />
* 方法二
*
* @throws Exception
*/
@Test
public void testVerifyMod2() throws Exception {
// 通过 Whitebox 来执行
boolean result = Whitebox.invokeMethod(uc, "verifyMod", 1);
Assert.assertEquals(result, true);
}
/**
* mock新建对象
*
* @throws Exception
*/
@Test
public void testCountUser() throws Exception {
UserDto ud = new UserDto();
ud.setId(1);
PowerMockito.whenNew(UserDto.class).withNoArguments().thenReturn(ud);
int count = uc.countUser();
Assert.assertEquals(count, 1);
}
/**
* 参数的模糊匹配
*/
@Test
public void mockFileHelper2() {
PowerMockito.mockStatic(FileHelper.class);
PowerMockito.when(FileHelper.getName(Matchers.anyString())).thenReturn("lily");
Assert.assertEquals(FileHelper.getName("lucy"), "lily");
Assert.assertEquals(FileHelper.getName("hanmeimei"), "lily");
}
}
7、Mock静态私有方法
MockStaticPrivateMethod
public class MockStaticPrivateMethod {
public static boolean isTrue() {
return returnTrue();
}
private static boolean returnTrue() {
return true;
}
}
PowerMockTest
@RunWith(PowerMockRunner.class)
@PrepareForTest(MockStaticPrivateMethod.class)
@PowerMockIgnore("javax.management.*")
public class PowerMockTest {
@Test
public void mockStaticPrivate() throws Exception {
PowerMockito.mockStatic(MockStaticPrivateMethod.class);
PowerMockito.when(MockStaticPrivateMethod.class, "returnTrue")
.thenReturn(false);
PowerMockito.when(MockStaticPrivateMethod.isTrue()).thenCallRealMethod();
assertThat(MockStaticPrivateMethod.isTrue(), is(false));
}
}
可以使用Whitebox来方便的调用静态私有方法
public class MockStaticPrivateMethod {
private static boolean returnTrue() {
return true;
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest(MockStaticPrivateMethod.class)
public class PowerMockTest {
@Test
public void mockStaticPrivate() throws Exception {
PowerMockito.mockStatic(MockStaticPrivateMethod.class);
PowerMockito.when(MockStaticPrivateMethod.class, "returnTrue")
.thenReturn(false);
assertThat(Whitebox.invokeMethod(MockStaticPrivateMethod.class, "returnTrue"),
is(false));
}
}