在博客Android單元測試之Mockito中,主要介紹了Mockito測試框架和基本使用。在博客結束時,我們提出了一個問題,由于Mockito的局限性,對final、private、static等方法不能mock,那如何對這樣的方法進行單元測試呢?我們是不是真的束手無策呢?肯定不是的啦,今天我們一起來學習PowerMockito測試框架,又是如何完美的彌補Mockito測試框架的不足呢。
PowerMockito簡介
PowerMock是一個擴展了其它如EasyMock等mock框架的、功能更加強大的框架。PowerMock使用一個自定義類加載器和字節碼操作來模擬靜態方法,構造函數,final類和方法,私有方法,去除靜態初始化器等等。通過使用自定義的類加載器,簡化采用的IDE或持續集成服務器不需要做任何改變。熟悉PowerMock支持的mock框架的開發人員會發現PowerMock很容易使用,因為對于靜態方法和構造器來說,整個的期望API是一樣的。PowerMock旨在用少量的方法和注解擴展現有的API來實現額外的功能。目前PowerMock支持EasyMock和Mockito。
PowerMockito入門
Gradle配置如下:
repositories {
jcenter()
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile "junit:junit:4.12"
testCompile "org.assertj:assertj-core:1.7.0"
testCompile "org.robolectric:robolectric:3.3.2"
// PowerMock brings in the mockito dependency
testCompile 'org.powermock:powermock-module-junit4:1.6.5'
testCompile 'org.powermock:powermock-module-junit4-rule:1.6.5'
testCompile 'org.powermock:powermock-api-mockito:1.6.5'
testCompile 'org.powermock:powermock-classloading-xstream:1.6.5'
}
PowerMock有三個重要的注解:
@RunWith(PowerMockRunner.class)
@PrepareForTest({YourClassWithEgStaticMethod.class})
@PowerMockIgnore("javax.management.*")
如果你的測試用例里沒有使用注解@PrepareForTest,那么可以不用加注解@RunWith(PowerMockRunner.class),反之亦然。當你需要使用PowerMock強大功能(Mock靜態、final、私有方法等)的時候,就需要加注解@PrepareForTest。這一點和Mockito的使用方式是類似的,要么使用這種注解的方式
@RunWith(PowerMockRunner.class)
@PrepareForTest({YourClassWithEgStaticMethod.class})
要么使用注解加代碼的方式
@PrepareForTest({YourClassWithEgStaticMethod.class})
MockitoAnnotations.initMocks(this);
其中@PrepareForTest注解是聲明需要進行mock的靜態類,如果你需要聲明多個靜態類,使用
@PrepareForTest({Example1.class, Example2.class, ...})
這種方式聲明。
最后就是@PowerMockIgnore注解,聲明package路徑,表示不使用PowerMockito來加載所聲明的package路徑的類。
PowerMockito使用
(1) 普通Mock:Mock參數傳遞的對象
測試目標代碼:
public class CommonExample {
public boolean callArgumentInstance(File file) {
return file.exists();
}
}
測試用例代碼:
public class CommonExampleTest {
@Test
public void testCallArgumentInstance() {
File file = PowerMockito.mock(File.class);
CommonExample commonExample = new CommonExample();
PowerMockito.when(file.exists()).thenReturn(true);
Assert.assertTrue(commonExample.callArgumentInstance(file));
}
}
注意到了么有,普通Mock不需要加@RunWith和@PrepareForTest注解,在這種情況下,使用Mockito也是可以實現的。上述步驟如下:
- 通過
PowerMockito.mock(File.class)
創建出一個mock對象 - 然后再通過
PowerMockito.when(file.exists()).thenReturn(true);
來指定這個mock對象具體的行為 - 再將mock對象作為參數傳遞個測試方法,執行測試方法。
(2) Mock方法內部new出來的對象
測試目標代碼:
public class CommonExample {
public boolean callArgumentInstance(String path) {
File file = new File(path);
return file.exists();
}
}
測試用例代碼:
public class CommonExampleTest extends BasePowerMockTestCase {
@Test
@PrepareForTest(CommonExample.class)
public void callCallArgumentInstance2() throws Exception {
File file = PowerMockito.mock(File.class);
CommonExample commonExample = new CommonExample();
PowerMockito.whenNew(File.class).withArguments("aaa").thenReturn(file);
PowerMockito.when(file.exists()).thenReturn(true);
Assert.assertTrue(commonExample.callArgumentInstance("aaa"));
File newFile = Mockito.mock(File.class);
newFile.exists();
Mockito.verify(newFile).exists();
}
}
當使用PowerMockito.whenNew().thenReturn()
方法時,必須加上注解@PrepareForTest和@RunWith,注解@PrepareForTest里寫的類是需要mock的new對象代碼所在的類。需要mock的對象是在方法內部new出來的,這是一種比較常見的mock方式。 步驟(已經講過的步驟省略):
- 通過
PowerMockito.whenNew(File.class).withArguments("aaa").thenReturn(file);
來指定當以參數為"aaa"創建File對象的時候,返回已經mock的File對象。 - 在測試方法之上加注解
@PrepareForTest(CommonExample.class)
,注解里寫的類是需要mock的new對象代碼所在的類。
(3) Mock普通對象的final方法
測試目標代碼:
public class ClassDependency {
public final boolean isAlive() {
// do something
return false;
}
}
public class CommonExample {
public boolean callFinalMethod(ClassDependency dependency) {
return dependency.isAlive();
}
}
測試用例代碼:
public class CommonExampleTest extends BasePowerMockTestCase {
@Test
@PrepareForTest(ClassDependency.class)
public void callFinalMethod() throws Exception {
ClassDependency depencency = PowerMockito.mock(ClassDependency.class);
CommonExample commonExample = new CommonExample();
PowerMockito.when(depencency.isAlive()).thenReturn(true);
Assert.assertTrue(commonExample.callFinalMethod(depencency));
}
}
Mock的步驟和之前的一樣,只是需要在測試方法之上加注解@PrepareForTest(ClassDependency.class),注解里寫的類是需要mock的final方法所在的類。
(4) Mock普通類的靜態方法
測試目標代碼:
public final class Utils {
public static String generateNewUUId() {
return UUID.randomUUID().toString();
}
}
測試用例代碼:
@PrepareForTest(Utils.class)
public class PowerMockSampleTest extends BasePowerMockTestCase {
@Test
public void testPrintUUID() throws Exception {
PowerMockito.mockStatic(Utils.class);
PowerMockito.when(Utils.generateNewUUId()).thenReturn("FAKE UUID");
PowerMockSample sample = new PowerMockSample();
assertThat(sample.printUUID()).isEqualTo("FAKE UUID");
}
}
當需要mock靜態方法的時候,必須加注解@PrepareForTest和@RunWith。注解@PrepareForTest里寫的類是靜態方法所在的類。
(5) Mock 私有方法
測試目標方法:
public class CommonExample {
public boolean callPrivateMethod() {
return isExist();
}
private boolean isExist() {
return false;
}
}
測試用例代碼:
public class CommonExampleTest extends BasePowerMockTestCase {
@Test
@PrepareForTest(CommonExample.class)
public void testCallPrivateMethod() throws Exception {
CommonExample commonExample = PowerMockito.mock(CommonExample.class);
PowerMockito.when(commonExample.callPrivateMethod()).thenCallRealMethod();
PowerMockito.when(commonExample, "isExist").thenReturn(true);
Assert.assertTrue(commonExample.callPrivateMethod());
}
}
和Mock普通方法一樣,只是需要加注解@PrepareForTest(CommonExample.class),注解里寫的類是私有方法所在的類。
(6) Mock系統類的靜態和final方法
目標測試方法:
public class CommonExample {
public String callSystemStaticMethod(String str) {
return System.getProperty(str);
}
}
測試用例代碼:
public class CommonExampleTest extends BasePowerMockTestCase {
@Test
@PrepareForTest(CommonExample.class)
public void callSystemStaticMethod() {
CommonExample commonExample = new CommonExample();
PowerMockito.mockStatic(System.class);
PowerMockito.when(System.getProperty("aaa")).thenReturn("bbb");
Assert.assertEquals("bbb", commonExample.callSystemStaticMethod("aaa"));
}
}
和Mock普通對象的靜態方法、final方法一樣,只不過注解@PrepareForTest里寫的類不一樣 ,注解里寫的類是需要調用系統方法所在的類。
(7) Mock普通類的私有變量
測試目標方法:
public class PowerMockSample {
private static final int STATE_NOT_READY = 0;
private static final int STATE_READY = 1;
private int mState = STATE_NOT_READY;
public boolean doSomethingIfStateReady() {
if (mState == STATE_READY) {
// DO some thing
return true;
} else {
return false;
}
}
}
測試用例代碼:
public class PowerMockSampleTest extends BasePowerMockTestCase {
@Test
public void testDoSomethingIfStateReady() throws Exception {
PowerMockSample sample = new PowerMockSample();
Whitebox.setInternalState(sample, "mState", 1);
assertThat(sample.doSomethingIfStateReady()).isTrue();
}
}
當需要mock私有變量mState的時候,不需要加注解@PrepareForTest和@RunWith,而是使用Whitebox來mock私有變量mState并注入你預設的變量值。
上面介紹了PowerMockito的簡單使用,對static方法、構造方法、private方法以及final方法的mock支持,專治Mockito各種不服。不過還有其他的API沒有介紹,感興趣的同學可以參考PowerMockito官方wiki。
PowerMockito原理簡單介紹
我們看一下PowerMockito的依賴:
可以看出來,它有兩個重要的依賴:javassist和objenesis,javassist是一個修改java字節碼的工具包,objenesis是一個繞過構造方法來實例化一個對象的工具包。由此看來,PowerMock的本質是通過修改字節碼來實現對靜態和final等方法的mock的。
我們結合上面的例子來介紹:
testCallPrivateMethod()被注解@PrepareForTest(CommonExample.class)標注以后,在運行時,會創建一個org.powermock.core.classloader.MockClassLoader對象來加載該測試用例使用到的類(系統類除外)。
PowerMockito會根據你的mock要求,去修改寫在注解@PrepareForTest里的class文件(當前測試類會自動加入注解中),以滿足特殊的mock需求。例如:去除final方法的final標識,在靜態方法的最前面加入自己的虛擬實現等。
如果mock的是系統類的final方法和靜態方法,PowerMockito不會直接修改系統類的class文件,而是修改調用系統類的class文件,以滿足mock需求
PowerMockito踩坑
1.使用PowerMockito會提示classloader錯誤
加入注解:@PowerMockIgnore(YourPackagePath.*)來解決由于MockClassLoader造成的class加載錯誤,通過這個注解可以讓報錯的類使用系統的ClassLoader來加載報錯類。如下:
setUp now
java.security.NoSuchAlgorithmException: class configured for SSLContext: sun.security.ssl.SSLContextImpl$TLSContext not a SSLContext
at sun.security.jca.GetInstance.checkSuperClass(GetInstance.java:260)
at sun.security.jca.GetInstance.getInstance(GetInstance.java:237)
at sun.security.jca.GetInstance.getInstance(GetInstance.java:164)
at javax.net.ssl.SSLContext.getInstance(SSLContext.java:156)
at me.ele.shopcenter.network.request.CustomClient.getSslSocketFactory(CustomClient.java:124)
這種就是ClassLoader造成的SSLContext對象不一致的問題,只要加載注解@PowerMockIgnore({"sun.security.*", "javax.net.*"})
就可以解決。
2.PowerMockito 和Mockito mock出來的對象不能相互使用,否則會拋出異常
小結
小伙伴們,感受到PowerMockito的強大了吧。根據實踐發現,推薦使用JUnit4+Mockito+PowerMockito測試工具,在Java單元測試中可以說是無所不能。
感謝您對本篇博客的關注,要是有什么不足歡迎指正!前面介紹了在JVM運行環境下的單元測試,要是在Android呢?PowerMockito無法解決,因為PowerMockito是運行在JVM上,無法調用Android相關的類和方法。仔細的同學,你會發現,在上面的Gradle配置中有這么一句testCompile "org.robolectric:robolectric:3.3.2"
,在前面的博客中都沒有講到robolectric。其實robolectric就是解決在JVM中可以調用Android相關的類和方法進行單元測試的,具體情況,請關注博客Android單元測試之Robolectric。