PowerMock 框架也是属于 Mock 框架的一种,它是从 EasyMock 和 Mockito 两个框架扩展而来的,其最大的优点就是可以 mock 或者屏蔽一些私有、静态、final 等方法。

PowerMock 正如它的名字一样,是一种“强大”的 mock 框架。PowerMock 和 Mock 一样,主要为了解决代码中难以构建或者难以测试部分,它通过在测试用例运行期间修改字节码文件实现 mock、stub 或其它功能。它涵盖了 EasyMockMockito的所有功能,同时对这两个框架进行了扩展,弥补了这两个框架不能 mock 私有、静态、final 方法及变量等缺点。

PowerMock 框架支持 JUnit 以及 TestNG,同时支持多个版本的 JUnit 版本,分别为 JUnit 4.4+JUnit 4.0~4.3,在 PowerMock 2.0 版本之后,JUnit 3 已被废除。

Android 配置 PowerMock

Android Studio 中使用 PowerMock 框架需要添加如下依赖:

dependencies {
// powermock framework
def powerMockVersion = '2.0.0-beta.5'
// powermock junit4
testImplementation "org.powermock:powermock-module-junit4:${powerMockVersion}"
// powermock with mockito2
testImplementation "org.powermock:powermock-api-mockito2:${powerMockVersion}"
// powermock with easymock
testImplementation "org.powermock:powermock-api-easymock:${powerMockVersion}"
}

基本注释

  • @RunWith(PowerMockRunner.class)

    如果测试用例使用 PowerMock 框架运行,则需要在测试类前添加@RunWith(PowerMockRunner.class)注释,PowerMockRunner也可以替换成自定义的继承自PowerMockRunner的 Runner;

  • @PrepareForTest({SomeClass.class}, ...)

    此注释意为告诉 PowerMock 准备一些测试所必须的类,包括一些 final 修饰的类,包含静态方法或变量、私有方法或变量以及 native 修饰的方法。

    该注释可以用来注释某一(些)测试类,也可以修饰特定测试方法,这取决于你测试用例的设计,如果用来注释测试类,则 PowerMock 会为该测试类中的所有方法准备所指定的类,如果在测试类和测试方法都用PrepareForTest()注释,则测试类方法的注释会覆盖测试类的注释。

  • @PrepareOnlyThisForTest({SomeClass.class}, ...)

    PrepareForTest()方法类似,两者的区别在于PrepareOnlyThisForTest()只针对某个特定的类,不对类的层次结构进行操作,但是PrepareForTest()会对整个类的层次结构进行操作。

  • @PrepareEverythingForTest()

    此注释意为准备除特定的系统和测试相关类以外的所有类用来测试,但是同样会加载静态初始化块。

  • @PoweMockIgnore

    在正常情况下,PowerMock 会使用MockClassLoader加载所有的类,但是在某些情况下不需要使用 PowerMock 的类加载器加载,例如在 PowerMock 和 Robolectric 框架搭配使用时,此时需要使用@PowerMockIgnore对其进行注释,其参数可以是某一个类也可以包,例如

    @RunWith(PowerMockRunner.class)
    @PowerMockIgnore({"cc.istarx.MainActivity","android.*"})
    public class ExamplePowerMockTest {
    }

    但是使用@PowerMockIgnore注释的最大的缺点就是必须对每个不同的测试类都需要该注释,这样就造成了相同的代码在很多处都存在,给维护带来很大的不便。对于这种情况有两种方法可以解决:

    • 编写基类,并把公有的类和包在基类中使用@PowerMockIgnore注释,并让所有的测试类都继承该基类

      @RunWith(PowerMockRunner.class)
      @PowerMockIgnore({"cc.istarx.MainActivity","android.*"})
      public class BaseClass {
      // 基类
      }

      public TestClass1 extends BaseClass {
      @Test
      public void test1(){
      // do some test here
      }
      ...
      }
    • 利用 PowerMock 1.7.0 版本之后可以使用配置文件指定忽略加载的类和包,例如:

      powermock.global-ignore="cc.istarx.MainActivity" // 单个类
      powermock.global-ignore="cc.istarx.MainActivity","android.*" // 多个类和包

      所有用户定义的配置文件在目录org/powermock/extensions/configuration.properties中。如果在某一测试类中需要取消这些配置,则只需将globalIgnore值置为false即可。例如:

      @RunWith(PowerMockRunner.class)
      @PowerMockIgnore(globalIgnore = false)
      public TestClass {
      ...
      }

    @PrepareForTest@PrepareOnlyThisForTest@PrepareEverythingForTest@PowerMockIgnore注释同时出现时,@PrepareForTest@PrepareOnlyThisForTest优先级要高于@PowerMockIgnore,但是 @PrepareEverythingForTest的优先级要低于@PowerMockIgnore

