偶然碰到这篇介绍mock框架的文章,作者未知,在此处Mark下
什么是类的部分mock(partial mock)?
A:部分mock是说一个类的方法有些是实际调用,有些是使用mockito的stubbing(桩实现)。
为什么需要部分mock?
A:当需要测试一个组合方法(一个方法需要其它多个方法协作)的时候,某个叶子方法(只供别人调用,自己不依赖其它反复)已经被测试过,我们其实不需要再次测试这个叶子方法,so,让叶子打桩实现返回结果,上层方法实际调用并测试。
单元测试是对应用中的某一个模块的功能进行验证。在单元测试中,我们常遇到的问题是应用中其它的协同模块尚未开发完成,或者被测试模块需要和一些不容易构造、比较复杂的对象进行交互。另外,由于不能肯定其它模块的正确性,我们也无法确定测试中发现的问题是由哪个模块引起的。
Mock 对象能够模拟其它协同模块的行为,被测试模块通过与 Mock 对象协作,可以获得一个孤立的测试环境。此外,使用Mock 对象还可以模拟在应用中不容易构造和比较复杂的对象,从而使测试顺利进行。
Mockito 是一个针对 Java 的单元测试模拟框架,它与EasyMock 和 jMock 很相似,相对于 EasyMock 和 jMock,Mockito 的优点是通过在执行后校验哪些函数已经被调用,消除了对期望行为(expectations)的需要。其它的mocking 库需要在执行前记录期望行为(expectations),而这导致了丑陋的初始化代码。但是,Mockito 也并不是完美的,它不提供对静态方法、构造方法、私有方法以及Final 方法的模拟支持。
环境配置方法
对于需要的开发包,PowerMock 网站提供了”一站式”下载 : 从 此页面中选择以类似 PowerMock 1.4.10with Mockito and JUnit including dependencies 为注释的链接,该包中包含了最新的 JUnit 库,Mockito 库,PowerMock库以及相关的依赖。
如果是使用 Eclipse 开发,只需要在 Eclipse 工程中包含这些库文件即可。
如果是使用 Maven 开发,则需要根据版本添加以下清单内容到 POM 文件中:
JUnit 版本 4.4 以上请参考清单 1
清单 1
<properties> <powermock.version>1.4.10</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> |
PowerMock 在单元测试中的应用
1、模拟 Static 方法
在任何需要用到 PowerMock 的类开始之前,首先我们要做如下声明:
@RunWith(PowerMockRunner.class)
然后,还需要用注释的形式将需要测试的静态方法提供给PowerMock:
@PrepareForTest( {YourClassWithEgStaticMethod.class })
首先,需要有一个含有 static 方法的代码 , 如清单4:
清单 4
public class IdGenerator {
...
public static long generateNewId() { ... }
... } |
然后,在被测代码中,引用了以上方法 , 如清单 5 所示:
清单 5
public class ClassUnderTest { ... public void methodToTest() { .. final long id = IdGenerator.generateNewId(); .. } ... } |
为了达到单元测试的目的,需要让静态方法generateNewId()返回各种值来达到对被测试方法 methodToTest()的覆盖测试,实现方式如清单 6 所示:
清单 6
@RunWith(PowerMockRunner.class) //We prepare the IdGenerator for test because the static method is normally not mockable @PrepareForTest(IdGenerator.class) public class MyTestClass { @Test public void demoStaticMethodMocking() throws Exception { mockStatic(IdGenerator.class); /* * Setup the expectation using the standard Mockito syntax, * generateNewId() will now return 2 everytime it's invoked * in this test. */ when(IdGenerator.generateNewId()).thenReturn(2L);
new ClassUnderTest().methodToTest();
// Optionally verify that the static method was actually called verifyStatic(); IdGenerator.generateNewId(); } } |
如清单 6 中所展示,在测试代码中,可以使用When().thenReturn() 语句来指定被引用的静态方法返回任意需要的值,达到覆盖测试的效果。
2、模拟构造函数
有时候,能模拟构造函数,从而使被测代码中 new 操作返回的对象可以被随意定制,会很大程度的提高单元测试的效率,考虑如清单7 的代码:
清单 7
public class DirectoryStructure { public boolean create(String directoryPath) { File directory = new File(directoryPath);
if (directory.exists()) { throw new IllegalArgumentException( "\"" + directoryPath + "\" already exists."); }
return directory.mkdirs(); } } |
为了充分测试 create()函数,我们需要被 new 出来的File 对象返回文件存在和不存在两种结果。在 PowerMock 出现之前,实现这个单元测试的方式通常都会需要在实际的文件系统中去创建对应的路径以及文件。然而,在PowerMock 的帮助下,本函数的测试可以和实际的文件系统彻底独立开来:使用 PowerMock 来模拟 File 类的构造函数,使其返回指定的模拟File 对象而不是实际的 File 对象,然后只需要通过修改指定的模拟 File 对象的实现,即可实现对被测试代码的覆盖测试,参考如清单 8 的代码:
清单 8
@RunWith(PowerMockRunner.class) @PrepareForTest(DirectoryStructure.class) public class DirectoryStructureTest { @Test public void createDirectoryStructureWhenPathDoesntExist() throws Exception { final String directoryPath = "mocked path";
File directoryMock = mock(File.class);
// This is how you tell PowerMockito to mock construction of a new File. whenNew(File.class).withArguments(directoryPath).thenReturn(directoryMock);
// Standard expectations when(directoryMock.exists()).thenReturn(false); when(directoryMock.mkdirs()).thenReturn(true);
assertTrue(new NewFileExample().createDirectoryStructure(directoryPath));
// Optionally verify that a new File was "created". verifyNew(File.class).withArguments(directoryPath); } } |
使用 whenNew().withArguments().thenReturn()语句即可实现对具体类的构造函数的模拟操作。然后对于之前创建的模拟对象 directoryMock使用 When().thenReturn() 语句,即可实现需要的所有功能,从而实现对被测对象的覆盖测试。在本测试中,因为实际的模拟操作是在类DirectoryStructureTest 中实现,所以需要指定的 @PrepareForTest 对象是DirectoryStructureTest.class。
3、模拟私有以及 Final 方法
为了实现对类的私有方法或者是 Final 方法的模拟操作,需要PowerMock 提供的另外一项技术:局部模拟。
在之前的介绍的模拟操作中,我们总是去模拟一整个类或者对象,然后使用When().thenReturn()语句去指定其中值得关心的部分函数的返回值,从而达到搭建各种测试环境的目标。对于没有使用When().thenReturn()方法指定的函数,系统会返回各种类型的默认值(具体值可参考官方文档)。
局部模拟则提供了另外一种方式,在使用局部模拟时,被创建出来的模拟对象依然是原系统对象,虽然可以使用方法When().thenReturn()来指定某些具体方法的返回值,但是没有被用此函数修改过的函数依然按照系统原始类的方式来执行。
这种局部模拟的方式的强大之处在于,除开一般方法可以使用之外,Final方法和私有方法一样可以使用。
参考如清单 9 所示的被测代码:
清单 9
public final class PrivatePartialMockingExample { public String methodToTest() { return methodToMock("input"); }
private String methodToMock(String input) { return "REAL VALUE = " + input; } } |
为了保持单元测试的纯洁性,在测试方法methodToTest()时,我们不希望受到私有函数 methodToMock()实现的干扰,为了达到这个目的,我们使用刚提到的局部模拟方法来实现 , 实现方式如清单10:
清单 10
@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());
/* * Setup the expectation to the private method using the method name */ when(underTest, nameOfMethodToMock, input).thenReturn(expected);
assertEquals(expected, underTest.methodToTest());
// Optionally verify that the private method was actually called verifyPrivate(underTest).invoke(nameOfMethodToMock, input); } } |
可以发现,为了实现局部模拟操作,用来创建模拟对象的函数从mock() 变成了 spy(),操作对象也从类本身变成了一个具体的对象。同时,When() 函数也使用了不同的版本:在模拟私有方法或者是 Final 方法时,When()函数需要依次指定模拟对象、被指定的函数名字以及针对该函数的输入参数列表。