為什么要做單元測試
學(xué)習(xí)過或者了解軟件工程的人一定對這個東西不陌生帅刊,很多人也知道這個東西很重要法挨,但是總是以各種借口來推脫,這其中就包括我逛薇。大學(xué)我學(xué)習(xí)的并不是軟件工程,所以對什么黑盒測試疏虫、白盒測試永罚、灰盒測試只是聽說過啤呼,并沒有什么具體感覺。前段時間正好看了bob大叔的的《代碼整潔之道》和 另外一本經(jīng)典《重構(gòu):改善既有代碼的設(shè)計》呢袱,兩位作者都對單元測試以及TDD(測試驅(qū)動開發(fā))推崇備至官扣,我看完之后也是激動不已,正好手頭有個新項目产捞。于是當(dāng)即決定將這種開發(fā)模式引入進(jìn)來醇锚。
題外話說了這么多,我們就先來看看寫單元測試的一些好處吧坯临!
1. 開發(fā)新功能時焊唬,避免遺漏功能點
我們在開發(fā)的過程中,常常出現(xiàn)因為新功能太多而遺忘的情況看靠。等我們提測之后赶促,看到滿屏的bug,心里一定是崩潰的挟炬。但是如果我們先寫好單元測試鸥滨,或者說打好樁,然后根據(jù)功能點一個一個的開發(fā)谤祖,既避免了遺忘婿滓,又能測試我們的代碼,一舉兩得爸嘞病凸主!
2. 重構(gòu)代碼時,避免影響其他功能
重構(gòu)的時候额湘,我們最擔(dān)心的就是影響其他模塊或功能卿吐。但是如果我們提前寫好了單元測試,我們就能很輕易的就發(fā)現(xiàn)我們在重構(gòu)的過程中出現(xiàn)的side effect
3. 提高我們的編程能力
單元測試寫多了之后锋华,我們很容易就能在編碼的過程中注意到各種邊界條件嗡官,從而寫出健壯性更好的程序
4. 提高我們的效率
很多人看到這點的時候會覺得奇怪。雖然看起來我們花了很多時間在編寫單元測試上毯焕,但是一旦單元測試寫好了之后衍腥,基本上就是一勞永逸的,難道你不覺得讓電腦自動去測試比我們挨個挨個去點我們的app效率更高嗎纳猫?更何況紧阔,假設(shè)有人過來接手我們寫的項目的時候,他們只需要打開單元測試就知道我們這個項目续担,這個模塊做了些什么事情,就能更快速的上手了活孩。
看了這么多單元測試的好處之后物遇,是不是有些躍躍欲試了?先不著急,我們先來看看Clean Architecture 的結(jié)構(gòu)询兴,分析分析Clean Architecture 的特點再對癥下藥乃沙。
Clean Architecture 中,它的業(yè)務(wù)邏輯代碼放在了domain 層诗舰,是純 java 代碼警儒,data 層用到了一些 android 平臺的東西,包括網(wǎng)絡(luò)訪問眶根、數(shù)據(jù)存儲等蜀铲。UI 層又劃分成了P(presentor) 和 V(view),presentor 是純 java代碼属百,view 部分才是跟 android 緊密相關(guān)的记劝。所以我們需要兩類工具:測試純java代碼的和測試android相關(guān)的。
Java 代碼的單元測試
純 Java 代碼測試框架很多族扰,最出名的應(yīng)該是 Junit 和 TestNG 了厌丑。Android Studio 默認(rèn)使用的是 JUnit 4 ,大概是因為JUnit 4的使用人群最多吧渔呵。我們接下來就介紹JUnit 4的功能和特點怒竿。
使用JUnit進(jìn)行單元測試
最開始的JUnit 是由Kent Beck 和Eric Gamma 在飛機(jī)上寫出來的(膜拜ing)。因為Android Studio已經(jīng)將其內(nèi)置了扩氢,所以我們就不需要再額外引入了耕驰。只需要在我們想要添加單元測試的module 的build.gradle 中添加下面這句話:
testCompile "junit:junit:4.12"
一般來說,單元測試分為三步:
setup:即new 出待測試的類类茂,設(shè)置一些前提條件
執(zhí)行動作:即調(diào)用被測類的被測方法耍属,并獲取返回結(jié)果
驗證結(jié)果:驗證獲取的結(jié)果跟預(yù)期的結(jié)果是一樣的
一個簡單的例子
假設(shè)我們的代碼中有一個購物車類和一個商品類,每當(dāng)一個商品加入到購物車之后巩检,購物車會計算商品總價厚骗。
public class Goods {
private String name;
private long id;
private long price;
private int quantities;
public Goods(long id){
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getId() {
return id;
}
public long getPrice() {
return price;
}
public void setPrice(long price) {
this.price = price;
}
public int getQuantities() {
return quantities;
}
public void setQuantities(int quantities) {
this.quantities = quantities;
}
public long getTotalPrice() {
return price * quantities;
}
}
生成測試代碼
我們在Goods.java 的源碼中任意位置單擊右鍵,按照下圖所示兢哭,Android Studio 就會引導(dǎo)我們生成測試類领舰。
Testing library 一欄選擇JUnit4。class name 就用默認(rèn)的迟螺,否則就無法從被測試類快速跳轉(zhuǎn)到測試類中了冲秽。如果我們要做一些初始化工作,我們就需要勾選setUp矩父。然后在下面方法列表中選擇我們想要測試哪些方法锉桑。一般來說,我們不會去測試代碼中簡單的 setter 和 getter , 除非里面有很復(fù)雜的邏輯代碼窍株。所以民轴,我們這里只測試 getTotalPrice 這個方法攻柠;
最后在生成的代碼中添加以下測試代碼:
public class GoodsTest {
private Goods goods;
@Before
public void setUp() throws Exception {
goods = new Goods(1);
}
@Test
public void testGetTotalPrice() throws Exception {
goods.setPrice(112);
goods.setQuantities(10);
Assert.assertEquals(112*12,goods.getTotalPrice());
}
}
其中 setup 方法對應(yīng)的前面所說的 setup 部分,testGetTotalPrice 方法的前兩句對應(yīng)前面所說的執(zhí)行動作后裸,而最后一句 Assert 就是第三部分驗證結(jié)果瑰钮。
我們可以單擊方法名左側(cè)的綠色三角按鈕來測試單個方法,或者快捷鍵 Ctrl+Shift+F10 來運行整個測試類微驶。測試結(jié)果會在下方顯示:
測試失敗浪谴,運行結(jié)果會告訴我們在哪一行出錯了以及期望的結(jié)果是什么,而實際結(jié)果又是什么因苹。
使用Mock框架
在寫單元測試的過程中苟耻,一個很普遍的問題是,要測試的類會有很多依賴容燕,這些依賴的類/對象/資源又會有別的依賴梁呈,從而形成一個大的依賴樹,要在單元測試的環(huán)境中完整地構(gòu)建這樣的依賴蘸秘,是一件很困難的事情官卡。所幸我們有一個應(yīng)對這個問題的解決方案:mock。我們使用 mock 框架可以模擬任何類醋虏,構(gòu)建一個假對象寻咒,而不需要實際運行這個類,我們可以定義這些假對象上的行為颈嚼,提供給被測試對象使用毛秘。被測試對象像使用真的對象一樣使用它們。用這種方式阻课,我們可以把測試的目標(biāo)限定于被測試對象本身叫挟,就如同在被測試對象周圍做了一個劃斷,形成了一個盡量小的被測試目標(biāo)限煞。
引入Mock框架
Mock的框架有很多抹恳,最為知名的一個是Mockito,這是一個開源項目署驻,使用廣泛奋献。同樣,在build.gradle中添加以下語句:
testCompile "org.mockito:mockito-core:1.9.5"
我們先來看一個官方的示例:
import org.mockito.Mockito;
// 創(chuàng)建mock對象
List mockedList = Mockito.mock(List.class);
// 設(shè)置mock對象的行為 - 當(dāng)調(diào)用其get方法獲取第0個元素時旺上,返回"one"
Mockito.when(mockedList.get(0)).thenReturn("one");
// 使用mock對象 - 會返回前面設(shè)置好的值"one"瓶蚂,即便列表實際上是空的
String str = mockedList.get(0);
Assert.assertTrue("one".equals(str));
Assert.assertTrue(mockedList.size() == 0);
// 驗證mock對象的get方法被調(diào)用過,而且調(diào)用時傳的參數(shù)是0
Mockito.verify(mockedList).get(0);
代碼中的注釋描述了代碼的邏輯:先創(chuàng)建mock對象mockedList宣吱,然后設(shè)置mock對象上的方法get窃这,指定當(dāng)get方法被調(diào)用,并且參數(shù)為0的時候征候,返回”one”钦听;然后洒试,調(diào)用被測試方法(被測試方法會調(diào)用mock對象的get方法);
上面這個示例揭示了最簡單的使用情況朴上,當(dāng)我最開始看到這個示例的時候,對最后一句很是困惑卒煞,覺得這句完全沒有必要痪宰,直到我在項目中寫下面這些代碼:
被測試類
public class UserLoginImpl extends UseCase implements UserLogin {
UserRepository userRepository;
private String name;
private String pwd;
@Inject
public UserLoginImpl(UserRepository userRepository,ThreadExecutor threadExecutor, PostExecutionThread postExecutionThread){
super(threadExecutor,postExecutionThread);
this.userRepository = userRepository;
}
@Override
protected Observable buildUseCaseObservable() {
return userRepository.userLogin(name,pwd);
}
@Override
public UserLogin setAccount(String name) {
this.name = name;
return this;
}
@Override
public UserLogin setPwd(String pwd) {
this.pwd = MD5.parseStrToMd5U32(pwd);
return this;
}
}
測試類
public class UserLoginTest {
private UserLoginImpl userLogin;
@Mock private ThreadExecutor mockThreadExecutor;
@Mock private PostExecutionThread mockPostExecutionThread;
@Mock private UserRepository mockUserRepository;
private static final String FAKE_USER_ACCOUNT = "13478969876";
private static final String FAKE_USER_PWD = "13478969876";
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
userLogin = new UserLoginImpl(mockUserRepository,mockThreadExecutor,mockPostExecutionThread);
userLogin.setAccount(FAKE_USER_ACCOUNT);
userLogin.setPwd(FAKE_USER_PWD);
}
@Test
public void testBuildUseCaseObservable() throws Exception {
userLogin.buildUseCaseObservable();
verify(mockUserRepository).userLogin(FAKE_USER_ACCOUNT,FAKE_USER_PWD);
verifyNoMoreInteractions(mockUserRepository);//驗證mockUserRepository是否還有其他地方被調(diào)用過
verifyZeroInteractions(mockPostExecutionThread); //驗證mockPostExecutionThread是否被調(diào)用過
verifyZeroInteractions(mockThreadExecutor);//驗證mockThreadExecutor是否被調(diào)用過
}
}
竟然失敗了!它告訴我它期望的密碼是13478969876畔裕,而實際調(diào)用的確是一長串的字符衣撬。研究了很久,終于發(fā)現(xiàn)了原因:在設(shè)置密碼的時候扮饶,我算出密碼的MD5值后具练,將MD5值存成為密碼,執(zhí)行 userLogin 方法的時候調(diào)用的是加密后的字符串甜无,而我在 verify 函數(shù)中使用的是沒有加密的密碼扛点,兩者不一致,因而編譯器報錯岂丘。
使用 Mock 框架陵究,不但能讓其返回任何我們?nèi)魏挝覀冃枰臄?shù)據(jù),而且還能驗證我們是否正確調(diào)用了奥帘,這么強(qiáng)大又好用的功能铜邮,還不趕快用起來!
Android 代碼的單元測試
android 的單元測試比純java代碼的復(fù)雜多了寨蹋。純 java 代碼我們直接在PC上就能運行了松蒜,因為它只依賴JVM。但是android 代碼要跑起來當(dāng)然需要android的運行環(huán)境了已旧,比如TextView秸苗、Toast等,雖然它也是一個變種的JVM评姨。如果我們的代碼還需要在模擬器或真機(jī)上才能測試难述,那效率可想而知有多慢了。有沒有能在PC的JVM上就能運行測試代碼的工具呢吐句?當(dāng)然有胁后,接下來我們就介紹我們的主角——Robolectric。
使用Robolectric進(jìn)行單元測試
Robolectric 是一個開源框架嗦枢,它實現(xiàn)了一套在 JVM 上能運行的 Android 開發(fā)環(huán)境攀芯。它實現(xiàn)一套 Shadow* 的東西,比如ShadowTextView , ShadowToast等控件文虏。顧名思義侣诺,影子對象(Shadow Object)并不是真正的對象殖演,它只是真實對象的一個影子。真實對象做了任何動作年鸳,產(chǎn)生了任何效果趴久,我們通過影子對象就能知道,并能夠通過影子對象就能知道真實對象的結(jié)果搔确。
Robolectric環(huán)境搭建
把下面這段話加入到您的build.gradle中來就可以將Robolectric 引入你的項目中了:
testCompile "org.robolectric:robolectric:3.0"
但是如果您的項目使用了MultiDex彼棍,那您就需要使用最新的3.2了。
按照前文介紹過的方法膳算,我們生成對應(yīng)的測試代碼座硕,然后通過注解配置TestRunner
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class LoginActivityTest {
}
Activity 的測試
創(chuàng)建activity 實例
@Test
public void testActivity() {
SampleActivity sampleActivity = Robolectric.setupActivity(SampleActivity.class);
assertNotNull(sampleActivity);
assertEquals(sampleActivity.getTitle(), "SimpleActivity");
}
activity 生命周期
@Test
public void testLifecycle() {
ActivityController<SampleActivity> activityController = Robolectric.buildActivity(SampleActivity.class).create().start();
Activity activity = activityController.get();
TextView textview = (TextView) activity.findViewById(R.id.tv_lifecycle_value);
assertEquals("onCreate",textview.getText().toString());
activityController.resume();
assertEquals("onResume", textview.getText().toString());
activityController.destroy();
assertEquals("onDestroy", textview.getText().toString());
}
跳轉(zhuǎn)
@Test
public void testStartActivity() {
forwardBtn.performClick();
Intent expectedIntent = new Intent(sampleActivity, LoginActivity.class);
Intent actualIntent = ShadowApplication.getInstance().getNextStartedActivity();
assertEquals(expectedIntent, actualIntent);
}
UI組件狀態(tài)
@Test
public void testViewState(){
CheckBox checkBox = (CheckBox) sampleActivity.findViewById(R.id.checkbox);
Button inverseBtn = (Button) sampleActivity.findViewById(R.id.btn_inverse);
assertTrue(inverseBtn.isEnabled());
checkBox.setChecked(true);
inverseBtn.performClick();
assertTrue(!checkBox.isChecked());
inverseBtn.performClick();
assertTrue(checkBox.isChecked());
}
Dialog
@Test
public void testDialog(){
//點擊按鈕,出現(xiàn)對話框
dialogBtn.performClick();
AlertDialog latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
assertNotNull(latestAlertDialog);
}
Toast
@Test
public void testToast(){
toastBtn.performClick();
assertEquals(ShadowToast.getTextOfLatestToast(),"we love UT");
}
Fragment
如果使用support的Fragment涕蜂,需添加以下依賴
testCompile "org.robolectric:shadows-support-v4:3.0"
shadow-support包提供了將Fragment主動添加到Activity中的方法:SupportFragmentTestUtil.startFragment(),簡易的測試代碼如下
@Test
public void testFragment(){
SampleFragment sampleFragment = new SampleFragment();
//此api可以主動添加Fragment到Activity中华匾,因此會觸發(fā)Fragment的onCreateView()
SupportFragmentTestUtil.startFragment(sampleFragment);
assertNotNull(sampleFragment.getView());
}
訪問資源
@Test
public void testResources() {
Application application = RuntimeEnvironment.application;
String appName = application.getString(R.string.app_name);
String activityTitle = application.getString(R.string.title_activity_simple);
assertEquals("LoveUT", appName);
assertEquals("SimpleActivity",activityTitle);
}
Service的測試
Service的測試類似于BroadcastReceiver,以IntentService為例机隙,可以直接觸發(fā)onHandleIntent()方法蜘拉,用來驗證Service啟動后的邏輯是否正確。
public class SampleIntentService extends IntentService {
public SampleIntentService() {
super("SampleIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
SharedPreferences.Editor editor = getApplicationContext().getSharedPreferences(
"example", Context.MODE_PRIVATE).edit();
editor.putString("SAMPLE_DATA", "sample data");
editor.apply();
}
}
以上代碼的單元測試用例:
@Test
public void addsDataToSharedPreference() {
Application application = RuntimeEnvironment.application;
RoboSharedPreferences preferences = (RoboSharedPreferences) application
.getSharedPreferences("example", Context.MODE_PRIVATE);
SampleIntentService registrationService = new SampleIntentService();
registrationService.onHandleIntent(new Intent());
assertEquals(preferences.getString("SAMPLE_DATA", ""), "sample data");
}
BroadcastReceiver 的測試
首先看下廣播接收者的代碼
public class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
SharedPreferences.Editor editor = context.getSharedPreferences(
"account", Context.MODE_PRIVATE).edit();
String name = intent.getStringExtra("EXTRA_USERNAME");
editor.putString("USERNAME", name);
editor.apply();
}
}
廣播的測試點可以包含兩個方面黍瞧,一是應(yīng)用程序是否注冊了該廣播诸尽,二是廣播接受者的處理邏輯是否正確,關(guān)于邏輯是否正確印颤,可以直接人為的觸發(fā)onReceive()方法您机,驗證執(zhí)行后所影響到的數(shù)據(jù)。
@Test
public void testBoradcast(){
ShadowApplication shadowApplication = ShadowApplication.getInstance();
String action = "com.geniusmart.loveut.login";
Intent intent = new Intent(action);
intent.putExtra("EXTRA_USERNAME", "geniusmart");
//測試是否注冊廣播接收者
assertTrue(shadowApplication.hasReceiverForIntent(intent));
//以下測試廣播接受者的處理邏輯是否正確
MyReceiver myReceiver = new MyReceiver();
myReceiver.onReceive(RuntimeEnvironment.application,intent);
SharedPreferences preferences = shadowApplication.getSharedPreferences("account", Context.MODE_PRIVATE);
assertEquals( "geniusmart",preferences.getString("USERNAME", ""));
}
自定義控件的測試
Robolectric 定義了很多影子類年局,它擴(kuò)展或繼承了Android OS對應(yīng)的類际看。當(dāng)創(chuàng)建一個Android 類,Robolectric 就會查找對應(yīng)的影子類矢否,如果找到了仲闽,Robolectric 就會創(chuàng)建一個影子對象與之對應(yīng)。當(dāng)調(diào)用Android 類的方法的時候僵朗,Robolectric 就會確保對應(yīng)的方法被調(diào)用了赖欣。如果 Robolectric 的影子類不能滿足您的要求,你還可以安裝一定的要求編寫自己的影子類验庙。最常見的應(yīng)該就是自定義控件了顶吮。
我們封裝了一個toast,它的代碼如下所示:
public class FBToast {
private static Toast toast = null;
private static TextView view = null;
public static void showShortToast(Context context, String msg) {
showToast(context,msg,Toast.LENGTH_SHORT);
}
public static void showToast(Context context, String msg, int duration) {
if (toast == null) {
toast = new Toast(context);
view = (TextView) LayoutInflater.from(context)
.inflate(R.layout.publish_toast, null);
view.setText(msg);
view.setPadding(30, 80, 30, 80);
toast.setView(view);
toast.setDuration(duration);
toast.setGravity(Gravity.CENTER, 0, 0);
} else {
view.setText(msg);
toast.setDuration(duration);
}
toast.show();
}
public static void cancel(){
if(toast != null) {
toast.cancel();
}
}
}
代碼中使用如下:
@Override
public void showErrorMessage(String message) {
FBToast.showShortToast(this,message);
}
如果我們不擴(kuò)展它的影子類粪薛,那我們是無法測試程序是否正確調(diào)用了Toast的相關(guān)代碼悴了。
FBToast的shadow 對象:
@Implements(FBToast.class)
public class ShadowFBToast extends ShadowToast{
@Implementation
public static void showShortToast(Context context, String msg){
showToast(context,msg,Toast.LENGTH_SHORT);
}
@Implementation
public static void showToast(Context context, String msg, int duration){
ShadowToast.makeText(context,msg,duration).show();
}
public static String getTextOfLatestToast(){
return ShadowToast.getTextOfLatestToast();
}
}
自定義shadow對象要求必須在類定義中加上@Implements(AndroidClassName.class)
注解,并在你在代碼中使用的公共方法上也加上@Implementation
注解。最后湃交,你需要在你的測試代碼的config注解加上這個自定義Shadow 對象熟空。
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class,shadows = {ShadowFBToast.class})
public class LoginActivityTest {
....
}
RxJava 單元測試的注意事項
RxJava 給我們帶來了非常大的便利,它簡化了邏輯搞莺,避免了"嵌套地獄"息罗,邏輯清晰簡單,如水銀泄地才沧。但是它也給我們進(jìn)行單元測試帶來了一些麻煩阱当。
匿名類的問題
由于Mockito 不支持匿名類,所以我們在使用RxJava的時候要特別注意糜工。相信很多人跟我一樣,Subscriber 都是像下面這樣寫的录淡。
userLogin.setAccount(account)
.setPwd(pwd)
.execute(new BaseSubscriber<Boolean>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(Boolean o) {
view.renderSuccessView();
}
});
@Test
public void testSubmit() throws Exception {
TestSubscriber<Boolean> testSubscriber = new TestSubscriber<>();
loginPresenter.submit(FAKE_ACCOUNT,FAKE_PWD);
Mockito.verify(userLogin).setAccount(FAKE_ACCOUNT);
Mockito.verify(userLogin).setPwd(FAKE_ACCOUNT);
Mockito.verify(userLogin).execute(testSubscriber);
}
控制臺就會報下面這個錯誤:
為了解決這個問題捌木,你就必須將匿名類變成內(nèi)部類。
@Override
public void submit(String account, String pwd) {
if(validate(account,pwd)){
userLogin.setAccount(account)
.setPwd(pwd)
.execute(new UserLoginSubscriber());
}
}
public final class UserLoginSubscriber extends BaseSubscriber<Boolean> {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(Boolean aBoolean) {
view.renderSuccessView();
}
}
異步回調(diào)的測試
還是以前一節(jié)的代碼為例嫉戚,如果我們想測試UserLoginSubscriber這個類里面的三個方法刨裆,我們該怎么做呢?
Mockito 為我們提供了兩個解決方案:
1. doAnswer
我們可以使用都doAnswer為一個函數(shù)進(jìn)行打樁以測試異步函數(shù)彬檀。當(dāng)被測試的方法被調(diào)用時我們生成了一個通用的anwser,這個回調(diào)會被執(zhí)行帆啃。UserLoginSubscriber 是回調(diào)函數(shù)所在的類,LoginPresenter是我們要測試的類窍帝,UserLogin是我們mock的對象努潘,UserLogin執(zhí)行了UserLoginSubscriber的回調(diào)方法。具體看代碼:
@Mock
UserLogin userLogin;
@Mock
LoginPresenter.View mockView;
private LoginPresenter loginPresenter;
@Test
public void testSubmit() throws Exception {
Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
((UserLoginSubscriber)invocation.getArguments()[0]).onNext(true);
return null;
}
}).when(userLogin).execute(Mockito.any(UserLoginSubscriber.class));
loginPresenter.submit(FAKE_ACCOUNT,FAKE_PWD);
Mockito.verify(userLogin,Mockito.times(1)).execute(Mockito.any(BaseSubscriber.class));
Mockito.verify(mockView).renderSuccessView();
}
2. ArgumentCaptor
在這里我們的UserLoginSubscriber是異步的: 我們通過ArgumentCaptor捕獲傳遞到UserLogin對象的UserLoginSubscriber回調(diào)坤学。
@Mock
UserLogin userLogin;
@Mock
LoginPresenter.View mockView;
@Captor
ArgumentCaptor<LoginPresenterImpl.UserLoginSubscriber> argumentCaptor;
private LoginPresenter loginPresenter;
@Test
public void testSubmit() throws Exception {
loginPresenter.submit(FAKE_ACCOUNT,FAKE_PWD);
Mockito.verify(userLogin).execute(argumentCaptor.capture());
argumentCaptor.getValue().onNext(true);
Mockito.verify(mockView).renderSuccessView();
}
doAnswer 和 ArgumentCaptor 都是值得大書特書的東西疯坤,這里我們就只簡單介紹到這里。如果有機(jī)會深浮,會單獨寫一篇來介紹這兩個東西压怠。
RxJava 的Subscriber 的測試
RxJava 由于使用鏈?zhǔn)秸{(diào)用,而且通常最后subscribe方法是沒有返回值的飞苇,所以我們沒有辦法去像常規(guī)單元測試一樣對其進(jìn)行測試菌瘫。所以,我們不得不動用一些非常規(guī)武器---TestSubscriber布卡。哈哈雨让,其實不是,這是官方提供的羽利,使用方法如下:
@Test
public void testGetGoodsCategories() throws Exception {
TestSubscriber<List<GoodsCategory>> testSubscriber = new TestSubscriber<>();
GoodsRepository.getGoodsCategories(FAKE_CATEGORY_ID).subscribe(testSubscriber);
testSubscriber.assertNoErrors();
}
@Test
public void testInvalidCategoryId(){
TestSubscriber<List<GoodsCategory>> testSubscriber = new TestSubscriber<>();
GoodsRepository.getGoodsCategories(INVALID_CATEGORY_ID).subscribe(testSubscriber);
testSubscriber.assertError(IllegalArgumentException.class);
}
非常簡單宫患,我們通過檢查TestSubscriber 的回調(diào)結(jié)果,就能知道我們的程序是否按照我們預(yù)想的運行。TestSubscriber 還有其他方法娃闲,諸如assertValue()虚汛,assertCompleted()等等』拾铮總之卷哩,RxJava 為我們提供了豐富的工具來進(jìn)行測試。
結(jié)束語
本文只是最基本的單元測試指南属拾,許多高級使用技巧我們并沒有涉及到将谊,比如JUnit 的Rule。 這需要我們在日后的工作中一點點去學(xué)習(xí)和積累渐白。其實尊浓,關(guān)于Dagger2 的,還有一個據(jù)說很神奇的開源庫DaggerMock,但由于我沒有試驗成功纯衍,所以這里就不介紹了栋齿,等到哪天我學(xué)會如何使用之后,再把這塊內(nèi)容補(bǔ)上襟诸。