当被测方法中存在依赖具体环境或上下文的外部方法调用(例如创建数据库链接),而这些上下文条件又很难预设时,该方法往往难以进行单元测试。
PowerMock可以通过修改类字节码,并使用自定义的ClassLoader加载运行,以模拟类或实例的方法,进而隔离被测方法对外部的依赖。
使用PowerMock的基本步骤:
- 添加对powermock开源类库的引用。
- 使用
@RunWith(PowerMockRunner.class)
注解测试类。 - 使用
@PrepareForTest(使用模拟类的被测试类)
注解测试类。 - 测试代码中使用PowerMockito工具设置需要被模拟的类及其行为。
软件准备
我们以Maven构建Java程序为例,只需在pom.xml中添加对PowerMock的依赖即可:
<dependencies>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
</dependencies>
本文后续以如下FileUtil作为被测试类,演示如何使用PowerMock:
import java.io.File;
import java.io.FileNotFoundException;
public class FileUtil {
private static final String LOG_FILE = "root.log";
public boolean mkdirs(String path) {
log("start to mkdirs:" + path);
File fileDir = new File(path);
if (fileDir.exists()) {
throw new IllegalArgumentException(path + "alreay exists!");
} else {
return fileDir.mkdirs();
}
}
public File getLogFile() throws FileNotFoundException {
log("start to get log file");
String logPath = System.getenv("LOG_PATH");
if (null == logPath) {
throw new IllegalArgumentException("System env:LOG_PATH not exist!");
}
if (isExists(logPath)) {
return new File(logPath + LOG_FILE);
}
throw new FileNotFoundException("File " + logPath + LOG_FILE + "not exist!");
}
private boolean isExists(String logPath) {
return false;
}
public void log(String log) {
System.out.println(log);
}
}
模拟类
PowerMock支持在测试代码中模拟一个类的实例,当这个类的创建需要难以构造的条件时,模拟类将会很有帮助。
如下代码可获取一个模拟FileUtil类的实例,模拟公有方法部分也有演示如何使用fileUtil模拟类:
FileUtil fileUtil = PowerMockito.mock(FileUtil.class);
模拟构造方法
模拟构造函数同样可以创建一个模拟的类的实例,一般用于覆盖被测方法中的类的创建,使得被测方法中new出的对象可以被随意定制。
以下testMkdirs
测试方法模拟了File
类的new
操作,并且模拟待创建目录不存在、目录创建操作能正确返回的场景。
这使得被测方法可以根据测试代码中设置模拟条件,按照测试人员期望的方式执行指定代码的条件分支。
import static org.junit.Assert.assertTrue;
import java.io.File;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@RunWith(PowerMockRunner.class)
@PrepareForTest(FileUtil.class)
public class FileUtilMkdirTest {
@Before
public void init() throws Exception {
File fileDir = PowerMockito.mock(File.class);
PowerMockito.whenNew(File.class).withAnyArguments().thenReturn(fileDir);
PowerMockito.when(fileDir.exists()).thenReturn(false);
PowerMockito.when(fileDir.mkdirs()).thenReturn(true);
}
@Test
public void testMkdirs() {
assertTrue(new FileUtil().mkdirs("testDir"));
}
}
说明:
FileUtilMkdirTest
中只有FileUtil
被测试类中使用到模拟类,只需将FileUtil类添加到注解中:
@PrepareForTest(FileUtil.class)
- 如果被测试类中还调用其他类型,且都使用到模拟类,需要将这些被测试类都添加到注解中:
@PrepareForTest({FileUtil.class, FileUtil2.class})
模拟静态方法
Java仅提供获取环境变量的方法System.getenv
,但不提供设置环境变量的方法System.setenv
。
本示例模拟了System.getenv
方法,使得测试人员可以设置需要的环境变量。
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@RunWith(PowerMockRunner.class)
@PrepareForTest(FileUtil.class)
public class FileUtilGetLogFileTest {
@Test
public void testGetLogFileWithException() {
PowerMockito.mockStatic(System.class);
PowerMockito.when(System.getenv("LOG_PATH")).thenReturn("./");
try {
new FileUtil().getLogFile();
fail();
} catch (FileNotFoundException e) {
assertTrue(true);
}
}
}
模拟公有、私有方法
本示例为测试类FileUtilGetLogFileTest
添加了两个测试方法,用于模拟FileUtil的公有和私有方法。
- 有返回值
如下testGetLogFile
方法模拟了FileUtil类的私有方法isExists
,并让其返回true
:
@Test
public void testGetLogFile() throws Exception {
PowerMockito.mockStatic(System.class);
PowerMockito.when(System.getenv("LOG_PATH")).thenReturn("./");
FileUtil fileUtil = PowerMockito.spy(new FileUtil());
PowerMockito.when(fileUtil, "isExists", "./").thenReturn(true);
File logFile = fileUtil.getLogFile();
assertTrue(logFile != null);
}
- 无返回值
如下testGetLogFileWithoutLog
方法额外模拟了FileUtil类的无返回值的公有方法log
,让其什么都不做(不打印日志):
@Test
public void testGetLogFileWithoutLog() throws Exception {
PowerMockito.mockStatic(System.class);
PowerMockito.when(System.getenv("LOG_PATH")).thenReturn("./");
FileUtil fileUtil = PowerMockito.spy(new FileUtil());
PowerMockito.when(fileUtil, "isExists", "./").thenReturn(true);
PowerMockito.doNothing().when(fileUtil, "log", Mockito.anyString());
File logFile = fileUtil.getLogFile();
assertTrue(logFile != null);
}
说明:
如果需要模拟方法抛出异常,需要被测方法声明抛出异常,否则不能实现。
针对有、无返回值,可以采用以下两种方式:
- 有返回值:
PowerMockito.when(fileUtil, "getLogFile", "./").thenThrow(new FileNotFoundException());
- 无返回值:
PowerMockito.doThrow(new Exception()).when(fileUtil, "xxx");