Robolectric 之 Jessyan Arm框架之Mvp
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.pngLoginActvity
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
- 写好最基本测试迫不急待地运行
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,一个温润的男子,版权所有,未经允许不得抄袭。