操作私有属性和方法

正常情况下,为降低代码的侵入性,不介意修改私有变量或者操作私有方法和构造函数。但是为了达到更高的覆盖率,防止后面代码重构破坏原有功能,这种情况下操作私有变量、方法以及构造函数是很有必要的。

PowerMock 提供了一个用来操作私有属性、方法以及构建函数,该类中提供了操作私有变量、方法、构造函数的方法。例如:

Whitebox.newInstance(Class<T> classToInstantiate); // 不执行构造函数而实例化
Whitebox.ssetInternalState(Object object, Class<?> fieldType, Object value); // 私有属性的赋值
Whitebox.invokeConstructor(Class<T> classThatContainsTheConstructorToTest, Class<?>[] parameterTypes, Object[] arguments); // 执行私有的构建函数
Whitebox.invokeMethod(Class<?> klass, Object... arguments); // 执行私有方法
...

一个完整的例子如下:

被测试类:

public class WhiteboxDemo {
private String name;
private static WhiteboxDemo demo;

private WhiteboxDemo() { }

public WhiteboxDemo getInstance() {
if (demo == null) {
demo = new WhiteboxDemo();
}
return demo;
}

public String getName() { return name; }

private void setName(String name) { this.name = name; }
}

对应的测试用例:

@Test
public void WhiteBoxTest() throws Exception {
String name = "whitebox";

// 不执行构造函数而实例化
WhiteboxDemo demo = Whitebox.newInstance(WhiteboxDemo.class);

// 静态私有属性的赋值
Whitebox.setInternalState(WhiteboxDemo.class, "demo", demo);
// 非静态私有属性的赋值
Whitebox.setInternalState(demo, "name", name);
Field field = Whitebox.getField(WhiteboxDemo.class, "name");
assertEquals(name, field.get(demo));

// 执行私有的构建函数
WhiteboxDemo demo1 = Whitebox.invokeConstructor(WhiteboxDemo.class);
assertNotNull(demo1);

// 执行私有方法
Whitebox.invokeMethod(demo, "setName","setName");
assertEquals("setName", Whitebox.getInternalState(demo, "name")); // 获取私有变量
}

屏蔽执行

在被测代码中,某些方法的执行对测试带来了阻碍,比如 System.loadLibrary()等。类似的系统方法可以利用 PowerMock 框架可以模拟出来,但是某些情况下,比如某个变量、构造函数等,不能利用模拟的方法解决这类问题,PowerMock 提供了一种方法可以绕过这些方法、变量、构造函数的执行,极大的提高了测试的便利性。

PowerMock 用来绕过执行的方法是suppress(),该方法在org.powermock.api.support.membermodification.MemberModifier类中,该方法的参数可以是 MehtodFiedconstructor,如果同时绕过多个Field或者constructor,则可以构建一维数组进行传递。

绕过某个方法、变量、构造函数需要注意一下几点:

  • 测试用例所在的测试类必须要以@RunWith(PowerMockRunner.class)进行注释;
  • 如果要屏蔽某个类的静态初始化块,则需要对对测试类添加注释@SuppressStaticInitializationFor(class.want.to.suppress.ClassName);
  • 如果要对某个类的构造函数、方法、变量进行屏蔽,则需要在测试类之前添加@PrepareForTest()注释;

例如有如下代码:

public class SuppressDemo {
public String firstName = "test";
public String secondName;
public static int id;

static {
id = 123;
}

public SuppressDemo(String secondName) {
this.secondName = secondName;
}

public void setSecondName (String secondName) {
this.secondName = secondName;
}

public String updateSecondName (String name) {
setSecondName(name);
return this.secondName;
}
}

对应的测试用例如下:

