前言
首先祥诽,對于MVP枫吧、RxJava還不了解的同學宴倍,請先閱讀這幾篇文章:
了解 Retrofit痹束、okHttp检疫,直接看Squre官網(wǎng)
之所以說解耦,很大程度是 MVP参袱、Rxjava电谣、Retrofit 在 java工程 就能使用,本身不依賴Android SDK抹蚀。這一點對Android單元測試至關(guān)重要剿牺。
MVP & RxJava在2015年已經(jīng)很火了,加上2016年發(fā)布正式版的 OkHttp3.0 & Retrofit2.0 火上澆油环壤,全世界簡直炸開了鍋晒来,Android開發(fā)有了質(zhì)的飛躍(代碼層面)。
國內(nèi)Android開發(fā)者逐漸成熟郑现,翻墻越來越方便湃崩,國外的技術(shù)在國內(nèi)使用順理成章荧降。目前,國內(nèi)狀況是攒读,Android開發(fā)者不缺朵诫,缺的是大量Android中級開發(fā)者。因此薄扁,學會使用MVP剪返、RxJava、Retrofit邓梅、Mockito單元測試勢在必行脱盲。
逆水行走,不進則退日缨。
請求User數(shù)據(jù)钱反,并在顯示
User
bean:
public class User {
public int uid;
public String name;
}
UserView
,網(wǎng)絡加載完User數(shù)據(jù)匣距,回調(diào)onUserLoaded(user)
:
public interface UserView {
void onUserLoaded(User user);
}
UserService
面哥,Retrofit
代理的請求接口:
public interface UserService {
@GET("user/{uid}.json")
Observable<User> loadUser(@Path("uid") int uid);
}
與View (Activity)
交互的UserPresenter
接口、以及實現(xiàn)UserPresenterImpl
:
public interface UserPresenter {
void loadUser(int uid);
}
public class UserPresenterImpl implements UserPresenter {
UserService userService;
UserView userView;
public UserPresenterImpl(UserView userView) {
this.userView = userView;
userService = new Retrofit.Builder().baseUrl("http://**.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
.create(UserService.class);
}
@Override
public void loadUser(int uid) {
// 異步網(wǎng)絡請求User數(shù)據(jù)墨礁,并在onNext(user)返回
userService.loadUser(uid)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<User>() {
@Override
public void onNext(User user) {
userView.onUserLoaded(user);
}
......
});
}
}
MainActivity
:
public class MainActivity extends Activity implements UserView {
UserPresenter userPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
userPresenter = new UserPresenterImpl(this);
}
@Override
public void onUserLoaded(User user) {
textView.setText(user.toString());
}
}
為何解耦幢竹?
前言中提及耳峦,RxJava
與Retrofit
是不依賴Android SDK
的獨立的第三方庫恩静,MVP模式通過接口編程,把依賴Android SDK
的View層(Activity)
與Presenter蹲坷、Model
隔離驶乾。這里說的Model
是指retrofit
和代理的Service
網(wǎng)絡請求接口,對于Model
層依賴與Android SDK
的DAO循签、sqlite
级乐,之后會討論。
當P層县匠、網(wǎng)絡M層不依賴Android SDK
风科,我們就可以用JUnit
寫單元測試,并直接運行在JVM
上了乞旦。
JUnit4+Mockito單元測試
很多Android開發(fā)的同學贼穆,不了解單元測試。對于不是測試專業(yè)出身兰粉、又沒技術(shù)大牛調(diào)教過的程序猿故痊,缺乏單元測試知識,比比皆是玖姑。要了解單元測試愕秫,推薦閱讀:
美團點評技術(shù)團隊 的《Android單元測試研究與實踐》
鄒小創(chuàng) 《Android單元測試(二):再來談談為什么》
老實說慨菱,我也是2016年初,才真正接觸單元測試戴甩。2016年3月才正式對項目寫單元測試符喝。寫了一個多月,越來越意識到單元測試的重要性甜孤。單元測試達到的目的洲劣,總結(jié)成兩點:
- 快速開發(fā)
- 提高代碼質(zhì)量
你沒看錯,確實是“快速”课蔬!
對于需求“請求User囱稽,并顯示”,噼里啪啦寫完Presenter二跋、Service战惊、Activity
,需要 編譯扎即、運行 在真機or模擬器才能debug吞获,如果寫錯了,修改代碼后谚鄙,還要編譯各拷、運行在真機....還寫錯,修改闷营、編譯烤黍、運行.... 小型項目怎么也要40s~1分鐘吧,還要花幾分鐘時間手動操作界面...用Android Sutdio debug或Log.....這個過程太漫長了!!
如果你學會寫Junit
單元測試傻盟,可以直接對單個Presenter速蕊、Service
編譯運行,不需要關(guān)心是否受到其他類的代碼or網(wǎng)絡環(huán)境娘赴、服務器是否正常的影響规哲。運行一下就幾秒鐘,Junit
和Mockito
的錯誤提示诽表,還讓你快速定位問題唉锌。
瞎逼逼了那么久释移,該上代碼眉踱。
Presenter單元測試
打開UserPresenter
,對著類名 右鍵 -> Go To -> Test
創(chuàng)建OK之后冷蚂,你會得到UserPresenterTest.java
:
public class UserPresenterTest {
@Before
public void setUp() throws Exception {
}
@Test
public void testLoadUser() throws Exception {
}
}
要對UserPresenterImpl
進行單元測試议双,還需要做一點點改進:
public class UserPresenterImpl implements UserPresenter {
UserService userService;
UserView userView;
// 讓外部傳入UserService & UserView
public UserPresenterImpl(UserService userService, UserView userView) {
this.userService = userService;
this.userView = userView;
}
...
}
import static org.mockito.Mockito.mock;
public class UserPresenterTest {
UserPresenter userPresenter;
UserView userView;
UserService userService;
@Before
public void setUp() throws Exception {
RxUnitTestTools.openRxTools();
// 生成mock對象
userView = mock(UserView.class);
userService = mock(UserService.class);
userPresenter = new UserPresenterImpl(userService, userView);
}
}
注意痘番,這里import static org.mockito.Mockito.mock
,靜態(tài)引用org.mockito.Mockito
的mock()
靜態(tài)方法。我們不用自己敲這句import
汞舱,通過代碼補全提示就可以自動生成了伍纫,如圖:
這里有行RxUnitTestTools.openRxTools()
到底是什么?
public class RxUnitTestTools {
private static boolean isInitRxTools = false;
/**
* 把異步變成同步昂芜,方便測試
*/
public static void openRxTools() {
if (isInitRxTools) {
return;
}
isInitRxTools = true;
RxAndroidSchedulersHook rxAndroidSchedulersHook = new RxAndroidSchedulersHook() {
@Override
public Scheduler getMainThreadScheduler() {
return Schedulers.immediate();
}
};
RxJavaSchedulersHook rxJavaSchedulersHook = new RxJavaSchedulersHook() {
@Override
public Scheduler getIOScheduler() {
return Schedulers.immediate();
}
};
// reset()不是必要莹规,實踐中發(fā)現(xiàn)不寫reset(),偶爾會出錯泌神,所以寫上保險^_^
RxAndroidPlugins.getInstance().reset();
RxAndroidPlugins.getInstance().registerSchedulersHook(rxAndroidSchedulersHook);
RxJavaPlugins.getInstance().reset();
RxJavaPlugins.getInstance().registerSchedulersHook(rxJavaSchedulersHook);
}
}
這個類是讓RxJava&RxAndroid
的Schedulers.io()
和AndroidSchedulers.mainThread()
轉(zhuǎn)換成Schedulers.immediate()
良漱,從而讓Obserable
從異步變同步。
然后欢际,寫testLoadUser()
:
@Test
public void testLoadUser() throws Exception {
User user = new User();
user.uid = 1;
user.name = "kkmike999";
when(userService.loadUser(anyInt())).thenReturn(Observable.just(user));
userPresenter.loadUser(1);
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(userService).loadUser(1);
verify(userView).onUserLoaded(captor.capture());
User result = captor.getValue(); // 捕獲的User
Assert.assertEquals(result.uid, 1);
Assert.assertEquals(result.name, "kkmike999");
}
讓我解析一下:
when...thenReturn...
when(userService.loadUser(anyInt())).thenReturn(Observable.just(user));
當調(diào)用userService.loadUser(...)
母市,參數(shù)為任意int,返回Observable.just(user)
對象损趋。
verify
verify(userService).loadUser(1);
患久,驗證 userService.loadUser(...)
是否被調(diào)用,并校驗傳入?yún)?shù)uid==1
浑槽。
這一步很重要蒋失,這個loadUser(uid)
參數(shù)比較少,當方法參數(shù)多時(例如loadXXX(int,int,int,int...String,String....)
)桐玻,特別容易搞錯篙挽。當后端接口修改了,service
相應也要修改镊靴,這時多參數(shù)的方法很容易出問題铣卡。
verify(userView).onUserLoaded(captor.capture());
,驗證userView.onUserLoaded(...)
是否被調(diào)用邑闲,并捕獲傳入的user參數(shù)
ArgumentCaptor
顧名思義參數(shù)捕獲器算行,就是捕獲傳入?yún)?shù)。當userService.loadUser()
執(zhí)行完并返回Observable<User>
苫耸,在onNext(user)
回調(diào)User
傳給userView.onUserLoaded(...)
,但我們不確定回調(diào)的user
是否正確儡陨。因此我們需要捕獲user
參數(shù)褪子,并校驗其正確性。
如果參數(shù)是List<T>
類型骗村,ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class)
即可嫌褪,不需要寫List泛型參數(shù)。
assertEquals
這個不用說了吧.....
Service(Model層)單元測試
測試這一層的目的胚股,是驗證從服務器返回的數(shù)據(jù)笼痛,是否解析成正確的對象。單元測試時,應該模擬服務器返回json數(shù)據(jù)缨伊。由于UserService
被Retrofit
代理過摘刑,所以單元測試需要一點技巧。
寫一個MockRetrofitHelper
:
public class MockRetrofitHelper {
public <T> T create(Class<T> clazz) {
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new MockInterceptor())
.build();
Retrofit retrofit = new Retrofit.Builder().baseUrl("http://api.***.com")
.client(client)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build();
return retrofit.create(clazz);
}
private String path;
public void setPath(String path) {
this.path = path;
}
private class MockInterceptor implements Interceptor{
@Override
public Response intercept(Chain chain) throws IOException {
// 模擬網(wǎng)絡數(shù)據(jù)
String content = AssestsReader.readFile(path);
ResponseBody body = ResponseBody.create(MediaType.parse("application/x-www-form-urlencoded"), content);
Response response = new Response.Builder().request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(200)
.body(body)
.build();
return response;
}
}
}
解釋一下刻坊,MockInterceptor
的職責枷恕,讀取本地數(shù)據(jù),并直接返回谭胚。因此徐块,OkHttpClient
并沒有真正請求網(wǎng)絡數(shù)據(jù),而是用了本地數(shù)據(jù)灾而。
對OkHttp Interceptor
不熟悉的同學胡控,參考:
然后,寫UserServiceTest
單元測試:
public class UserServiceTest {
UserService userService;
MockRetrofitHelper retrofit;
@Before
public void setUp() throws Exception {
retrofit = new MockRetrofitHelper();
userService = retrofit.create(UserService.class);
}
@Test
public void testLoadUser() throws Exception {
retrofit.setPath(".../User.json");
TestSubscriber<User> testSubscriber = new TestSubscriber<>();
userService.loadUser(1)
.toBlocking()
.subscribe(testSubscriber);
User user = testSubscriber.getOnNextEvents()
.get(0);
Assert.assertEquals(user.uid, 1);
Assert.assertEquals(user.name, "kkmike999");
}
}
當Observalbe<User>
調(diào)用subscribe(...)
時旁趟,TestSubscriber
會捕獲onNext(user)
參數(shù)铜犬,并放進List<User>
事件隊列。我們通過testSubscriber.getOnNextEvents()
獲取事件隊列轻庆,從這個隊列獲取User
癣猾,并驗證正確性。
不用TestSubscriber
也可以這樣:
User user = userService.loadUser(1)
.toBlocking()
.first();
小結(jié)
文章已經(jīng)到尾聲余爆。對于mockito纷宇、retrofit、okhttp intercepor熟悉的你蛾方,本文并沒有太多難點像捶。
為新功能寫代碼時,應該先寫Presenter
或者Service
桩砰,不急著運行拓春,再寫PresenterTest
和ServiceTest
,在JVM上驗證代碼是否正確亚隅。寫完單元測試后硼莽,再讓Activity
調(diào)用Presenter
。
對Activity煮纵、Service
單元測試感興趣的同學懂鸵,不妨了解Robolectric(發(fā)音比較坑爹,重音在l
而不是R
)行疏。它可以讓你在JVM
運行Activity單元測試匆光,比真機調(diào)試快多了。
各位同學酿联,千萬不要覺得 很麻煩终息、項目很趕 就不寫單元測試夺巩,這些都是業(yè)界大牛的經(jīng)驗之談,有益無害周崭!當你發(fā)現(xiàn)代碼無法單元測試柳譬,證明代碼本身有問題,應該去改進休傍,而不是放棄單元測試征绎。
實際項目與Retrofit
一個中型的APP項目,可能由幾位工程師一起編寫磨取,對于Retrofit等新技術(shù)并不是每個人都接受人柿。希望《同事拒絕Retrofit,怎么辦忙厌?》對你有幫助凫岖。
關(guān)于作者
我是鍵盤男。
在廣州生活逢净,在創(chuàng)業(yè)公司上班哥放,猥瑣文藝碼農(nóng)。喜歡科學爹土、歷史甥雕,玩玩投資,偶爾獨自旅行胀茵。希望成為獨當一面的工程師社露。