Robolectric 之 Jessyan Arm框架之Mvp 单元测试解耦过程(View 层)

Robolectric 实战解耦整个系列:
Robolectric 之 Jessyan Arm框架之Mvp 单元测试解耦过程(View 层)
Robolectric 之 Jessyan Arm框架之Mvp 单元测试解耦过程(Presenter 层)
Robolectric 之 Jessyan Arm框架之Mvp 单元测试本地json测试
github的链接: https://github.com/drchengit/Robolectric_arm_mvp

我的项目用的是jassyan的arm快速开发框架!
我做robolectric单元测试时,想哭!!
因为他的dagger2 用得飞起,我写代码的时候一边用一边说着:
"牛啤了!大兄弟!"
“哦!还能这么用,牛啤!牛啤”
但是在解耦单元测试的时候:
“这是啥哟!这个东西从哪里来的?”
“又报一个空指针”
“我是照着demo代码一步步敲下来的!报错是啥子情况?”
如果你用了arm 而且要做Robolectric 单元测试的话,不妨看看哟!如果不会Robolectric 建议先学学再看:测试资源放送

第一步: 写个登录功能

熟悉的配方新建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("手机号码不正确");
            return;
        }
        if(mRootView.getPassWordStr().length() < 1){
            mRootView.showMessage("密码太短");
            return;
        }
        //调用登录接口,正确的密码:abc  手机号只要等于11位判断账号为正确
        mModel.login(mRootView.getMobileStr(),mRootView.getPassWordStr())
                .compose(RxUtils.applySchedulers(mRootView))
                .subscribe(new MyErrorHandleSubscriber<User>(mErrorHandler) {
                    //这个类是我自定义的一个类,统一拦截所有error 并回调给: ResponseErrorListenerImpl
                    // 可以不统一处理,直接重写覆盖:
//                     @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) {
        //调用登录接口,正确的密码:abc  手机号只要等于11位判断账号为正确
        String name;
        if(passWordStr.equals("abc")){//正确密码,
            name = "drchengit";
        }else {
            name = "drchengi";
        }

        //由于不知道上哪里去找一个稳定且长期可用的登录接口,所以用的接口是github 上的查询接口:https://api.github.com/users/drchengit
        // 这里的处理是正确的密码,请求存在的用户名:drchengit  错误的密码请求不存在的用户名: drchengi
        // 将就一下
        return mRepositoryManager.obtainRetrofitService(CommonService.class).getUser(name);
    }
···
}

注意我通过Okhttp 的插值器 回调GlobalHttpHandlerImpl 类的onHttpResultResponse()方法,如果返回 "no found" 内部会 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;
    }

 ···
}

我省略了过程,总之就是输入正确的手机号和密码就可以登录,输错就会提示"密码错误"。

第二步导包和配置

其实androidx 已经出了,https://github.com/robolectric/robolectric,但是jessyan在简书回复我androidx 现在没打算适配(第一次收到作者的回复,可把我牛逼坏了,学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
  • 写好最基本测试迫不急待地运行
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(){
        //直接点击登录
        loginTv.performClick();
        Assert.assertEquals("手机号码不正确", ShadowToast.getTextOfLatestToast());


    }

}
  • 第一个问题,泄露框架出问题,点过去看下,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 是内存泄露,对单元测试没啥用,直接try {}catch

  • 再次运行,ok,一路绿灯,下面进行登录接口测试
    @Test
    public  void login(){
        //直接点击登录
//        loginTv.performClick();
//        Assert.assertEquals("手机号码不正确", ShadowToast.getTextOfLatestToast());
        phoneEt.setText("13547250999");
        //没有输入密码
//        loginTv.performClick();
//        Assert.assertEquals("密码太短", ShadowToast.getTextOfLatestToast());
        //错误密码
        passWrodEt.setText("aaaa");
        loginTv.performClick();
        //这里是验证网络框架提示
        Assert.assertEquals("密码错误", ShadowToast.getTextOfLatestToast());

    }
  • 报了null,根本没有打toast
    图片.png
  • debug了半天,发现没有回调
    图片.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();
            }
        });

    }
  • 心想现在应该没有问题了吧!结果还是同样的问题,没有打印toast!
    接下来我就进入了终极debug和翻源码模式,终于看到了这样一段代码
@Singleton
public class RepositoryManager implements IRepositoryManager {
 ···
    private <T> T createWrapperService(Class<T> serviceClass) {
        // 通过二次代理,对 Retrofit 代理方法的调用包进新的 Observable 里在 io 线程执行。
        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);
                                // 执行真正的 Retrofit 动态代理的方法
图片.png
                        }
                        // 返回值不是 Observable 的话不处理
                        final T service = getRetrofitService(serviceClass);
                        return getRetrofitMethod(service, method).invoke(service, args);
                    }
                });
    }
···
}
  • 莫不是sign线程没有同步???加上Sign()线程同步,调用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();
        //直接点击登录
        loginTv.performClick();
        Assert.assertEquals("手机号码不正确", ShadowToast.getTextOfLatestToast());
        phoneEt.setText("13547250999");
        //没有输入密码
        loginTv.performClick();
        Assert.assertEquals("密码太短", ShadowToast.getTextOfLatestToast());
        //错误密码
        passWrodEt.setText("aaaa");
        loginTv.performClick();
        //这里是验证网络框架提示
        Assert.assertEquals("密码错误", ShadowToast.getTextOfLatestToast());
        //正确密码登录
        passWrodEt.setText("abc");
        loginTv.performClick();
        Assert.assertEquals("登录成功",ShadowToast.getTextOfLatestToast());

        //验证跳转
        ShadowActivity shadowActivity = Shadows.shadowOf(loginActivity);
        Intent intent = shadowActivity.getNextStartedActivity();
        Assert.assertEquals(intent.getComponent().getClassName(), MainActivity.class);


    }
  • 一路绿灯,到这里View 层的Robolectric单元测试 才算完成,后面是Presenter 的业务解耦

Robolectric 实战解耦整个系列:
Robolectric 之 Jessyan Arm框架之Mvp 单元测试解耦过程(View 层)
Robolectric 之 Jessyan Arm框架之Mvp 单元测试解耦过程(Presenter 层)
Robolectric 之 Jessyan Arm框架之Mvp 单元测试本地json测试
github的链接: https://github.com/drchengit/Robolectric_arm_mvp
测试资源放送

基本的配置 | https://www.jianshu.com/p/7a4024925193
常见的坑(分包导致测试报错等) | https://blog.csdn.net/weixin_34204057/article/details/91418305

我是drchen,一个温润的男子,版权所有,未经允许不得抄袭。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266