RxJava2关于Junit的错误解决方法

背景

这段时间做的项目用到了RxJava2,整体框架为MVP。
因为我公司的电脑差的厉害,没有硬件加速,项目环境起步就是5.0,所以只能用真机调试,公司的测试机比电脑还差,慢的要死,种种问题的压迫。
我一拍脑门,决定Junit来进行测试我的P层逻辑,反正数据这种东西,测试通的过,到真机上面基本也就是刷新视图界面的问题了。


问题

在整体coding的过程中还是遇到了部分棘手的问题,因为以往很少用Junit,应该说从来没有用过,所以踩了很多坑,我遇到的问题是在单元测试的过程中,无法用Rxjava2进行测试,而我MVP的主要核心就是Rxjava2,本文也主要是解决这个问题。


Rxjava2 & Junit 引入

依赖:

dependencies {
    ...

    testImplementation 'junit:junit:4.12'  

    implementation 'io.reactivex.rxjava2:rxjava:2.0.4'  

    implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'  

    implementation 'com.google.code.gson:gson:2.8.2'  

}

注:因为我用的gradle版本比较高,所以‘compile’ 被 ‘implementation’取代了。junit应该是自带的,所以也可以不用引入。

Rxjava2 & Presenter

一个Rxjava的例子,因为我的项目是以MVP为基础的,所以这个demo也是以MVP为基础的,没有MVP基础的可以先去学一下,项目是简化了很多版的MVP框架。
我先将我整个Demo给贴上,在叙述具体的问题解决方式。

目录结构

mark

Presenter

在Presenter层编写获取信息的代码,处理数据层信息,将结果反馈给视图层,以下是我Presenter层的代码:

package com.qingheyang.testjr;

//省略包
import ...;
/**
 * project: TestJR
 * package: com.qingheyang.testjr
 * creater: qingheyang
 * date: 2018/2/17
 * describe:
 */
public class MainPresenter {
    MainActivity activity;

    public MainPresenter(MainActivity activity) {
        this.activity = activity;
    }

    /**
     * 获取user信息
     */
    public void getUser(final String path) {
        Observable.create(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(ObservableEmitter<String> e) throws Exception {
                String json = HttpUtils.httpGet(path);//请求网络
                if (json!=null) {//判断服务器返回数据是否为空
                    e.onNext(json);
                }else {
                    e.onError(new Exception("获取user失败"));
                }
            }
        }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<String>() {
                    @Override
                    public void onSubscribe(Disposable d) {

                    }

                    @Override
                    public void onNext(String s) {
                        System.out.println(s);//因为junit输出日志不能用log,所以改用sys。
                        Gson gson = new Gson();
                        User user = gson.fromJson(s, User.class);
                        //处理数据成功的话,交给activity的回调
                        activity.getDateSuccess(user);
                    }

                    @Override
                    public void onError(Throwable e) {
                        //失败交给activity给用户返回失败信息
                        activity.getDateFailed(0x001);
                    }

                    @Override
                    public void onComplete() {

                    }
                });
    }
}

MainActivity

package com.qingheyang.testjr;

//省略包
import ...;

public class MainActivity extends AppCompatActivity {
    private Button button;
    private MainPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initUi();
    }

    private void initUi() {
        button = findViewById(R.id.main_btn);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                presenter.getUser(HttpUtils.URL);//地址要自己更换
            }
        });
    }

    /**
     * 成功的回调
     * @param user
     */
    public void getDateSuccess(User user){
        //doSomethings...
    }

    /**
     * 失败的回调
     * @param code
     */
    public void getDateFailed(int code){
        switch (code){
            case 0x001:
                Toast.makeText(this, "获取信息失败", Toast.LENGTH_SHORT).show();
                break;
            case 0x002:
                break;
        }
    }
}

HttpUtils

工具类,因为是demo,获取方式为GET请求:

package com.qingheyang.testjr;

//省略包
import ...;

/**
 * project: TestJR
 * package: com.qingheyang.testjr
 * creater: qingheyang
 * date: 2018/2/17
 * describe:http get请求
 */
public class HttpUtils {

    //此处更换ip地址
    public static final String URL = "http://192.168.1.1:8080";

