原因1:
mvp中,全部业务逻辑都集中在这个类中,bug的高发区,只要这块测试好了,app稳定性可以大大提高。
原因2:
在mvp架构中model层主要进行负责存储、检索、操纵数据(包括网络请求),这些并不涉及业务逻辑的处理,没能想到可以怎么测试,如果读者有什么好建议可以留言给我;而view层主要进行ui操作,与用户进行交互,更加适合进行UI测试。
二,如何测试Presenter?
总共分为两个步骤,以welcome功能模块为例(检测是否来自其他平台用户登录)
步骤1:
编写契约类,实现mvp
契约类:
/** * 欢迎模块 处理其他平台登录用户 */ public interface WelcomeContract { interface View extends BaseView { void handleError(String errorMsg); void outsideLoginSuccess(LoginBean loginBean); } interface Presenter extends BasePresenter { boolean handleData(Intent data); } interface Model extends BaseModel { void outsideLogin(String jsonData, SubscriberAction subscriberAction); void outsideLoginSuccess(LoginBean loginBean); } } |
public class WelcomePresenter implements WelcomeContract.Presenter { private static final String TAG = "WelcomePresenter"; WelcomeContract.View mView; WelcomeContract.Model mModel; public WelcomePresenter(WelcomeContract.View view, WelcomeContract.Model model) { this.mView = view; this.mModel = model; } @Override public boolean handleData(Intent data) { if (data != null) { try { Uri uri = data.getData(); if (uri != null) { String json = uri.getQueryParameter("data"); JSONObject jsonObject = new JSONObject(json); Logger.t("outsideLogin").e(jsonObject.toString()); if (jsonObject != null) { String userId = null; String userType = null; String source = null; try { userId = jsonObject.getString("userId"); userType = jsonObject.getString("userType"); source = jsonObject.getString("source"); } catch (Exception e) { Logger.e(e, TAG); } if (userId == null) { String errorMsg = "userId为空"; mView.handleError(errorMsg); return true; } if (userType == null) { String errorMsg = "userType为空"; mView.handleError(errorMsg); return true; } if (source == null) { String errorMsg = "source为空"; mView.handleError(errorMsg); return true; } mView.showProgressDialog(); //验证没有问题,请求服务器获取登录数据 mModel.outsideLogin(json, new SubscriberAction<LoginBean>(mView, loginBean -> { mView.dismissProgressDialog(); if (loginBean == null) { mView.handleError("返回数据为空"); } else { mModel.outsideLoginSuccess(loginBean); mView.outsideLoginSuccess(loginBean); } }, throwable -> { throwable.printStackTrace(); if (mView.getVActivity() != null && mView.getVActivity().isFinishing()) { mView.getVActivity().runOnUiThread(() -> { mView.handleError(throwable.getMessage()); mView.dismissProgressDialog(); }); } })); return true; } else { mView.handleError("解析json出错"); return false; } } else { return false; } } catch (Exception e) { Logger.e(e, TAG); mView.handleError("解析登录数据出错"); return true; } } return false; } } |
model层:
public class WelcomeModel implements WelcomeContract.Model { private StudentService mService = StudentRetrofitClient.INSTANCE().getService(); public WelcomeModel() { } @Override public void outsideLogin(String jsonData, SubscriberAction subscriberAction) { StudentRetrofitClient.INSTANCE().toSubscribe(mService.outsideLogin(jsonData), subscriberAction); } @Override public void outsideLoginSuccess(LoginBean loginBean) { LoginBiz.saveLoginData(loginBean); } } |
view层就不贴了,主要关注点在presenter层,model层代码有助于测试中参数捕抓的理解
步骤2:
编写针对presenter的测试类
功能写完后,验证业务逻辑是否能处理各种数据的输入。
特别注意:以前完成了功能后就一直等后台接口数据,接口调通了,心里才踏实;而现在,我不需要等后台接口,直接就能验证presenter的业务逻辑写得好不好,能不能处理各种突发意外情况,这是单元测试的一大好处。单元测试给我最大的感受:一个字:稳 ,两个字:踏实 ,具体一点来说:对自己写的代码不会胆战心惊,不会害怕功能上线了惊呼:我擦,这什么情况?我写的时候完全就没想到会有这种情况发生的!写单元测试其实是意识到自己代码具有局限性的的过程,无论对自己,对项目都是大有裨益的。
测试内容:
1,验证handleData(Intent data)能否处理空数据
2,验证handleData(Intent data)能否处理异常数据
3,验证handleData(Intent data)能否处理正常数据
WelcomePresenterTest:
/** * Android单元测试示例 * 使用框架简介: * junit(纯java代码可用该框架测试), * mockito(模拟数据), * robolectric(模拟Android运行环境,可以测试Android代码) * 纯java部分的可以通过Junit4来进行单元测试, * 而对于用到android自身代码的测试不能依靠Junit进行, * 对于这种情况解决方案之一就是使用Robolectric */ /** * 知识点1,runWith:RobolectricTestRunner * 表示测试时使用robolectric运行环境,可以测试Android代码,比如:textview.setText()这样的代码 * 如果测试Presenter中没有涉及Android代码,则不要加,否则拖慢测试速度。 */ @RunWith(RobolectricTestRunner.class) /** * 知识点2,指定manifest文件,格式如下: * @Config(manifest = "../app/AndroidManifest.xml") * */ @Config(manifest = Config.NONE) public class WelcomePresenterTest { WelcomeContract.Presenter mPresenter; /** * 知识点3,@mock 注解介绍: * 模拟某个类对象 * 为什么要模拟? * 答:因为这是测试环境,view对象的获取很麻烦很困难,并且view并不是我们测试的对象。 */ @Mock WelcomeContract.View mView; @Mock WelcomeContract.Model mModel; /** * 知识点4:参数捕抓器 * 用于捕抓model层方法中的参数 */ ArgumentCaptor<SubscriberAction> captor; /** * 在测试前的数据初始化 */ @Before public void setUp() { //Mockito的初始化 MockitoAnnotations.initMocks(this); /** * 知识点5:Presenter的创建 * 注意:在view层就需要创建model,将之作为presenter的构造方法参数。 * 对比之前的写法:mPresenter = new WelcomePresenter(this)的写法 * 这样的写法好处:model可以在测试中模拟,如果model完全隐藏在presenter的 * 构造方法中,model还需要用参数捕抓出来,比较麻烦。 */ mPresenter = new WelcomePresenter(mView, mModel); captor = ArgumentCaptor.forClass(SubscriberAction.class); /** *知识点6: 把将Rxjava接口调用的异步操作变成同步,加快测试速度。 */ UnitTestHelper.openRxTools(); } /** * 传递给presenter的参数异常的测试 * * @throws Exception */ @Test public void handleDataFail() throws Exception { Intent intent = mock(Intent.class); Uri uri = mock(Uri.class); intent.setData(uri); when(uri.getQueryParameter("data")) //模拟数据为空情况 // .thenReturn(null) //模拟数据缺失情况,少了userId .thenReturn("{\"source\":\"xxxx\",\"userType\":\"xxxx\"}"); when(intent.getData()).thenReturn(uri); mPresenter.handleData(intent); // assertFalse(mPresenter.handleData(intent)); verify(mView).handleError(any(String.class)); } /** * 传递给presenter的参数正常的测试 * @throws Exception */ @Test public void handleDataSuccess() throws Exception { /** * 模拟数据 */ Intent intent = mock(Intent.class); Uri uri = mock(Uri.class); when(uri.getQueryParameter("data")).thenReturn("{\"userId\":\"xxxx\",\"source\":\"xxxx\",\"userType\":\"xxxx\"}"); when(intent.getData()).thenReturn(uri); mPresenter.handleData(intent); /** * mPresenter.handleData调用后 * 1,验证(verify)model是否调用了outsideLogin方法, * 2,并且捕获outsideLogin方法参数subscriberAction对象 */ verify(mModel).outsideLogin(any(String.class), captor.capture()); /** * 疑问:为什么要捕抓subscriberAction对象? * 答:因为模拟调用接口成功中需要用到subscriberAction这个订阅者对象。 * */ UnitTestHelper.mockCallBack(new LoginBean(), captor.getValue()); /** * 接口数据LoginBean成功模拟返回后 * 验证(verify)Presenter是否调用了model以及view中outsideLoginSuccess方法。 */ verify(mModel).outsideLoginSuccess(any(LoginBean.class)); verify(mView).outsideLoginSuccess(any(LoginBean.class)); } } |
UnitTestHelper单元测试工具类:
/** * 用于: *1,模拟model中网络请求返回的数据 *2,把RXJava的异步变成同步,方便测试 */ public class UnitTestHelper { public static void mockFailCallBack(SubscriberAction sub) { mockCallBack(99,"我错了",null,sub); } public static void mockFailCallBack(int resultCode,String msg,SubscriberAction sub) { mockCallBack(resultCode,msg,null,sub); } public static void mockEmptyCallBack(SubscriberAction sub) { mockCallBack(0,"模拟接口调用成功",null,sub); } public static void mockCallBack(Object data,SubscriberAction sub) { mockCallBack(0,"模拟接口调用成功",data,sub); } public static void mockCallBack(int resultCode,String msg,Object data,SubscriberAction sub) { BaseRetrofitClient.toSubscribe(Observable.just(new HttpResult<>(resultCode,msg,data)),sub); } private static boolean isInitRxTools = false; /** * 把RXJava的异步变成同步,方便测试 */ public static void openRxTools() { if (isInitRxTools) { return; } isInitRxTools = true; RxAndroidSchedulersHook rxAndroidSchedulersHook = new RxAndroidSchedulersHook() { @Override public Scheduler getMainThreadScheduler() { return Schedulers.immediate(); } }; RxJavaSchedulersHook rxJavaSchedulersHook = new RxJavaSchedulersHook() { @Override public Scheduler getIOScheduler() { return Schedulers.immediate(); } }; // reset()不是必要,实践中发现不写reset(),偶尔会出错,所以写上保险 RxAndroidPlugins.getInstance().reset(); RxAndroidPlugins.getInstance().registerSchedulersHook(rxAndroidSchedulersHook); RxJavaPlugins.getInstance().reset(); RxJavaPlugins.getInstance().registerSchedulersHook(rxJavaSchedulersHook); } } |
这两个类是这篇博客的精华所在,耗费了我们Android组不少时间,不少精力探索出来的,有兴趣的读者可以慢慢读这段代码,收获会超乎想象。
三,Android测试填坑
1,选框架的坑
非常建议采用robolectric框架,工欲善其事必先利其器,一开始没有选择robolectric框架,就开始撸单元测试,摔得脸好疼,郁闷了一整天:明明我这样写单元测试没有错的呀,怎么就死活都没法通过测试呢?
原因在于mvp中测试presenter过程中无可避免会调用Android系统API,而junit不支持,mock也不可能面面俱到,有些方法中Android API藏得比较深,很难都mock到,而用了robolectric框架就完全没有问题。
robolectric原理:实现一套JVM能运行的Android代码,然后在unit test运行的时候去截取android相关的代码调用,然后转到他们的他们实现的Shadow代码去执行这个调用
1,创建单元测试类的小坑
有个同事不知道AS能自动生成测试类,然后说,单元测试好麻烦,创建一个类要写这么多东西。
贴上一个自动创建测试类的小教程:
自动创建测试类-步骤3.png
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。