【原创文章,转载请注明原文章地址,谢谢!】
一、为什么要使用Mock工具
在做单元测试的时候,我们会发现我们要测试的方法会引用很多外部依赖的对象,比如:(发送邮件,网络通讯,远程服务, 文件系统等等)。 而我们没法控制这些外部依赖的对象,为了解决这个问题,我们就需要用到Mock工具来模拟这些外部依赖的对象,来完成单元测试。
二、PowerMock简介
PowerMock 也是一个单元测试模拟框架,它是在其它单元测试模拟框架的基础上做出的扩展。通过提供定制的类加载器以及一些字节码篡改技巧的应用,PowerMock 现了对静态方法、构造方法、私有方法以及 Final 方法的模拟支持,对静态初始化过程的移除等强大的功能。因为 PowerMock 在扩展功能时完全采用和被扩展的框架相同的 API, 熟悉 PowerMock 所支持的模拟框架的开发者会发现 PowerMock 非常容易上手。PowerMock 的目的就是在当前已经被大家所熟悉的接口上通过添加极少的方法和注释来实现额外的功能。
三、环境配置
如果是使用 Eclipse 开发,只需要在 Eclipse 工程中包含相关库文件即可。
如果是使用 Maven 开发,则需要根据版本添加以下清单内容到 POM 文件中:
JUnit 版本 4.4 以上请参考清单 1
清单 1
1.4.10 org.powermock powermock-module-junit4 ${powermock.version} test org.powermock powermock-api-mockito ${powermock.version} test
JUnit 版本 4.0-4.3 请参考清单 2,
清单 2
1.4.10 org.powermock powermock-module-junit4-legacy ${powermock.version} test org.powermock powermock-api-mockito ${powermock.version} test
JUnit 版本 3 请参考清单 3,
清单 3
`
1.4.10
四、PowerMock入门
PowerMock有两个重要的注解:
@RunWith(PowerMockRunner.class)
@PrepareForTest( { YourClassWithEgStaticMethod.class })
如果你的测试用例里没有使用注解 @PrepareForTest
,那么可以不用加注解 @RunWith(PowerMockRunner.class)
,反之亦然。当你需要使用PowerMock强大功能(Mock静态、final、私有方法等)的时候,就需要加注解 @PrepareForTest
。
五、PowerMock基本用法
模拟 Static 方法
首先,我们需要有一个含有 static 方法的代码,如下所示:
package com._520it.test01;
public class IdGenerator {
public static long generateNewId() {
return 0L;
}
}
然后,需要在在被测试代码中调用上面的方法,测试代码如下所示:
package com._520it.test01;
public class ClassUnderTest {
public long methodToTest() {
final long id = IdGenerator.generateNewId();
return id;
}
}
为了测试各种情况,我们需要让静态方法generateNewId()
返回不同的值来对被测试的方法methodToTest()
的覆盖测试,实现方式如下:
package com._520it.test01;
import org.junit.Assert;
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(IdGenerator.class)
public class TestStatic {
// 模拟 Static 方法
@Test
public void testCallInternalInstance() throws Exception {
PowerMockito.mockStatic(IdGenerator.class);
// 在这个测试用例中,当generateNewId()每次被调用时,都会返回15
PowerMockito.when(IdGenerator.generateNewId()).thenReturn(15L);
Assert.assertEquals(15L, new ClassUnderTest().methodToTest());
//验证generateNewId()方法是否被调用
PowerMockito.verifyStatic();
IdGenerator.generateNewId();
}
}
模拟构造函数
有时候,可以很好的模拟构造函数,从而使被测代码中 new 操作返回的对象可以被随意定制,会很大程度的提高单元测试的效率,测试代码如下所示:
package com._520it.test02;
import java.io.File;
public class ClassUnderTest {
public boolean createDirectoryStructure(String directoryPath) {
File directory = new File(directoryPath);
if (directory.exists()) {
String msg = "\"" + directoryPath + "\" 已经存在.";
throw new IllegalArgumentException(msg);
}
return directory.mkdirs();
}
}
为了充分测试 createDirectoryStructure()函数,我们需要被 new 出来的 File 对象返回文件存在和不存在两种结果。在 PowerMock 出现之前,实现这个单元测试的方式通常都会需要在实际的文件系统中去创建对应的路径以及文件。然而,在 PowerMock 的帮助下,本函数的测试可以和实际的文件系统彻底独立开来:使用 PowerMock 来模拟 File 类的构造函数,使其返回指定的模拟 File 对象而不是实际的 File 对象,然后只需要通过修改指定的模拟 File 对象的实现,即可实现对被测试代码的覆盖测试.测试用例如下图所示:
package com._520it.test02;
import static org.junit.Assert.*;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.verifyNew;
import static org.powermock.api.mockito.PowerMockito.when;
import static org.powermock.api.mockito.PowerMockito.whenNew;
import java.io.File;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@RunWith(PowerMockRunner.class)
@PrepareForTest(ClassUnderTest.class)
public class TestConstruction {
//模拟构造函数
@Test
public void createDirectoryStructureWhenPathDoesntExist() throws Exception {
final String directoryPath = "seemygod";
//创建File的模拟对象
File directoryMock = mock(File.class);
//在当前测试用例下,当出现new File("seemygod")时,就返回模拟对象
whenNew(File.class).withArguments(directoryPath).thenReturn(directoryMock);
//当调用模拟对象的exists时,返回false
when(directoryMock.exists()).thenReturn(false);
//当调用模拟对象的mkdirs时,返回true
when(directoryMock.mkdirs()).thenReturn(true);
assertTrue(new ClassUnderTest().createDirectoryStructure(directoryPath));
//验证new File(directoryPath); 是否被调用过
verifyNew(File.class).withArguments(directoryPath);
}
}
模拟私有以及 Final 方法
类的私有方法和Final方法的测试则需要用到局部模拟.
在使用局部模拟,被创建出来的模拟对象依然是原系统对象,被 When().thenReturn()指定的函数将返回指定的值,没有指定的函数将按原有的方式执行.测试代码如下图所示:
package com._520it.test03;
public class PrivatePartialMockingExample {
public String methodToTest() {
return methodToMock("input");
}
private String methodToMock(String input) {
return "REAL VALUE = " + input;
}
}
测试用例:
package com._520it.test03;
import static org.junit.Assert.assertEquals;
import static org.powermock.api.mockito.PowerMockito.*;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@RunWith(PowerMockRunner.class)
@PrepareForTest(PrivatePartialMockingExample.class)
public class PrivatePartialMockingExampleTest {
@Test
public void demoPrivateMethodMocking() throws Exception {
final String expected = "TEST VALUE";
final String nameOfMethodToMock = "methodToMock";
final String input = "input";
PrivatePartialMockingExample underTest = spy(new PrivatePartialMockingExample());
when(underTest, nameOfMethodToMock, input).thenReturn(expected);
assertEquals(expected, underTest.methodToTest());
verifyPrivate(underTest).invoke(nameOfMethodToMock, input);
}
}
更多的Mock方法
六、PowerMock简单实现原理
1.当某个测试方法被注解@PrepareForTest
标注以后,在运行测试用例时,会创建一个新的org.powermock.core.classloader.MockClassLoader
实例,然后加载该测试用例使用到的类(系统类除外)。
2.PowerMock会根据你的mock要求,去修改写在注解@PrepareForTest
里的class文件(当前测试类会自动加入注解中),以满足特殊的mock需求。例如:去除'final方法的final标识,在静态方法的最前面加入自己的虚拟实现等。
3.如果需要mock的是系统类的final方法和静态方法,PowerMock不会直接修改系统类的class文件,而是修改调用系统类的class文件,以满足mock需求。