    public static String httpGet(String path) {
        URL url = null;
        HttpURLConnection conn = null;
        InputStream is = null;
        ByteArrayOutputStream baos = null;
        try {
            url = new URL(path);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        if (url != null) {
            try {
                conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("GET");
                conn.setReadTimeout(8000);
                conn.setConnectTimeout(8000);
                conn.connect();
                int responseCode = conn.getResponseCode();
                if (responseCode == HttpURLConnection.HTTP_OK) {
                    is = conn.getInputStream();
                    baos = new ByteArrayOutputStream();
                    byte[] buffer = new byte[1024];
                    int len = 0;
                    while (-1 != (len = is.read(buffer))) {
                        baos.write(buffer, 0, len);
                        baos.flush();
                    }
                    is.close();
                    baos.close();
                    return baos.toString("utf-8");
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (is != null) {
                    try {
                        is.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (baos != null) {
                    try {
                        baos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                conn.disconnect();
            }
        }
        return null;
    }

}

User

javaBean:

package com.qingheyang.testjr;

import java.util.List;

/**
 * project: TestJR
 * package: com.qingheyang.testjr
 * creater: qingheyang
 * date: 2018/2/17
 * describe: javaBean
 */
public class User {

    private List<MessageInfoBean> messageInfo;

    public List<MessageInfoBean> getMessageInfo() {
        return messageInfo;
    }

    public void setMessageInfo(List<MessageInfoBean> messageInfo) {
        this.messageInfo = messageInfo;
    }

    public static class MessageInfoBean {

        private String msg1;
        private String author;
        private String view;

        public String getMsg1() {
            return msg1;
        }

        public void setMsg1(String msg1) {
            this.msg1 = msg1;
        }

        public String getAuthor() {
            return author;
        }

        public void setAuthor(String author) {
            this.author = author;
        }

        public String getView() {
            return view;
        }

        public void setView(String view) {
            this.view = view;
        }
    }
}

Junit

  • 如果你已经依赖了Junit了,请继续往下看,没有的话,我上面已经附上了依赖。
  • 新建一个Presenter的单元测试类。
    在要测试的方法上面点击右键,选择goto,选择最后一个test。
    mark
    点击后出现Create New Test,点击。
    mark
    出现Create Test的对话框,勾选setUp,tearDown,以及你想测试的方法,getUser,点击OK。
    mark
    左边的列表中会出现刚才建立的MainPresenter。
    mark
  • test代码
package com.qingheyang.testjr;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.*;

/**
 * project: TestJR
 * package: com.qingheyang.testjr
 * creater: qingheyang
 * date: 2018/2/18
 * describe:MainPresenter的单元测试
 */
public class MainPresenterTest {
    private MainPresenter presenter;
    @Before
    public void setUp() throws Exception {
        presenter = new MainPresenter();
    }

    @After
    public void tearDown() throws Exception {
        presenter = null;
    }

    @Test
    public void getUser() {
        presenter.getUser(HttpUtils.URL);
    }
}

以上就是rxjava+MVP的全部代码。
当然这样肯定会出错的。
顺便说一下,服务器搭建是Linux平台上面随便启动的一个tomcat服务器。


Run & Error

运行一下,单元测试的运行是在方法上面运行:
可以点击左边的运行按钮,也可以对方法点击右键。
mark
运行过后会发现报错,原因一是因为部分Android的API,Junit不支持,没法做到完美支持,会报错。
解决方法有两个。

  • 依赖mock。
  • 注释掉关于Android的代码。(我选择注释)
    也就是Rxjava中观察者中的onNext方法中关于Android的部分代码要注释。
    @Override
                      public void onNext(String s) {
                          System.out.println(s);//因为junit输出日志不能用log,所以改用sys。
                          //Gson gson = new Gson();
                         // User user = gson.fromJson(s, User.class);
                          //处理数据成功的话,交给activity的回调
                          //activity.getDateSuccess(user);
                      }
    

再次运行。
mark
再次发现错误,也是我遇到的核心问题:Rxjava2在Junit中的线程问题,以及如何用API来解决它。


解决

这个问题粗略的说一下其实也很简单,就是Rxjava2的子线程与主线程在电脑上面运行无法同步的问题。
解决方法就是将子线程与安卓UI线程调整至同一线程即可。
也搜索过很多网上的解决方法,基本都是Rxjava1的解决方法,Rxjava2的又一般是kotlin,直接上代码吧。

package com.qingheyang.testjr;

//省略包
import ...;

/**
 * 项目名称:
 * 类创建者:QHY.
 * 时间:2017/12/21
 * 类说明:
 */

public class RxTools {
    public static void asyncToSync() {

        /**
         * 因为RxJava2已经没有了immediate线程,所以要自己做一个线
         */

        final Scheduler immediate = new Scheduler() {
            @Override
            public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
                return super.scheduleDirect(run, 0, unit);
            }

            @Override
            public Worker createWorker() {
                return new ExecutorScheduler.ExecutorWorker(new ScheduledThreadPoolExecutor(1) {
                    @Override
                    public void execute(@NonNull Runnable runnable) {
                        runnable.run();
                    }
                });
            }
        };

        /**
         * 将自己写的线程转换成为一个调度线程
         */
        Function<Callable<Scheduler>, Scheduler> schedulerFunc = new Function<Callable<Scheduler>, Scheduler>() {
            @Override
            public Scheduler apply(Callable<Scheduler> schedulerCallable) throws Exception {
                return immediate;
            }


        };

        /**
         * 将单元测试的所有调度线程都统一一个线程
         */
        RxJavaPlugins.reset();
        RxJavaPlugins.setInitIoSchedulerHandler(schedulerFunc);
        RxJavaPlugins.setInitComputationSchedulerHandler(schedulerFunc);
        RxJavaPlugins.setInitNewThreadSchedulerHandler(schedulerFunc);
        RxAndroidPlugins.reset();
        RxAndroidPlugins.setInitMainThreadSchedulerHandler(schedulerFunc);
    }

    public static void resetPlugins(){
        RxJavaPlugins.reset();
        RxAndroidPlugins.reset();
    }

}

使用方法很简单,如下:

@Before
   public void setUp() throws Exception {
       RxTools.asyncToSync();
       presenter = new MainPresenter();
   }

   @After
   public void tearDown() throws Exception {
       RxTools.resetPlugins();
       presenter = null;
   }

在MainPresenterTest中,setUp以及tearDown中分别调整下代码即可。
结果如下:
mark


这个项目也可以在我的github中找到,如果有帮到你,欢迎给一个star。
链接可以在主页GitHub图标获取。
以上

请我喝杯咖啡吧~

支付宝
微信