Robolectric 之 Jessyan Arm框架之Mvp 單元測試解耦過程(View 層)

Robolectric 實(shí)戰(zhàn)解耦整個系列:
Robolectric 之 Jessyan Arm框架之Mvp 單元測試解耦過程(View 層)
Robolectric 之 Jessyan Arm框架之Mvp 單元測試解耦過程(Presenter 層)
Robolectric 之 Jessyan Arm框架之Mvp 單元測試本地json測試
github的鏈接: https://github.com/drchengit/Robolectric_arm_mvp

我的項(xiàng)目用的是jassyan的arm快速開發(fā)框架!
我做robolectric單元測試時,想哭1咎椤样漆!
因?yàn)樗膁agger2 用得飛起放祟,我寫代碼的時候一邊用一邊說著:
"牛啤了!大兄弟眉撵!"
“哦纽疟!還能這么用,牛企∷痢!牛啤”
但是在解耦單元測試的時候:
“這是啥喲亡问!這個東西從哪里來的?”
“又報一個空指針”
“我是照著demo代碼一步步敲下來的酝陈!報錯是啥子情況锈死?”
如果你用了arm 而且要做Robolectric 單元測試的話其屏,不妨看看喲偎行!如果不會Robolectric 建議先學(xué)學(xué)再看:測試資源放送

第一步: 寫個登錄功能

熟悉的配方新建mvp.png

LoginActvity

public class LoginActivity extends BaseActivity<LoginPresenter> implements LoginContract.View {
···
 @OnClick(R.id.tv_login)
    public void onViewClicked() {
        mPresenter.login();
    }
···
}

LoginPresenter

public class LoginPresenter extends BasePresenter<LoginContract.Model, LoginContract.View> {
···
public void login() {
        if(mRootView.getMobileStr().length() != 11){
            mRootView.showMessage("手機(jī)號碼不正確");
            return;
        }
        if(mRootView.getPassWordStr().length() < 1){
            mRootView.showMessage("密碼太短");
            return;
        }
        //調(diào)用登錄接口妙真,正確的密碼:abc  手機(jī)號只要等于11位判斷賬號為正確
        mModel.login(mRootView.getMobileStr(),mRootView.getPassWordStr())
                .compose(RxUtils.applySchedulers(mRootView))
                .subscribe(new MyErrorHandleSubscriber<User>(mErrorHandler) {
                    //這個類是我自定義的一個類,統(tǒng)一攔截所有error 并回調(diào)給: ResponseErrorListenerImpl
                    // 可以不統(tǒng)一處理菱阵,直接重寫覆蓋:
//                     @Override
//                    public void onError(@NonNull Throwable t) {}
                    @Override
                    public void onNext(User user) {
                            mRootView.loginSuccess();
                    }
                });

    }
···
}

LoginModel

public class LoginModel extends BaseModel implements LoginContract.Model {
···
@Override
    public Observable<User> login(String mobileStr, String passWordStr) {
        //調(diào)用登錄接口嗤堰,正確的密碼:abc  手機(jī)號只要等于11位判斷賬號為正確
        String name;
        if(passWordStr.equals("abc")){//正確密碼琳钉,
            name = "drchengit";
        }else {
            name = "drchengi";
        }

        //由于不知道上哪里去找一個穩(wěn)定且長期可用的登錄接口,所以用的接口是github 上的查詢接口:https://api.github.com/users/drchengit
        // 這里的處理是正確的密碼,請求存在的用戶名:drchengit  錯誤的密碼請求不存在的用戶名: drchengi
        // 將就一下
        return mRepositoryManager.obtainRetrofitService(CommonService.class).getUser(name);
    }
···
}

注意我通過Okhttp 的插值器 回調(diào)GlobalHttpHandlerImpl 類的onHttpResultResponse()方法,如果返回 "no found" 內(nèi)部會 throw 一個 "密碼錯誤" 的自定義異常慨蓝,被框架捕獲并打印 Toast

 
public class GlobalHttpHandlerImpl implements GlobalHttpHandler {
    
    @Override
    public Response onHttpResultResponse(String httpResult, Interceptor.Chain chain, Response response) {
        if (!TextUtils.isEmpty(httpResult) && RequestInterceptor.isJson(response.body().contentType())) {
            User user;
            //                https://blog.csdn.net/qfikh/article/details/75669939
//                List<User> list = ArmsUtils.obtainAppComponentFromContext(context).gson().fromJson(httpResult, new TypeToken<List<User>>() {
//                }.getType());
            user = ArmsUtils.obtainAppComponentFromContext(context).gson().fromJson(httpResult, User.class);
            if(user.isLoginFaild()){
                throw new MyNetException(10001,"密碼錯誤");
            }
        }


 
        return response;
    }

 ···
}

我省略了過程,總之就是輸入正確的手機(jī)號和密碼就可以登錄,輸錯就會提示"密碼錯誤"。

第二步導(dǎo)包和配置