@RunWith(PowerMockRunner.class)
// 屏蔽静态初始化块
@SuppressStaticInitializationFor("cc.istarx.powermockdemo.SuppressDemo")
public class ExampleTest {
@Test
public void suppressTest() {
// 屏蔽方法的执行
suppress(method(SuppressDemo.class,"setSecondName", String.class));
// 屏蔽构造函数,
suppress(constructor(SuppressDemo.class,String.class));
// 屏蔽变量
suppress(field(SuppressDemo.class,"firstName"));

// 屏蔽构造函数之后可以利用 WhiteBox.newInstance() 方法实例化参数
SuppressDemo demo = Whitebox.newInstance(SuppressDemo.class);
// firstName 变量被屏蔽,下面这句断言位真
assertNull(demo.getFirstName());
// 静态初始化块被屏蔽,下面这句断言位真
assertEquals(0, SuppressDemo.id);
// 对象成员变量都是默认值
assertNull(demo.secondName);
// 由于 setSecondName() 方法被屏蔽,所以 secondName 不会被更新
assertNull(demo.updateSecondName("suppress"));
}
}

Mock 静态方法

在实际项目测试中,被测代码中难免会有静态方法,给测试带来了一定的难度。PowerMock 比 Mockito 框架好的地方就是可以对静态、私有、final类型的方法等进行模拟。PoweMock 在模拟一个静态方法时有一套固定的流程,主要步骤如下:

  • @RunWith(PowerMockRunner.class)对测试类进行注释;

  • 对包含静态方法的类以@PrepareForTest()注释,所准备的类为包含静态方法的类,次注释可以是类级别也可以是测试方法级别;

  • 在测试用例中的步骤如下:

    PowerMock.mockStatic(WithStaticMethod.class);
    when(WithStaticMethod.getNum()).thenReturn(11);

    // 执行静态方法

    PowerMockito.verifyStatic(WithStaticMethod.class);
    WithStaticMethod.getNum();
  • verify的时候同样可以进行参数匹配或者次数验证:

    PowerMockito.verifyStatic(WithStaticMethod.class, times(1));
    WithStaticMethod.getNumWithNum(anyInt());

    必须在每个静态方法的验证之前调用PowerMockito.verifyStatic(Static.class)

举例:

// 被测试代码
public class WithStaticMethod {
public static int getNum() {
return 999;
}

public static int getNumWithNum(int num) {
return num;
}
}

// 测试用例代码
@Test
@PrepareForTest(WithStaticMethod.class)
public void mockStaticMethodTest() {
PowerMockito.mockStatic(WithStaticMethod.class);
when(WithStaticMethod.getNum()).thenReturn(11);
when(WithStaticMethod.getNumWithNum(9)).thenReturn(19);

assertEquals(11, WithStaticMethod.getNum());
assertEquals(19, WithStaticMethod.getNumWithNum(9));

PowerMockito.verifyStatic(WithStaticMethod.class, times(1));
WithStaticMethod.getNum();
PowerMockito.verifyStatic(WithStaticMethod.class, atLeast(1));
WithStaticMethod.getNumWithNum(eq(9));
}

Mock 私有方法

PowerMock 提供了验证私有方法的方法verifyPrivate(tested).invoke("privateMethodName", argument1),例如:

待测试代码:

public class VerifyPrivateAndConstructor {
private String privateMethod(String str) {
return str;
}
public String getStr() {
return privateMethod("test");
}
}

测试用例:

@Test
public void verifyPrivateMethod() throws Exception {
VerifyPrivateAndConstructor pr = mock(VerifyPrivateAndConstructor.class);

when(pr.getStr()).thenCallRealMethod();
pr.getStr();

PowerMockito.verifyPrivate(pr, times(1)).invoke("privateMethod", "test");
pr.getStr();
}

Mock 构造函数

Mock 构造函数时必须用@PrepareForTest(XXX.class)对测试方法或者类进行注释,并用PowerMock.whenNew()对构造函数进行 mock,例如:

@Test(expected = IllegalArgumentException.class)
@PrepareForTest(VerifyPrivateAndConstructor.class)
public void mockConstructorTest() throws Exception {

whenNew(VerifyPrivateAndConstructor.class).withAnyArguments().thenThrow(new IllegalArgumentException("error message"));
new VerifyPrivateAndConstructor("test");
}

总结

PowerMock 正如其名,确实很强大,可以解决一些测试中很难测试的点或者和框架、系统进行交互的地方,给测试带来了极大的便利性。PowerMock 也可以和其它框架进行配合使用,比如 Robolectric,关于 Robolectric 后续会有专门的介绍。

谢谢阅读,希望能给你带来帮助。

本文示例代码:PowerMock Demo