JUnit单元测试5—PowerMock

当被测方法中存在依赖具体环境或上下文的外部方法调用(例如创建数据库链接),而这些上下文条件又很难预设时,该方法往往难以进行单元测试。
PowerMock可以通过修改类字节码,并使用自定义的ClassLoader加载运行,以模拟类或实例的方法,进而隔离被测方法对外部的依赖。

使用PowerMock的基本步骤:

  1. 添加对powermock开源类库的引用。
  2. 使用@RunWith(PowerMockRunner.class)注解测试类。
  3. 使用@PrepareForTest(使用模拟类的被测试类)注解测试类。
  4. 测试代码中使用PowerMockito工具设置需要被模拟的类及其行为。

软件准备

我们以Maven构建Java程序为例,只需在pom.xml中添加对PowerMock的依赖即可:

    
        
            org.powermock
            powermock-module-junit4
            2.0.2
            test
        
        
            org.powermock
            powermock-api-mockito2
            2.0.2
            test
        
    

本文后续以如下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");

上一篇:JUnit单元测试4—模拟Web服务器
下一篇:JUnit单元测试6—@Rule注解

你可能感兴趣的:(JUnit单元测试5—PowerMock)