其實(shí)androidx 已經(jīng)出了,https://github.com/robolectric/robolectric,但是jessyan在簡書回復(fù)我androidx 現(xiàn)在沒打算適配(第一次收到作者的回復(fù),可把我牛逼壞了,學(xué)android 的人都這么平易近人嗎?)我也還有沒有處理遷移的bug,所以用了這框架只有將就sdk 27版本的測試用一下。

android {

  //單元測試
    testOptions {
        unitTests {
            includeAndroidResources = true
        }
    }
}

dependencies {
 ·····

 //單元測試
    testImplementation 'org.robolectric:robolectric:3.8'
    testImplementation "org.robolectric:shadows-support-v4:3.4-rc2"
    //依賴隔離
    testImplementation "org.mockito:mockito-core:2.11.0"

  
    }
} 

注意: includeAndroidResources = true這要加上

第三步測試View 層

  • 新建
    圖片.png

    ctrl + shift + T
    圖片.png
圖片.png
  • 寫好最基本測試迫不急待地運(yùn)行
package me.jessyan.mvparms.demo.mvp.ui.activity.login;


import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
import org.robolectric.shadows.ShadowToast;

import static org.junit.Assert.*;

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 27)
public class LoginActivityTest {
    TextView loginTv;

    EditText phoneEt;
    EditText passWrodEt;
    private LoginActivity loginActivity;


    @Before
    public void setUp() {
        ShadowLog.stream = System.out;
        loginActivity = Robolectric.buildActivity(LoginActivity.class)
                .create()
                .resume()
                .get();
        loginTv = loginActivity.findViewById(R.id.tv_login);
        phoneEt = loginActivity.findViewById(R.id.et_mobile);
        passWrodEt = loginActivity.findViewById(R.id.et_pass);

    }


    @Test
    public  void login(){
        //直接點(diǎn)擊登錄
        loginTv.performClick();
        Assert.assertEquals("手機(jī)號碼不正確", ShadowToast.getTextOfLatestToast());


    }

}
  • 第一個問題,泄露框架出問題,點(diǎn)過去看下,AppLifecyclesImpl類空針針
    圖片.png
public class AppLifecyclesImpl implements AppLifecycles {

  ···
    @Override
    public void onCreate(@NonNull Application application) {
      try {
            if (LeakCanary.isInAnalyzerProcess(application)) {
                // This process is dedicated to LeakCanary for heap analysis.
                // You should not init your app in this process.
                return;
            }
        }catch (NullPointerException e){

        }
      ···
    }

  ···
}

LeakCanary 是內(nèi)存泄露捏浊,對單元測試沒啥用,直接try {}catch

  • 再次運(yùn)行胡岔,ok靶瘸,一路綠燈,下面進(jìn)行登錄接口測試
    @Test
    public  void login(){
        //直接點(diǎn)擊登錄
//        loginTv.performClick();
//        Assert.assertEquals("手機(jī)號碼不正確", ShadowToast.getTextOfLatestToast());
        phoneEt.setText("13547250999");
        //沒有輸入密碼
//        loginTv.performClick();
//        Assert.assertEquals("密碼太短", ShadowToast.getTextOfLatestToast());
        //錯誤密碼
        passWrodEt.setText("aaaa");
        loginTv.performClick();
        //這里是驗(yàn)證網(wǎng)絡(luò)框架提示
        Assert.assertEquals("密碼錯誤", ShadowToast.getTextOfLatestToast());

    }
  • 報了null,根本沒有打toast
    圖片.png
  • debug了半天,發(fā)現(xiàn)沒有回調(diào)
    圖片.png
  • 查了一下,原來測試要線程同步帜平,于是加上同步代碼冗锁。(下面的代碼讓Rxjava io線程和android 的main 線程同步)
        private void initRxJava() {
        RxJavaPlugins.reset();
        RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
            @Override
            public Scheduler apply(Scheduler scheduler) throws Exception {
                return Schedulers.trampoline();
            }
        });
        RxAndroidPlugins.reset();
        RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
            @Override
            public Scheduler apply(Scheduler scheduler) throws Exception {
                return Schedulers.trampoline();
            }
        });

    }
  • 心想現(xiàn)在應(yīng)該沒有問題了吧!結(jié)果還是同樣的問題,沒有打印toast摔敛!
    接下來我就進(jìn)入了終極debug和翻源碼模式,終于看到了這樣一段代碼
