使用Retrofit和Mockito進行可靠的Android API測試
測試與API互動的HTTP呼叫是一件令人生厭的複雜事情。測試一個真實的Web伺服器時,一大堆問題隨之產生:脆性測試(brittle test,因為網路或API本身的問題而導致的測試失敗)、速度減慢測試(slow test,每一次HTTP呼叫都要花費好幾秒)和不完全測試(“如何觸發一個速率限制越界用例?想一想,我只希望速率限制會起作用……”)。
像Android這樣的平臺HTTP理應是非同步呼叫,問題會變得更加複雜。如果在這些測試組合中新增計時器,那麼你就準備好在測試API呼叫上認輸吧。
解決這些問題並且練習這些HTTP呼叫的一個絕妙方法是,使用一個很好的Mockito(一個Java測試雙庫 double library)通用程式:
ArgumentCaptor與混合測試雙有幾分相似;有點類似存根(stub),也有點類似偵聽程式(spy),但不完全是其中任何一個。可以使用引數捕獲器捕獲並存儲傳給mock/stub的引數。然而這裡真正的亮點是對捕獲的引數進行方法呼叫,對於像Retrofit回撥有很大幫助。
譯註:Retrofit是一個Android & Java的型別安全REST客戶端。
有了Retrofit,我們可以發起一個API呼叫並提供一個回撥方法。當伺服器做出響應時,Mockito會使用響應資料執行回撥方法。
下面這些程式碼使用Github API查詢使用者程式碼倉庫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
getApi().repositories( "swanson" ,
new Callback<List<Repository>>() {
@Override
public
void success(List<Repository> repositories, Response response) {
if
(repositories.isEmpty()) {
displaySadMessage();
}
mAdapter.setRepositories(repositories);
}
@Override
public
void failure(RetrofitError retrofitError) { displayErrorMessage();
}
});
|
這裡有三個我們想要測試的用例:理想路徑(happy path,獲取一些程式碼倉庫並把傳遞給介面卡)、錯誤路徑(error path,向用戶提示伺服器錯誤)、特殊用例(special case, 向用戶提示沒有程式碼倉庫錯誤)。
如果你的測試依賴於在真實的API伺服器,那麼第二和第三個用例會很複雜。我瞭解到最近GitHub有一些DDOS問題,但你肯定不能依賴它們來測試你的錯誤用例!
然而我們可以通過ArgumentCaptor捕獲回撥引數,進而完全控制傳送的資料。
看一下對理想路徑的測試(我用的是Robolectri,建議你也嘗試一下):
1 2 3 4 5 6 7 8 9 |
Mockito.verify(mockApi).repositories(Mockito.anyString(), cb.capture());
List<Repository> testRepos =
new ArrayList<Repository>();
testRepos.add( new
Repository( "rails" ,
"ruby" , new
Owner( "dhh" )));
testRepos.add( new
Repository( "android" ,
"java" , new
Owner( "google" )));
cb.getValue().success(testRepos,
null );
assertThat(activity.getListAdapter()).hasCount( 2 );
|
captor(cb)捕獲到回撥,呼叫getValue()方法以後,通過success方法向它傳遞一些偽物件(dummy object)。
你可能會感嘆“啊哈(原來可以這麼簡單)”。呵呵,如果沒有也沒關係。接下來可以看一下對錯誤路徑的測試:
1 2 3 4 5 |
Mockito.verify(mockApi).repositories(Mockito.anyString(), cb.capture());
cb.getValue().failure( null );
assertThat(ShadowToast.getTextOfLatestToast()).contains( "Failed" );
|
像之前一樣,我們捕獲了回撥。但是這一次我們呼叫了failure方法,它模擬了一個API錯誤。如果我們需要更有針對性的錯誤處理(例如:如果返回狀態是401,就重定向再登陸;如果是500, 彈出一條普通的系統錯誤訊息),可以通過建立合適RetrofitError物件作為failure呼叫的引數。
目前為止,ArgumentCaptor的威力真的很贊。我們完全控制了捕獲到的物件,並且能夠給這些物件設定任意的資料或者觸發任意想要測試的錯誤。
為了讓內容更加豐富,下面是對一個特殊用例的測試:
1 2 3 4 5 6 7 8 |
Mockito.verify(mockApi).repositories(Mockito.anyString(), cb.capture());
List<Repository> noRepos =
new ArrayList<Repository>();
cb.getValue().success(noRepos,
null );
assertThat(ShadowToast.getTextOfLatestToast()).contains( "No repos :(" );
assertThat(activity.getListAdapter()).isEmpty();
|
(你可以在GitHub上找到示例的全部原始碼和工程檔案)。
有一個特殊細節要注意:如果在宣告捕獲器時使用了Mockito註解,
1 2 |
@Captor
private
ArgumentCaptor<Callback<List<Repository>>> cb;
|
請確保在設定中的某個地方添加了下面程式碼:
1 |
MockitoAnnotations.initMocks( this );
|
這種測試方法完全符合書中提到的所有特點:快速、健壯、易於使用。我們還可以通過它很容易地測試專案中很少出現的邊緣情況(會話超時、伺服器維護、特殊值),確保我們的應用正常執行。
雖然本文示例是專門針對某種棧(Android、Robolectric、Retrofit、Mockito),但是類似的方法幾乎適用於任何應用。
祝測試愉快!