使用Powermock实现单元测试,提高单元测试覆盖率

时间:2021-05-14 11:32:54

1. PowerMock介绍(本章属于普及知识,熟悉这直接跳过)

   软件设计开发过程中,通常采用分模块、并行开发的模式。在开发周期中,当前模块所依赖的其他模块只有接口,没有具体实现。为了实现对当前模块的单元测试,需要通过mock手段来mock未实现的其他接口。另外,模块还有依赖其他第三方库的情况,而在运行单元测试的过程中,很多第三方lib要么因为缺少条件或资源无法加载,要么直接调用非常消耗运行资源。既然单元测试的重点是当前模块的逻辑,所以可以使用mock手段代替对第三方库的直接调用。

    在java程序的单元测试中常用的mock工具有Mockito和EasyMock。但是这两种mock工具都无法实现对静态、final、私有方法或类的mock。因此有了功能强大的PowerMock工具。PowerMock并不是一个独立、全新的工具而是在Mockito和EasyMock的基础上进行的扩展,它分别有针对Mockito级EasyMock的扩展实现。本文重点介绍PowerMockito。

    PowerMock是通过直接修改class文件字节码的方式实现对特定对象的mock。事实上是直接改变了mock对象的实现逻辑。在使用PowerMock的情况下,如Jacoco等的测试覆盖率工具是无发正确统计出单元测试覆盖率的。

2. 基本应用

待测试方法:

public String methodToTest() {
return classToMock.methodToMock();
}

测试case:

@RunWith(PowerMockRunner. class)
public class PowerMocTestCase {

@Test
public void normalTest(){
ClassToMockclassToMock = PowerMockito.mock(ClassToMock.class);
PowerMockito. when(classToMock.methodToMock()).thenReturn( "mockObject");
ClassToTestclassToTest = new ClassToTest(classToMock);
Assert. assertEquals("mockObject",classToTest.methodToTest());
}
}

其中@RunWith注解用于指定PowerMockRunner作为Junit的runner。

classToMock为生成的mock对象。

PowerMockito.when().thenReturn()...用于为mock对象的方法指定返回值。如果没用显示的指定方法行为,mock对象的方法调用就会根据powermock的具体配置执行默认行为,而不会调用真实方法。

 

3. Mock Static

待测试方法:

public String methodToTest() {
return ClassToMock.methodToMock();
}

测试case:

@RunWith(PowerMockRunner. class)
@PrepareForTest({ClassToMock.class })
public class PowerMocTestCase {

@Test
public void mockStaticTest(){
PowerMockito. mockStatic(ClassToMock.class);
PowerMockito. when(ClassToMock.methodToMock ()).thenReturn("staticReturnValue" );

ClassToTestclassToTest = new ClassToTest();
Assert. assertEquals("staticReturnValue",classToTest.methodToTest());
}
}

在Mock Static method的时候,含静态方法的类需要写到@PrepareForTest注解里面。该注解的作用,将在下面详细介绍。

 

4. Partial Mock & Mock Private

测试case:

@RunWith(PowerMockRunner. class)
@PrepareForTest({ClassToTest. class})
public class PowerMocTestCase {

@Test
public void mockPrivateTest(){
ClassToTest classToTest=PowerMockito. spy(new ClassToTest());
//ClassToTestclassToTest = Whitebox.newInstance(ClassToTest.class);
try {
PowerMockito. doReturn("privateObject").when(classToTest, "privateMethodToMock",Mockito.anyObject());
classToTest.methodCallPrivate();

PowerMockito. verifyPrivate(classToTest).invoke("privateMethodToMock",Mockito. anyObject());

} catch (Exceptione) {
e.printStackTrace();
Assert. fail();
}
}
}


 

由于private 方法只能在类内部调用,如果将待测试类整体mock ,则调用private的主调方法也会同时被mock掉。

此处通过spy进行partial mock 通过spy产生的mock对象,在没用通过do...return显式指定mock行为的情况下,会调用真实的方法。上述case中通过method name 和参数列表的方式指定private 方法privateMethodToMock(Object obj)的行为。

PowerMockito.verifyPrivate 通过判断private方法是否被调用来断言case是否执行通过。

对于Partial Mock来说,使用如下方式指定mock行为是行不通的

PowerMockito. when(classToTest.methodCallPrivate()).thenReturn( "returnSomeValue");

因为“classToTest.methodCallPrivate()”会触发真是方法的调用。所以partialmock的时候需要通过如下方式指定mock行为:

PowerMockito.doReturn("returnSomeValue" ).when(classToTest).methodCallPrivate();

5. Whitebox & Mock 内部成员

Mock内部成员就是通过PowerMock get及set类的内部属性值(包括私有)。使用该功能需要导入Whitebox类:

import org.powermock.reflect.Whitebox ;

PowerMock 提供WhiteBox的目的就是跳过面向对象语言的封装性,允许test case直接操作类的私有成员、私有方法、甚至通过私有构造函数创建实例。

获取内部成员值:

boolean result = Whitebox.getInternalState(classToTest, "innerFieldName" );

设置内部成员值:

Whitebox. setInternalState(classToTest, "innerFieldName", true);

直接调用类或对象的私有方法:

Whitebox.invokeMethod(..)

通过私有构造函数创建对象:

Whitebox.invokeConstructor(..)

Whitebox跳过构造函数直接实例化对象:

ClassToTest classToTest = Whitebox.newInstance( ClassToTest. class);

6. Mock 对象的构建(construction)

待测试方法:

public class ClassThatNewObject {

public void methodThatNewObject(){
ClassToMockclassToMock = new ClassToMock();
classToMock.methodToMock ();
}
}