@Singleton
public class RepositoryManager implements IRepositoryManager {
 ···
    private <T> T createWrapperService(Class<T> serviceClass) {
        // 通過二次代理,對 Retrofit 代理方法的調(diào)用包進(jìn)新的 Observable 里在 io 線程執(zhí)行。
        return (T) Proxy.newProxyInstance(serviceClass.getClassLoader(),
                new Class<?>[]{serviceClass}, new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, @Nullable Object[] args)
                            throws Throwable {
                        if (method.getReturnType() == Observable.class) {
                            // 如果方法返回值是 Observable 的話,則包一層再返回
                            return Observable.defer(() -> {
                                final T service = getRetrofitService(serviceClass);
                                // 執(zhí)行真正的 Retrofit 動態(tài)代理的方法
圖片.png
                        }
                        // 返回值不是 Observable 的話不處理
                        final T service = getRetrofitService(serviceClass);
                        return getRetrofitMethod(service, method).invoke(service, args);
                    }
                });
    }
···
}
  • 莫不是sign線程沒有同步乎莉?惋啃?绒瘦?加上Sign()線程同步予跌,調(diào)用initRxjava方法

        private void initRxJava() {
        RxJavaPlugins.reset();
        RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
            @Override
            public Scheduler apply(Scheduler scheduler) throws Exception {
                return Schedulers.trampoline();
            }
        });

        //這個喲
        RxJavaPlugins.setSingleSchedulerHandler(new Function<Scheduler, Scheduler>() {
            @Override
            public Scheduler apply(Scheduler scheduler) throws Exception {
                return Schedulers.trampoline();
            }
        });

        RxAndroidPlugins.reset();
        RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
            @Override
            public Scheduler apply(Scheduler scheduler) throws Exception {
                return Schedulers.trampoline();
            }
        });


    }
  • ok,綠燈,加上完整測試登錄邏輯
 @Test
    public  void login(){
        initRxJava();
        //直接點(diǎn)擊登錄
        loginTv.performClick();
        Assert.assertEquals("手機(jī)號碼不正確", ShadowToast.getTextOfLatestToast());
        phoneEt.setText("13547250999");
        //沒有輸入密碼
        loginTv.performClick();
        Assert.assertEquals("密碼太短", ShadowToast.getTextOfLatestToast());
        //錯誤密碼
        passWrodEt.setText("aaaa");
        loginTv.performClick();
        //這里是驗(yàn)證網(wǎng)絡(luò)框架提示
        Assert.assertEquals("密碼錯誤", ShadowToast.getTextOfLatestToast());
        //正確密碼登錄
        passWrodEt.setText("abc");
        loginTv.performClick();
        Assert.assertEquals("登錄成功",ShadowToast.getTextOfLatestToast());

        //驗(yàn)證跳轉(zhuǎn)
        ShadowActivity shadowActivity = Shadows.shadowOf(loginActivity);
        Intent intent = shadowActivity.getNextStartedActivity();
        Assert.assertEquals(intent.getComponent().getClassName(), MainActivity.class);


    }
  • 一路綠燈,到這里View 層的Robolectric單元測試 才算完成膳殷,后面是Presenter 的業(yè)務(wù)解耦

Robolectric 實(shí)戰(zhàn)解耦整個系列:
Robolectric 之 Jessyan Arm框架之Mvp 單元測試解耦過程(View 層)
Robolectric 之 Jessyan Arm框架之Mvp 單元測試解耦過程(Presenter 層)
Robolectric 之 Jessyan Arm框架之Mvp 單元測試本地json測試
github的鏈接: https://github.com/drchengit/Robolectric_arm_mvp
測試資源放送

基本的配置 | http://www.reibang.com/p/7a4024925193
常見的坑(分包導(dǎo)致測試報錯等) | https://blog.csdn.net/weixin_34204057/article/details/91418305

我是drchen勒极,一個溫潤的男子,版權(quán)所有匾七,未經(jīng)允許不得抄襲。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末扔嵌,一起剝皮案震驚了整個濱河市世澜,隨后出現(xiàn)的幾起案子寥裂,更是在濱河造成了極大的恐慌,老刑警劉巖鳖昌,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異速那,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)榆俺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門诺擅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酒觅,“玉大人,你說我怎么就攤上這事仗扬∽绲” “怎么了帜乞?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵匀谣,是天一觀的道長。 經(jīng)常有香客問我,道長霹疫,這世上最難降的妖魔是什么膀藐? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任蛉加,我火速辦了婚禮针饥,結(jié)果婚禮上苞七,老公的妹妹穿的比我還像新娘惠啄。我一直安慰自己趋距,他們只是感情好摘盆,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布囤锉。 她就那樣靜靜地躺著驱入,像睡著了一般雪情。 火紅的嫁衣襯著肌膚如雪誊锭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天,我揣著相機(jī)與錄音劫窒,去河邊找鬼逛艰。 笑死镇眷,一個胖子當(dāng)著我的面吹牛翁垂,可吹牛的內(nèi)容都是我干的衙伶。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼躺同,長吁一口氣:“原來是場噩夢啊……” “哼蹋艺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起咧栗,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤御毅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后捶索,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體插掂,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年情组,在試婚紗的時候發(fā)現(xiàn)自己被綠了燥筷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡院崇,死狀恐怖肆氓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情底瓣,我是刑警寧澤谢揪,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站捐凭,受9級特大地震影響拨扶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜茁肠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一患民、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧垦梆,春花似錦匹颤、人聲如沸仅孩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辽慕。三九已至,卻和暖如春赦肃,著一層夾襖步出監(jiān)牢的瞬間溅蛉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工他宛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留船侧,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓堕汞,卻偏偏與公主長得像勺爱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子讯检,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評論 2 353