前言
- 學習一樣東西的時候,從是什么開始,然后會去探究為什么以及這么做之后的好處是什么,當經(jīng)歷了這個過程之后。才能夠說是知道這個東西。
- 筆者在剛開始接觸單元測試的時候,只知道這個是測試用的,只知道調(diào)用這個類的這個方法是返回這個東西的,但是不知道怎么去測,為什么要去這樣子測,會覺得增加代碼量和工作負擔。
- 這個時候,就會被卡主。但是,如果是從更高一點的視角去看的話,就會知道怎么去測,我為什么要這樣子去測。
- 開發(fā)一個功能,首先從需求的提出,然后針對這個需求設計各種使用的用例。我們開發(fā)出一個功能無非就是為了滿足用戶各種使用場景的需求,根據(jù)各種不同的使用場景產(chǎn)生各種結(jié)果。
- 我們身為開發(fā)者,所寫的一些代碼也是為了實現(xiàn)功能。
- 如果可以在開發(fā)的同時,可以順便把每個用戶的使用場景都測試一遍,那該是多么爽的事情啊。這樣子寫出來的代碼就有依據(jù)。知道是
Cover
了什么的一些測試用例。 - Yeah! We have a thing for the shit! It is call the Test Drive Development(TDD)。It is born for this!
沒有測試之前
測試Pass之后
準備
新建類以及接口
LoginContract
LoginPresenter
LoginActivity
LoginPresenter和LoginActivity各自實現(xiàn)了自己的接口,在上面的接口中,我們是沒有定義任何一種方法,只是定義了一個模板。里面具體的方法都由接下來的用例所決定。
對于登錄這個功能來說,會有以下的測試用例:
用戶名為空的時候,顯示錯誤信息
密碼為空的時候,顯示錯誤信息
用戶名和密碼輸入成功,登錄驗證成功后進行跳轉(zhuǎn)
用戶名和密碼有輸入,但是驗證不通過,顯示錯誤信息
接下來,根據(jù)上面的測試用例,我們開始寫我們的單元測試代碼
下面只會選取第一個測試用例進行講解,完整的例子會分享在github上,戳這里
Go For the TDD
前期準備
加入Mockito
testCompile 'org.mockito:mockito-core:2.5.4'
在MVP中,業(yè)務邏輯都是寫在Presenter中,所以這里我們對LoginPresenter進行一個測試。右鍵點擊LoginPresenter自動生成對應的測試類。
在LoginPresenterTest類中的setup方法里做一些初始化的操作
public class LoginPresenterTest {
private LoginContract.View mView;
private Api api;
private LoginPresenter mPresenter;
@Before
public void setUp() throws Exception {
mView = mock(LoginContract.View.class);
api = mock(Api.class);
mPresenter = new LoginPresenter(mView, api);
}
}
主要是初始化presenter,presenter需要注入兩個對象來完成初始化。
我們把需要注入的兩個對象都Mock掉,被Mock掉的對象我們就可以自由操控它,讓他返回我們希望要的值。因為我們這里測試的是LoginPresenter,所以,與這個無關(guān)的對象的具體邏輯我們不用去關(guān)注。
關(guān)于Mockito的更多用法可以參考這篇文章Android單元測試(四):Mock以及Mockito的使用
測試第一個用例(用戶名為空的時候,顯示錯誤信息)
在LoginPresenterTest中新增一個@Test方法,新增后的代碼如下
public class LoginPresenterTest {
private LoginContract.View mView;
private Api api;
private LoginPresenter mPresenter;
@Before
public void setUp() throws Exception {
mView = mock(LoginContract.View.class);
api = mock(Api.class);
mPresenter = new LoginPresenter(mView, api);
}
@Test
public void testWhenTheUserNameIsEmpty() throws Exception {
}
}
接下來,我們要為這個測試用例寫上一些具體的測試邏輯,并感受TDD的美妙
首先,來寫一個偽“中文代碼”
presenter調(diào)用處理具體邏輯的方法
驗證當用戶名為空的時候,mView有沒有調(diào)用到我們期望的方法
以上就是這個測試用例對應的測試邏輯,是不是覺得上面這兩句話沒啥用,說了相當于沒說。
我們只需要去寫針對當前測試用例的測試邏輯,并驗證。
但是我覺得這才是單元測試美妙的地方,他可以把測試粒度分得很小,以至于我們能夠cover到每個case。
No code say a ***, show me the fucking source code
我們可以看到,很多方法是沒有的(上圖紅色的),我們只需要關(guān)注我們需要調(diào)用到哪一些方法,只需要關(guān)注我們的邏輯,一些模板代碼讓IDE幫我們生成。
按下快捷鍵(mac是option+enter
, windows是alt+enter
)
最終的結(jié)果就是,在LoginContract
中的對應的接口中自動生成了我們想要的方法。
最后,在我們實現(xiàn)了這個接口的類中實現(xiàn)這些方法,在給每個方法寫上具體的邏輯。
最終LoginActivity
如下
最終LoginPresenter
如下
以上就是完成了我們第一個測試用例的業(yè)務邏輯,接下來就Run
一下我們的測試方法,如果能夠Pass
,說明我們已經(jīng)Cover
了這個用例。那么就可以繼續(xù)開始寫下一個用例的測試方法,然后再去完善我們的邏輯代碼,再Run
一下我們新的用例的測試方法~如此循環(huán)。最后達到Cover
每個測試用例的目的。
看到綠色的真高興,說明我們的第一個測試用例已經(jīng)通過了。
接下來只需要重復上面的步驟,把剩下的測試用例Cover
到了就ok
總結(jié)
個人覺得在寫單元測試的時候,最難的地方在于測試用例的擇寫。
在看了前面的例子之后,有人會疑惑,我為什么要把獲取用戶名和獲取用戶密碼的那一部分抽出一個方法getInputUsername()
和getInputPwd()
,為什么不直接把用戶名和密碼當做參數(shù)傳給presenter的方法。
這樣子做的原因如下:
- 對Mock對象有一個更加深刻的理解,筆者剛接觸單元測試的時候,基本上都是被Mock對象給搞暈了,特別是類似這樣子的代碼
when(mView.getInputUsername()).thenReturn("louiszgm");
一開始不知道這樣子做的意義是什么,為什么要手動指定返回值,只知道這個方法是指定了Mock對象的特定方法返回了一個特定的值。
這個例子主要是用來去感受TDD的好處,很多地方都沒有去完善,比如:
- 在正式項目中,MVP的使用當然沒有這么簡陋,需要考慮
view
的泄漏之類的問題,也會根據(jù)每個公司的業(yè)務抽出一些基接口
。- Mockito的一些高級用法
- 結(jié)合Dagger2讓單元測試來的更爽
- 這些已經(jīng)有人寫了很多優(yōu)質(zhì)的文章,又在這里打一波廣告。關(guān)于單元測試框架的使用,推薦創(chuàng)神的單元測試系列的文章。
Android 單元測試: 首先,從是什么開始
個人感覺用TDD這個模式去開發(fā)的好處是:
對自己寫出來的代碼有信心,畢竟cover到了每個用例。
很多模板代碼都不用自己寫,讓關(guān)注點更多的關(guān)注到我們的邏輯