上述方法内部new了一个对象,如何mock改内部对象的某些方法的行为?请看下面test case:

@RunWith(PowerMockRunner. class)
@PrepareForTest({ClassThatNewObject.class})
public class PowerMocTestCase {

@Test
public void constructionMockTest(){
ClassToMockclassToMock = PowerMockito.mock(ClassToMock.class);
try {
PowerMockito.whenNew(ClassToMock.class).withAnyArguments().thenReturn(classToMock);
ClassThatNewObjectclassNewObject = new ClassThatNewObject();
classNewObject.methodThatNewObject();

PowerMockito.verifyNew(ClassToMock.class,Mockito.times(1));
} catch (Exceptione) {
e.printStackTrace();
Assert. fail();
}
}
}

利用PowerMock mock对象构建的时候需要将执行对象创建的类放到PrepareForTest注解中,也就是上例中的ClassThatNewObject. class 。

上述case中首先通过正常的mock方式准备一个mock对象,然后通过PowerMockito.whenNew方法来指定ClassToMock在构建的时候直接返回已经准备好的classToMock对象。

PowerMock同时提供了PowerMockito.verifyNew方法,用于验证特定的类是否在被测方法内部进行了实例化,以及实例化了几次。

7. 跳过特定方法的调用

有的时候需要跳过特定的构造函数、方法、静态初始化代码等才能保证单元测试正常执行。下面分几种情况进行介绍:

7.1 跳过父类构造函数的调用。

有些情况我们的类需要继承一些来自第三方库的类,而有些类在单元测试中无法成功的构造,如下面例子所示:

public class ExampleWithEvilParent extends EvilParent {

private finalString message;

public ExampleWithEvilParent(Stringmessage) {
this.message= message;
}

public String getMessage() {
returnmessage;
}
}
public class EvilParent {

public EvilParent(){
System.loadLibrary("evil.dll");
}
}

父类EvilParent的构造函数中需要加载native的lib,这种情况在junit中直接运行通常会因为无法成功加载native lib而失败,所以在进行unit test的时候需要跳过父类构造函数的调用。具体Test Case如下:

@RunWith(PowerMockRunner.class)
@PrepareForTest(ExampleWithEvilParent.class)
public class ExampleWithEvilParentTest {

@Test
public voidtestSuppressConstructorOfEvilParent() throws Exception {
suppress(constructor(EvilParent.class));
finalString message = "myMessage";
ExampleWithEvilParenttested = new ExampleWithEvilParent(message);
assertEquals(message, tested.getMessage());
}
}

这种应用场景中,需要将子类添加到PrepareForTest注解中。然后使用suppress(constructor(EvilParent.class))将父类的构造函数跳过。使用suppress方法的时候需要将其进行导入

import static org.powermock.api.support.membermodification.MemberModifier.suppress;

7.2 跳过类自身的构造函数

如#5中所述,通过Whitebox.newInstance()来跳过构造函数而直接创建实例

7.3 跳过指定的方法调用

如下所示,类中getEvilMessage涉及native lib的加载,在junit无法直接调用

public class ExampleWithEvilMethod {

private finalString message;

public ExampleWithEvilMethod(Stringmessage) {
this.message= message;
}

public StringgetMessage() {
returnmessage + getEvilMessage();
}

private StringgetEvilMessage() {
System.loadLibrary("evil.dll");
return"evil!";
}
}

可以通过suppress(method(ExampleWithEvilMethod.class, "getEvilMessage"));通跳过该方法的调用,具体TestCase如下

@RunWith(PowerMockRunner.class)
@PrepareForTest(ExampleWithEvilMethod.class)
public class ExampleWithEvilMethodTest {

@Test
public voidtestSuppressMethod() throws Exception {
suppress(method(ExampleWithEvilMethod.class, "getEvilMessage"));
finalString message = "myMessage";
ExampleWithEvilMethodtested = new ExampleWithEvilMethod(message);
assertEquals(message, tested.getMessage());
}
}

7.4 跳过静态初始化

有些情况下被测试类含有静态初始化代码段,静态代码段会在类被加载的时候调用。很多情况下,由于条件不满足,unit test中执行类的静态初始化会失败。一个典型的例子是eclipse swt的一系列类,如org.eclipse.swt.widgets...它们在静态初始化的时候会加载一些native的lib,所以在unittest中无法成功加载。此时可以通过@SuppressStaticInitializationFor跳过静态初始化代码段。如:

@SuppressStaticInitializationFor ({ "org.eclipse.swt.widgets.Widget" , "org.eclipse.swt.widgets.Display" ,"org.eclipse.swt.widgets.Shell" })


8. PrepareForTest注解

前面的讲解中很多地方用到了PrepareForTest的注解。(如何理解PrepareForTest,何时使用preparefortest)

PrepareForTest注解是用来告诉PowerMock为unit test运行准备指定的类,通常是指那些需要对字节码进行修改的类。

那么何时需要将类放到PrepareForTest中呢?通常在mock final 类、包含final 、private、static、native方法的类以及需要返回一个mock对象的类。

可以这样理解,所有Mockito不能做而PowerMock可以做的事情都需要PrepareForTest。即mock static、private、final的时候相应包含private、static、final的类需要添加到PrepareForTest。而在mock对象构建的时候,需要将真实的new Object()过程换成成指定mock对象的返回,因此具体执行new Object()的类需要添加到PrepareForTest,如上面#6所述。

PrepareForTest可以将整个package作为需要准备得对象:

@PrepareForTest("com.mypackage.*")