PowerMock 簡介
使用 PowerMock 以及 Mockito 實現單元測試
http://www.ibm.com/developerworks/cn/java/j-lo-powermock/
EasyMock 以及 Mockito 都因爲可以極大地簡化單元測試的書寫過程而被許多人應用在自己的工作中,但是這 2 種 Mock 工具都不可以實現對靜態函數、構造函數、私有函數、Final 函數以及系統函數的模擬,但是這些方法往往是我們在大型系統中需要的功能。PowerMock 是在 EasyMock 以及 Mockito 基礎上的擴展,通過定製類加載器等技術,PowerMock 實現了之前提到的所有模擬功能,使其成爲大型系統上單元測試中的必備工具。
單元測試模擬框架的功能及其實現簡介
單元測試在軟件開發過程中的重要性不言而喻,特別是在測試驅動開發的開發模式越來越流行的前提下,單元測試更成爲了軟件開發過程中不可或缺的部分。於是相應的,各種單元測試技術也應運而生。本文要介紹的 PowerMock 以及 Mockito 都是簡化單元測試書寫過程的工具。
Mockito 是一個針對 Java 的單元測試模擬框架,它與 EasyMock 和 jMock 很相似,都是爲了簡化單元測試過程中測試上下文 ( 或者稱之爲測試驅動函數以及樁函數 ) 的搭建而開發的工具。在有這些模擬框架之前,爲了編寫某一個函數的單元測試,程序員必須進行十分繁瑣的初始化工作,以保證被測試函數中使用到的環境變量以及其他模塊的接口能返回預期的值,有些時候爲了單元測試的可行性,甚至需要犧牲被測代碼本身的結構。單元測試模擬框架則極大的簡化了單元測試的編寫過程:在被測試代碼需要調用某些接口的時候,直接模擬一個假的接口,並任意指定該接口的行爲。這樣就可以大大的提高單元測試的效率以及單元測試代碼的可讀性。
相對於 EasyMock 和 jMock,Mockito 的優點是通過在執行後校驗哪些函數已經被調用,消除了對期望行爲(expectations)的需要。其它的 mocking 庫需要在執行前記錄期望行爲(expectations),而這導致了醜陋的初始化代碼。
但是,Mockito 也並不是完美的,它不提供對靜態方法、構造方法、私有方法以及 Final 方法的模擬支持。而程序員時常都會發現自己有對以上這些方法的模擬需求,特別是當一個已有的軟件系統擺在面前時。幸好 , 還有 PowerMock。
PowerMock 也是一個單元測試模擬框架,它是在其它單元測試模擬框架的基礎上做出的擴展。通過提供定製的類加載器以及一些字節碼篡改技巧的應用,PowerMock 現了對靜態方法、構造方法、私有方法以及 Final 方法的模擬支持,對靜態初始化過程的移除等強大的功能。因爲 PowerMock 在擴展功能時完全採用和被擴展的框架相同的 API, 熟悉 PowerMock 所支持的模擬框架的開發者會發現 PowerMock 非常容易上手。PowerMock 的目的就是在當前已經被大家所熟悉的接口上通過添加極少的方法和註釋來實現額外的功能,目前,PowerMock 僅支持 EasyMock 和 Mockito。
本文的目的就是和大家一起學習在 Mockito 框架上擴展的 PowerMock 的強大功能。
環境配置方法
對於需要的開發包,PowerMock 網站提供了”一站式”下載 : 從 此頁面中選擇以類似 PowerMock 1.4.10 with 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>
JUnit 版本 4.0-4.3 請參考清單 2,
清單 2
<properties> <powermock.version>1.4.10</powermock.version> </properties> <dependencies> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4-legacy</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>
JUnit 版本 3 請參考清單 3,
清單 3
<properties> <powermock.version>1.4.10</powermock.version> </properties> <dependencies> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit3</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 在單元測試中的應用
模擬 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(
) 語句來指定被引用的靜態方法返回任意需要的值,達到覆蓋測試的效果。
模擬構造函數
有時候,能模擬構造函數,從而使被測代碼中 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
。
模擬私有以及 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()
函數需要依次指定模擬對象、被指定的函數名字以及針對該函數的輸入參數列表。
結束語
以上列舉了擴展於 Mockito 版本的 PowerMock 的一部分強大的功能,特別是針對已有的軟件系統,利用以上功能可以輕易的完成清晰獨立的單元測試代碼,幫助我們提高代碼質量。
注:
本文中的部分測試代碼引用自 Johan Haleby 的 Untestable code with Mockito and PowerMock。
參考資料
學習
- Mockito 官方網站:Mockito 的官方站點 , 在這裏你可以找到完整的 Mockito 介紹、接口文檔,以及一些代碼示例。
- PowerMock 官方網站:PowerMock 的官方站點 , 在這裏你可以找到完整的 PowerMock 介紹、接口文檔,以及一些代碼示例。
- developerWorks Java 技術專區:這裏有數百篇關於 Java 編程各個方面的文章。
powermoker下載地址:https://code.google.com/p/powermock/wiki/Downloads?tm=2