Clean Architecture 學(xué)習(xí)之單元測試

為什么要做單元測試

學(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" 

一般來說,單元測試分為三步:

  1. setup:即new 出待測試的類类茂,設(shè)置一些前提條件

  2. 執(zhí)行動作:即調(diào)用被測類的被測方法耍属,并獲取返回結(jié)果

  3. 驗證結(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)用過
    }
}
Paste_Image.png

竟然失敗了!它告訴我它期望的密碼是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ǔ)上襟诸。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瓦堵,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子歌亲,更是在濱河造成了極大的恐慌菇用,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陷揪,死亡現(xiàn)場離奇詭異惋鸥,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)鹅龄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門揩慕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人扮休,你說我怎么就攤上這事迎卤。” “怎么了玷坠?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵蜗搔,是天一觀的道長。 經(jīng)常有香客問我八堡,道長樟凄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任兄渺,我火速辦了婚禮缝龄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己叔壤,他們只是感情好瞎饲,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著炼绘,像睡著了一般嗅战。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上俺亮,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天驮捍,我揣著相機(jī)與錄音,去河邊找鬼脚曾。 笑死东且,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的本讥。 我是一名探鬼主播苇倡,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼囤踩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起晓褪,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤堵漱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后涣仿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體勤庐,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年好港,在試婚紗的時候發(fā)現(xiàn)自己被綠了愉镰。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡钧汹,死狀恐怖丈探,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拔莱,我是刑警寧澤碗降,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站塘秦,受9級特大地震影響讼渊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜尊剔,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一爪幻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦挨稿、人聲如沸仇轻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拯田。三九已至,卻和暖如春甩十,著一層夾襖步出監(jiān)牢的瞬間船庇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工侣监, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留鸭轮,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓橄霉,卻偏偏與公主長得像窃爷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子姓蜂,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,104評論 25 707
  • 人生若只如初見按厘,何事秋風(fēng)悲畫扇。等閑變卻故人心钱慢,卻道故人心亦變逮京。驪山語罷清宵半,淚雨霖鈴終不怨束莫。何如薄幸錦衣郎懒棉,比...
    小妖的淺時光閱讀 390評論 0 0
  • 本文轉(zhuǎn)自微信公眾號:帆軟軟件 作者:frdashixiong 最近歡樂頌火爆啊策严,小編也是日夜熬黑眼圈擼完了啊,沒...
    一帆簡書閱讀 7,297評論 1 2
  • 幸 福 被人關(guān)愛饿敲,是莫大的幸福妻导! ——致友人們 牛懷斌 一首 沒有動詞的詩 被信鴿...
    八五二閱讀 393評論 0 3