很久之前就有聽說(shuō)過(guò)mockito和PowerMock的大名了,無(wú)奈我司寫單元測(cè)試的風(fēng)氣不濃,加上一直以來(lái)業(yè)務(wù)繁忙,惰性使我一直沒(méi)有寫單元測(cè)試的習(xí)慣斋荞。
正好現(xiàn)在手頭上的是一個(gè)全新的項(xiàng)目,可以在初期有時(shí)間也有沖動(dòng)將各種需要的東西都用上荞雏。于是這幾天就好好學(xué)習(xí)了一番,感覺PowerMock的確是無(wú)比強(qiáng)大。
什么是mock
維基百科上是這么寫的:
在面向?qū)ο蟪绦蛟O(shè)計(jì)中,模擬對(duì)象(英語(yǔ):mock object凤优,也譯作模仿對(duì)象)是以可控的方式模擬真實(shí)對(duì)象行為的假的對(duì)象悦陋。程序員通常創(chuàng)造模擬對(duì)象來(lái)測(cè)試其他對(duì)象的行為,很類似汽車設(shè)計(jì)者使用碰撞測(cè)試假人來(lái)模擬車輛碰撞中人的動(dòng)態(tài)行為筑辨。
在單元測(cè)試中俺驶,模擬對(duì)象可以模擬復(fù)雜的、真實(shí)的(非模擬)對(duì)象的行為挖垛, 如果真實(shí)的對(duì)象無(wú)法放入單元測(cè)試中痒钝,使用模擬對(duì)象就很有幫助秉颗。
如果我們使用依賴注入的方式編寫代碼,例如Context通常都是外部傳入的:
class ClassA{
public static boolean staticFunc(Context context, int arg) {
...
}
}
這個(gè)方法如果不使用mock object的方法,我們很難脫離安卓環(huán)境去編寫單元測(cè)試,因?yàn)镃ontext是系統(tǒng)生成的痢毒。
而使用mock技術(shù)去模擬一個(gè)Context出來(lái),就可以在android studio中編寫并且脫離安卓環(huán)境運(yùn)行單元測(cè)試了。
PowerMock
powermock是一個(gè)流行的java mock框架,通過(guò)它我們可以很方便的實(shí)現(xiàn)模擬對(duì)象蚕甥。它實(shí)際上是繼承并且拓展了EasyMock哪替、Mockito等其他的流行框架。
在android studio上導(dǎo)入powermock框架很簡(jiǎn)單,只需要在build.gradle中添加dependencies就好了:
dependencies {
...
testCompile 'junit:junit:4.12'
testCompile 'org.powermock:powermock-core:1.6.1'
testCompile 'org.powermock:powermock-module-junit4:1.6.1'
testCompile 'org.powermock:powermock-module-junit4-rule:1.6.1'
testCompile 'org.powermock:powermock-api-mockito:1.6.1'
}
這里有個(gè)坑點(diǎn)菇怀,之前我用的是1.5.6版本的powermock,但是我的junit是4.12版本的,于是在使用@RunWith(PowerMockRunner.class)的時(shí)候會(huì)報(bào)錯(cuò):
org.powermock.reflect.exceptions.FieldNotFoundException: Field 'fTestClass' was not found in class org.junit.internal.runners.MethodValidator.
到stackoverflow上搜索到國(guó)外大神的回答是powermock小于1.6.1的版本在使用junit 4.12的一個(gè)bug,在1.6.1被修復(fù)凭舶。所以要么用junit 4.12 + powermock 1.6.1,要么使用junit 4.11 + powermock 1.5.6.
mock的簡(jiǎn)單用法
先說(shuō)一下最近我拿到的一個(gè)需求。我們的應(yīng)用的按鈕點(diǎn)擊啟動(dòng)其他應(yīng)用的響應(yīng)需要在服務(wù)器上配置爱沟。服務(wù)器上可能配的是包名啟動(dòng)應(yīng)用,也可能是action啟動(dòng)應(yīng)用,還有可能是Uri啟動(dòng)應(yīng)用帅霜。
所以我這樣寫了一個(gè)工具類:
public class AppUtils {
public static boolean startApp(Context context, StartAppParam param) {
...
}
public static class StartAppParam {
private String packageName;
private String activity;
private String action;
private String uri;
private List<String> categorys = new ArrayList<>();
...
}
}
在服務(wù)器上配置一個(gè)json,傳到客戶端解析成StartAppParam,然后調(diào)用AppUtils. startApp方法。這樣就可以實(shí)現(xiàn)這個(gè)需求了呼伸。
我們使用TDD的方式開發(fā)這個(gè)功能身冀。首先考慮只配置Action的方式啟動(dòng):
@Test
public void testOpenAppByAction() {
Context context = Mockito.mock(Context.class);
AppUtils.StartAppParam param = Mockito.mock(AppUtils.StartAppParam.class);
PowerMockito.when(param.getAction()).thenReturn("package");
assertTrue(AppUtils.startApp(context, param));
Mockito.verify(context, Mockito.times(1)).startActivity(Matchers.any(Intent.class));
}
首先,使用Mockito.mock方法可以創(chuàng)建一個(gè)模擬對(duì)象出來(lái)。我們這里使用模擬的Context就可以直接在android studio中運(yùn)行單元測(cè)試了括享。
同時(shí)param也用mock的方式創(chuàng)建了出來(lái),而且還模擬了它的getAction方法,讓該方法返回"package",表示配置了使用Action去啟動(dòng)應(yīng)用:
AppUtils.StartAppParam param = Mockito.mock(AppUtils.StartAppParam .class);
PowerMockito.when(param.getAction()).thenReturn("package");
然后Mockito.verify方法可以用來(lái)驗(yàn)證調(diào)用了方法的調(diào)用次數(shù),比如這里我們就驗(yàn)證了startActivity被調(diào)用了一次搂根。
mock 方法內(nèi)部創(chuàng)建的對(duì)象
當(dāng)然這個(gè)測(cè)試不充分,因?yàn)槲覀儧](méi)有驗(yàn)證到底是不是通過(guò)Action啟動(dòng)的。也就是說(shuō)我們還需要判斷是不是通過(guò)new Intent(param.getAction())的方式創(chuàng)建了一個(gè)Intent出來(lái)铃辖。
這就用到了PowerMock的一個(gè)很屌的功能了,它不僅可以在外部mock一個(gè)對(duì)象通過(guò)參數(shù)傳給需要測(cè)試的方法,更可以直接mock方法內(nèi)部創(chuàng)建的對(duì)象(比如這里的Intent)!
@RunWith(PowerMockRunner.class)
public class AppUtilsTest {
@Test
@PrepareForTest({AppUtils.class})
public void testOpenAppByAction() throws Exception {
Intent intent = Mockito.mock(Intent.class);
PowerMockito.whenNew(Intent.class).withArguments("package").thenReturn(intent);
Context context = Mockito.mock(Context.class);
AppUtils.StartAppParam param = Mockito.mock(AppUtils.StartAppParam.class);
PowerMockito.when(param.getAction()).thenReturn("package");
assertTrue(AppUtils.startApp(context, param));
Mockito.verify(context, Mockito.times(1)).startActivity(intent);
Mockito.verify(intent, Mockito.times(0)).setData(Matchers.any(Uri.class));
Mockito.verify(intent, Mockito.times(0)).addCategory(Matchers.anyString());
Mockito.verify(intent, Mockito.times(0)).setClassName(Matchers.anyString(), Matchers.anyString());
}
}
首先需要用@RunWith(PowerMockRunner.class)注解AppUtilsTest類,用@PrepareForTest({AppUtils.class})注解testOpenAppByAction方法,傳入的AppUtils.class表示需要在AppUtils類內(nèi)部實(shí)現(xiàn)mock操作剩愧。
然后mock一個(gè)Intent出來(lái),接著使用下面的方法使得使用new Intent("package")得到的Intent是我們mock出來(lái)的intent,注意這里連傳入的"package"參數(shù)也需要匹配才能得到我們mock出來(lái)的intent。否則只能得到null:
PowerMockito.whenNew(Intent.class).withArguments("package").thenReturn(intent);
所以我們?cè)诤竺嬷恍枰?yàn)證startActivity調(diào)用的intent是不是我們mock出來(lái)的對(duì)象,就可以驗(yàn)證是不是通過(guò)Action啟動(dòng)的應(yīng)用了:
Mockito.verify(context, Mockito.times(1)).startActivity(intent);
當(dāng)然,為了保險(xiǎn)我們可以順便確認(rèn)一下Intent的其他方法是不是沒(méi)有被調(diào)用到:
Mockito.verify(intent, Mockito.times(0)).setData(Matchers.any(Uri.class));
Mockito.verify(intent, Mockito.times(0)).addCategory(Matchers.anyString());
Mockito.verify(intent, Mockito.times(0)).setClassName(Matchers.anyString(), Matchers.anyString());
使用BDD的方式編寫單元測(cè)試
BDD (Behavior-driven development,行為驅(qū)動(dòng)開發(fā))通過(guò)用自然語(yǔ)言書寫非程序員可讀的測(cè)試用例擴(kuò)展了測(cè)試驅(qū)動(dòng)開發(fā)方法娇斩。也就是說(shuō)用bdd方式寫的代碼就連不是程序員的人也能看得懂,這種可讀性的重要性就不用我多費(fèi)口舌了吧仁卷。
其實(shí)Mockito的BDD方式的寫法我覺得并不是特別的像自然語(yǔ)言。所以我想用C++的單元測(cè)試框架Catch框架來(lái)舉例:
GIVEN("a enable stub publish server entry") {
StubPublishServerEntry entry(true);
entry.Start();
WHEN("publish service") {
entry.PublishService(service, on_result, on_success, on_error);
THEN("publish successfully") {
REQUIRE(service_entry != nullptr);
REQUIRE(service_entry->IsPublished());
REQUIRE(is_on_success);
REQUIRE_FALSE(is_on_error);
}
}
}
這是我之前的半成品項(xiàng)目中的一個(gè)代碼片段犬第。如果將代碼部分去掉,只留下GIVEN锦积、WHEN、THEN三個(gè)宏里面的東西,基本只有是懂英語(yǔ)的人都能看得懂這段代碼想做什么:
GIVEN("a enable stub publish server entry") {
...
WHEN("publish service") {
...
THEN("publish successfully") {
...
}
}
}
PowerMock也是支持BDD的(應(yīng)該說(shuō)Mockito是支持BDD的),我們可以將上面寫的測(cè)試用例改成BDD的寫法:
public void testOpenAppByAction() throws Exception {
Intent intent = Mockito.mock(Intent.class);
PowerMockito.whenNew(Intent.class).withArguments("package").thenReturn(intent);
Context context = Mockito.mock(Context.class);
AppUtils.StartAppParam param = Mockito.mock(AppUtils.StartAppParam .class);
//given
BDDMockito.given(param.getAction()).willReturn("package");
//when
assertTrue(AppUtils.startApp(context, param));
//then
BDDMockito.then(context).should().startActivity(intent);
BDDMockito.then(intent).should(Mockito.never()).setData(Matchers.any(Uri.class));
BDDMockito.then(intent).should(Mockito.never()).addCategory(Matchers.anyString());
BDDMockito.then(intent).should(Mockito.never()).setClassName(Matchers.anyString(), Matchers.anyString());
}
感覺是不是和自然語(yǔ)言還是差別蠻大的,我們改造改造,將一些方法改成通過(guò)import static的方式import:
public void testOpenAppByAction() throws Exception {
Intent intent = mock(Intent.class);
whenNew(Intent.class).withArguments("package").thenReturn(intent);
Context context = mock(Context.class);
AppUtils.StartAppParam param = mock(AppUtils.StartAppParam .class);
//given
given(param.getAction()).willReturn("package");
//when
assertTrue(AppUtils.startApp(context, param));
//then
then(context).should().startActivity(intent);
then(intent).should(never()).setData(any(Uri.class));
then(intent).should(never()).addCategory(anyString());
then(intent).should(never()).setClassName(anyString(), anyString());
}
這樣是不是好多了瓶殃?讓我們繼續(xù)改造:
public class AppUtilsTest {
@Mock
private Intent mIntent;
@Mock
private Context mContext;
@Mock
private AppUtils.StartAppParam mParam;
@Before
public void setUp() throws Exception {
whenNew(Intent.class).withArguments("package").thenReturn(mIntent);
}
@Test
@PrepareForTest({AppUtils.class})
public void testOpenAppByAction() {
given(mParam.getAction()).willReturn("package");
//when
assertTrue(AppUtils.startApp(mContext, mParam));
then(mContext).should().startActivity(mIntent);
then(mIntent).should(never()).setData(any(Uri.class));
then(mIntent).should(never()).addCategory(anyString());
then(mIntent).should(never()).setClassName(anyString(), anyString());
}
}
因?yàn)镮ntent充包、Context、AppUtils.StartAppParam都是需要在不同測(cè)試用例中經(jīng)常被用到的,我們將它寫成成員變量并且用@Mock實(shí)現(xiàn)自動(dòng)mock,省去Mockito.mock()方法的調(diào)用。
然后將whenNew方法放到由@Before注解的setUp()方法中基矮。
現(xiàn)在看testOpenAppByAction是不是簡(jiǎn)潔多了淆储?只要有一點(diǎn)代碼功底的人都能很容易看明白這個(gè)用例到底是用來(lái)驗(yàn)證什么的。
當(dāng)然,這里的BDD寫法和上面Catch的寫法比起來(lái)在像自然語(yǔ)言方面還是有點(diǎn)差距的家浇。
現(xiàn)在我們已經(jīng)將測(cè)試用例寫出來(lái)了本砰,就可以開始寫代碼讓這個(gè)測(cè)試用例通過(guò)了。像這樣先寫行為測(cè)試用例再寫代碼的開發(fā)方式就叫做BDD钢悲。
mock 靜態(tài)方法
我們下一個(gè)需要實(shí)現(xiàn)的功能是什么呢点额?就實(shí)現(xiàn)通過(guò)包名啟動(dòng)應(yīng)用吧。將設(shè)只配置了包名,但沒(méi)有配置Activity名莺琳。我們就需要先找到這個(gè)應(yīng)用的Launch Activity,然后再去啟動(dòng)應(yīng)用还棱。
所以我們?cè)贏ppUtils中新增了一個(gè)方法,用于從包名獲取Activity名:
public class AppUtils {
public static boolean startApp(Context context, StartAppParam param) {
...
}
public static String getLaunchActivityByPackage(Context context, String packageName) {
return null;
}
}
如果是正常的開發(fā)流程我們需要寫一個(gè)getLaunchActivityByPackage測(cè)試用例,再實(shí)現(xiàn)這個(gè)方法惭等。這里我就省略了這步,讓getLaunchActivityByPackage這個(gè)方法先不實(shí)現(xiàn),直接返回null,測(cè)試的時(shí)候直接mock就好了珍手。
之后我們?cè)偃憇tartAppByPackage的測(cè)試用例:
@Test
public void startAppByPackage() {
mockStatic(AppUtils.class);
given(AppUtils.startApp(any(Context.class), any(AppUtils.StartAppParam.class)))
.willCallRealMethod();
given(AppUtils.getLaunchActivityByPackage(any(Context.class), anyString()))
.willReturn("LauncActivity");
given(mParam.getPackageName()).willReturn("packageName");
//when
assertTrue(AppUtils.startApp(mContext, mParam));
//then
verifyStatic(); //開啟static方法的驗(yàn)證,需要開啟才能驗(yàn)證AppUtils.getLaunchActivityByPackage是否被調(diào)用
AppUtils.getLaunchActivityByPackage(any(Context.class), eq("packageName"));
then(mIntent).should().setClassName(mParam.getPackageName(), "LauncActivity");
then(mContext).should().startActivity(mIntent);
}
首先我們使用mockStatic去模擬AppUtils,然后配置AppUtils.startApp調(diào)用實(shí)際的方法,而getLaunchActivityByPackage直接返回"LauncActivity"。
在驗(yàn)證getLaunchActivityByPackage是否被調(diào)用的時(shí)候要先調(diào)用verifyStatic()辞做。
之后再用下面的方式驗(yàn)證是不是調(diào)用了AppUtils.getLaunchActivityByPackage并且傳入了"packageName"
AppUtils.getLaunchActivityByPackage(any(Context.class), eq("packageName"));
這里多說(shuō)一點(diǎn),假設(shè)getLaunchActivityByPackage是一個(gè)private的方法,我們可以用下面的方式去mock它:
when(AppUtils.class, "getLaunchActivityByPackage", any(Context.class), anyString())
.thenReturn("LauncActivity");
完整Demo
其他剩下的測(cè)試用例我就不一個(gè)一個(gè)去講了,基本上通過(guò)之前對(duì)PowerMock用法的介紹大家也應(yīng)該能自己實(shí)現(xiàn)了琳要。
完整的demo代碼可以從這里獲取