写单元测试时常会使用 mocking framework,因为它能帮助我们轻鬆建立 mocked object,不必再为了单元测试而写假物件,更容易对待测物件隔绝外部相依,进而降低写单元测试的负担。目前有许多主流 mocking framework,如最受欢迎的 Mockito,以及本篇文章的主角 — PowerMock。
PowerMock 是基于 Mockito 并扩充了许多实用测试方法。PowerMock 让开发者可以在测试中轻易的模拟 private method, static, final class 甚至是 constructor 等。简而言之, Mockito 不能做到的事,PowerMock 几乎都能一手包办!不过,在 PowerMock 官方的 README 中说了一段耐人寻味的话:
Please note that PowerMock is mainly intended for people with expert knowledge in unit testing.
Putting it in the hands of junior developers may cause more harm than good.
既然 PowerMock 这么强大,为何作者做出此评论呢? 接着我将加以分析与探讨:
PowerMock 的优点
强大的 mock 功能: 能因应各种难以撰写测试的情况,尤其是针对棘手的 legacy code,可以在不改 production code 的条件下加入测试。用法与 Mockito 类似: 对于熟悉 Mockito 的广大使用者来说能快速上手。PowerMock 的缺点
1. 相同的 API
因为 PowerMock 与 Mockito 有许多 API 的用法是一模一样的,但两者间的支援度与行为却不同。所以如果在 IDE 没有特别指出,写出来的程式都会是一模一样,因此容易被误用,更难以 debug,而且开发者不该花时间 debug 测试程式码。
2. 複杂的用法
PowerMock 提供了很多的神奇的 API,例如 @PrepareForTest
, @SuppressStaticInitializationFor
, Whitebox
等等,当开发者在 test case 写了一堆 PowerMock 的各种神奇用法后,写测试的时间可能比开发更久,不仅如此,可能三个月后就看不懂,其他同事读起来也困难,造成不好维护。此外,若升级 Powermock 的版本,也可能会让旧的写法失效,甚至导致意想不到的问题。
3. Overhead
优秀的单元测试速度要快,不过 PowerMock 的初始化时间比 Mockito 更久,如果测试数量不多,也许还可以忍受;但随着专案渐渐庞大,累积了上百上千的测试案例,此时就容易让人下 skip test 指令,那就失去了写测试的意义了。
4. 容易冲突其他 library
PowerMock 容易与其他 library 产生冲突,例如某些版本的 mockito, javassist。尤其是大型专案中常会用到很多 library,万一有冲突时往往会消耗大量成本,在 stackoverflow 上已有许多苦主:
java.lang.NoSuchMethodError...PowerMock throws NoSuchMethodError5. 可能写出不好的单元测试
对于 PowerMock 来说, production code 的实作细节一览无遗,所以开发者很可能会写很多 arrange、一大堆的 mock/stub。导致每当程式码有小改动或重构时,就容易造成测试失败,让 test case 难以维护。
6. 容易忽略 code design
因为 PowerMock 如此 powerful,容易使开发者过于依赖与滥用,原因很简单,因为无论 production code 再怎么杂乱无章都能够写出单元测试,久而久之让人容易忽略 code design。这是我认为它最大的缺点。
如果你对于上面提的几点有感,觉得 PowerMock 弊大于利,或觉得现阶段不适合使用,因而决定弃用,那可以参考以下的方法 — 重构。
以重构取代 PowerMock
重构是为了提高程式码的可测试性(Testability),如果可测试性高,可维护(Maintainability)、可读(Readability)、可理解(Understandability) 性自然而然就提高了,这对专案的健康是有帮助的。但重构已经写好的程式是有一定风险的,重构前最好是搭配 code review、整合测试、end-to-end 测试等方案来防止重构时意外产生的 bug。
以下是几个 PowerMock 常见的 use case,我将提供几个重构方法与思路来取代 PowerMock:
Mock Static class/method
PowerMock 可以轻易的 mock static,我相信这应该是 PowerMock 受欢迎的理由。
虽然 static 使用方便、效能较快,但也因此常被滥用,造成物件隐含相依、维护困难、不易测试等问题,因此在使用 static 之前应以更严苛的标準来检视。以 getProperty()
为例:
// bad, it may cause error when server is shutdown.public static getProperty(String key) { return server.getProperty(key);}
从程式的角度看起来似乎没问题,但实际上在 Server 尚未启动时可能产生错误或是没有回传值。因为这个 method 相依了 Server 环境,所以不适合作为 static method,应该改成 instance method。
良好的 static method 不应该有外部环境依赖,自然就不需要被 mock,因此我不建议 mock static。虽然如此,实务上仍可能有不得不 mock static 的情形。好消息是 Mockito 3.4.0 发布了 mockStatic()
功能来模拟 static method,因此就可不需使用 PowerMock。
WhiteBox
PowerMock.Whitebox 是利用 Reflection 来绕过物件封装的特性,允许 test case 直接存取物件的 private field, method,例如
String foo = Whitebox.getInternalState(Foo.class, "FIELD_NAME");String bar = Whitebox.invokeMethod(MyUtil.class, "getStringFromArray", (Object)arrayOfStrings);
一般而言,在测试中存取 private 并不是好的做法,但如果非测不可的话,还是有其他出路的。
例如在测试中存取 private field,可以考虑在 production code 加上 package-private getter/setter,专门给单元测试使用;如果想要在 test case 中单独测试或呼叫 private method,这就表示该 method 也许应该属于另一个 class,此时可以考虑使用 Delegate Method
委派另一个类别。或者更简单粗暴的方式是使用 Reflection。
doNothing
例如你想要验证 getData
的回传值,却不想执行与测试不相干的 private method processA
时,可以使用 PowerMock 的 doNothing()
public Data getData(String key) { Cache cahce = getCache(); init(cache); if (flag) { processA(); } else { processB(); processC(); } calculate(); return cache.get(key);}@Testpublic void data_should_be_blabla() { // arrange PowerMockito.doNothing().when(myClass, method(MyClass.class, "processA")) ... // act Data result = myClass.getData(key); ...}
之所以会想要使用 doNothing()
,通常是因为程式做了太多事情。可以思考 getData()
是否违反 Single Responsibility Principle? 此时可以考虑使用 Delegate Method
委派另一个类别,权责分明,测试自然就好写。
Mock System Class
假设有一 method isLate()
相依了系统时间,所以每次执行测试时可能会有不同的结果,因此需要 mock System.class 来模拟系统时间。
// bad design. hard to test.public boolean isLate() { long now = System.currentTimeMillis(); return now > 1500000L ? true : false;}
而比较好的做法是:不让 method 自己去请求现在的时间,而是由 caller 藉由参数传递进去,有点像依赖注入 (Dependency Injection, DI) 的观念,透过 DI 能够使我们更容易控制输入端的资料。经过重构后的程式码,甚至连 mocking framework 都不需要了,如果能不依赖于 framework,通常会是个更好的 practice:
// betterpublic boolean isLate(long now) { return now > 1500000L ? true : false;}@Testpublic void exceed_some_time_is_late() { // arrange long now = 1600000L; MyClass myClass = new MyClass(); // act boolean result = myClass.isLate(now); // assert assertTrue(result);}
Mock Constructor
若写单元测试时,欲将 new
回传成不同的实作的 instance,这时可以使用 PowerMock 提供的 whenNew()
:
// In MyClass (SUT)public void doSomething() { Dependency dependency = new Dependency(); dependency.doSomething(); // ...}@Testpublic void whenNew_example() { // arrange Dependency mocked = mock(Dependency.class); PowerMockito.whenNew(Dependency.class) .withNoArguments() .thenReturn(mockedDependency); // bad // act sut.doSomething(); ...}
但其实有更好的替代方案:就是用依赖注入。我们先产生 mocked object,做好初始设定后,再透过 constructor 的参数的方式传入待测物件。如此一来不仅程式增加了弹性,也可以达到的测试目的。
// betterpublic MyClass(Dependency dependency) { this.dependency = dependency;}@Testpublic void alternatives_of_whenNew_example() { // arrange Dependency mockedDependency = mock(Dependency.class); MyClass sut = new MyClass(mockedDependency); // act sut.doSomething(); ...}
何时该用 PowerMock
虽然之前提到了 PowerMock 的一些缺点,但它并非一无是处,存在即合理,它在某些情境下仍然可以派上用场。我认为最适合使用 PowerMock 的场景是在重构和测试 legacy code 时。当我们需要重构一个缺乏单元测试保护的 legacy code 时,通常会先写一个大範围的测试,然后进行重构,让它具备基本的可测试性,最后再撰写单元测试。
但实际上 legacy code 里面什么鬼故事都有,例如程式相依时间或外部 API 等难以写整合测试的场合,或是可能需要用到 test double 时,这时 PowerMock 就派上用场了。正如前面所提到的:对于 PowerMock 来说,production code 的实作细节一览无遗。我们可以轻鬆地控制 production code 的各种行为,并且可以使用 PowerMock 的各种 API 来撰写测试案例。这样一来,legacy code 就具备了基本的保护和验证机制,使开发者在进行重构时更加自信和大胆。
结语
PowerMock 是一个功能强大的单元测试工具,但同时也必须承认,如果使用不当,容易让开发人员忽略程式码品质,导致未来需要花费更多的开发和维护成本。同样地,如果对于测试不熟悉的人使用 PowerMock,可能会让他们不知道如何写出优秀的测试案例和程式码。
如果测试程式码中大量使用 PowerMock,这可能意味着 production code 的可测试性不佳。因此,为了考虑专案的长远发展,我建议开发者在开发过程中遵循良好的设计原则,避免产生 code smell。在撰写单元测试时,如果有需要,只需使用 Mockito 即可。
总结而言,我认为 PowerMock 仅适用于 legacy code,不应该在正在开发中的系统中使用。
References
本文转录自
https://kaisheng714.github.io/articles/drawback-of-powermock