原文:Android官方MVP架构示例项目解析

Google在3月份推出了一个项目,用来介绍Android MVP架构的各种组合,可以认为是官方在这方面的最佳实践。令人称道的是除了MVP本身之外,这些工程配备了极其完善的单元测试用例,学习价值极高。本文着重针对todo-mvp的单元测试进行解读。

写在前面

  1. 关于MVP
    关于MVP的介绍很多,这不是本文的重点,这里列举近期一些比较好的文章。

    • Android官方MVP架构示例项目解析

    • 选择恐惧症的福音!教你认清MVC,MVP和MVVM
      这篇文章对MVC/MVP/MVVM有很多自己的思考,为什么要有这样架构的演变,以及各自的优缺点的思考,很棒!

    • 如何设计MVP中的Presentation层
      大部分业务场景一个View对应一个Presenter,但是如果一个界面需要多个View/Presenter或者同一个View有多个实现且使用同一个PresenterView时候,如何来设计Presenter,这篇文章是很好的延伸阅读。

  2. 关于单元测试
    对于单元测试,需要预先了解以下内容

    • Android Studio的test和AndroidTest

    • AndroidJUnitRunner:一个兼容Junit4的Andriod单元测试框架

    • Mockito:单元测试利器

    • Espresso:支持UI测试的单元测试框架

  3. 关于todo-mvp的功能

Android官方MVP项目单元测试_第1张图片

功能介绍


简而言之,这个工程包含了三个模块:待办事项列表模块,待办事项详情模块,统计模块。


MVP各层的单元测试选型

在该项目中,MVP各层所使用的单元测试框架如下图所示:

Android官方MVP项目单元测试_第2张图片

官方todo-mvp的UT选型

  • P层:不需要任何Android环境,因此使用Junit测试即可

  • V层:使用Google强大的Espresso进行UI的测试

  • M层:涉及到数据库相关操作,因此需要依赖Android环境,使用AndroidJUnitRunner进行测试

在此处,我们先大致了解一下MVP各层的UT选型,然后通过一个例子,看看各层之间如何配合测试,最后再对各层UT选型的原因进行分析,从而理解整体测试架构。

接下来我们以TO-DO List页面(TasksActivity/TaskFragment)中加载任务列表功能为例,此场景的功能界面如下图所示:

Android官方MVP项目单元测试_第3张图片

待办任务列表


Presenter层的测试

在这个功能里,Presenter只做了一件事情,就是loadTask(),时序图如下所示:

Android官方MVP项目单元测试_第4张图片

loadTask的时序图]

从时序图上看,loadTask执行的逻辑是,1.调用View层开启进度条->2.从Model层获取待办任务列表->3.Model层以回调函数的形式返回数据->4.调用View层关闭进度条->5.调用View层显示任务列表。这5个步骤里,每个步骤的逻辑是否准确是View层和Model层该测试的事情,对于Presenter层来讲,他的测试任务是确保这5个步骤如期调用。为了达成此目的,我们会采用Mockito.verify()的api进行测试,这个测试类是TasksPresenterTest,代码如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test publicvoidloadAllTasksFromRepositoryAndLoadIntoView(){ //确保当前视图是All视图 mTasksPresenter.setFiltering(TasksFilterType.ALL_TASKS); //第0步:开始加载数据 mTasksPresenter.loadTasks( true ); //验证第2步:获取待办事项的逻辑有调用 verify(mTasksRepository).getTasks(mLoadTasksCallbackCaptor.capture()); //通过Mockito的Capture进行回调函数的测试,对应第3步 mLoadTasksCallbackCaptor.getValue().onTasksLoaded(TASKS); //验证第1步:进度条显示 verify(mTasksView).setLoadingIndicator( true ); //验证第4步:进度条关闭 verify(mTasksView).setLoadingIndicator( false ); ArgumentCaptor<List>showTasksArgumentCaptor=ArgumentCaptor.forClass(List.class); //验证第5步:View层显示待办任务列表 verify(mTasksView).showTasks(showTasksArgumentCaptor.capture()); //在Before周期里,事先初始化了3条待办任务数据 assertTrue(showTasksArgumentCaptor.getValue().size()==3); }

注:这里涉及到异步回调函数如何测试的问题,使用Mockito的Capture可以解决此问题。具体细节,三言两语说不清,后续考虑专门写篇文章。

总结:让Presenter充当个合格的皮条客,去调用其他两层的逻辑,在假设其他两层代码逻辑都是正确的前提下,做一些mock测试,尽可能覆盖所有逻辑路径。

View层的测试

这一层的测试其实很清晰,站在QA的角度,我们想要验证待办任务列表时候,会设计以下的测试用例:

blob.png

验证待办任务列表的测试流程



通过Espresso可以模拟这些步骤,并进行验证,这个测试类是TasksScreenTest,代码如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 @Test publicvoidshowAllTasks(){ //添加2个待办任务,对应第1、2、3步 createTask(TITLE1,DESCRIPTION); createTask(TITLE2,DESCRIPTION); //切换为All视图,对应第4步 viewAllTasks(); //验证Title1和Title2对应的Item存在,对应第5步 onView(withItemText(TITLE1)).check(matches(isDisplayed())); onView(withItemText(TITLE2)).check(matches(isDisplayed())); }

其中,createTask()的实现如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 privatevoidcreateTask(Stringtitle,Stringdescription){ //点击添加按钮,对应第1步 onView(withId(R.id.fab_add_task)).perform(click()); //打开软键盘,输入标题和描述,对应第2步 onView(withId(R.id.add_task_title)).perform(typeText(title), closeSoftKeyboard()); onView(withId(R.id.add_task_description)).perform(typeText(description), closeSoftKeyboard()); //保存待办任务,对应第3步 onView(withId(R.id.fab_edit_task_done)).perform(click()); }

viewAllTasks()的实现如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 privatevoidcreateTask(Stringtitle,Stringdescription){ //点击添加按钮,对应第1步 onView(withId(R.id.fab_add_task)).perform(click()); //打开软键盘,输入标题和描述,对应第2步 onView(withId(R.id.add_task_title)).perform(typeText(title), closeSoftKeyboard()); onView(withId(R.id.add_task_description)).perform(typeText(description), closeSoftKeyboard()); //保存待办任务,对应第3步 onView(withId(R.id.fab_edit_task_done)).perform(click()); }

连上设备,跑起UT,会自动启动相应的Activity界面,做相应的操作后进行测试。

总结:Espresso好强大,而且这一层的测试站在用户的角度,所有逻辑是黑盒,在功能层面测试输入(用户操作)输出(用户得到的界面反馈),而技术层面,由于界面是所有层的入口,得到输出后,除了测试View层本身的逻辑之外,其实已经粗糙的覆盖了M和P的逻辑了。

Model层的测试

关于Model层的测试,首先要了解下该项目中,model层的设计,类层次如下图所示:

Android官方MVP项目单元测试_第5张图片

Model层的类图

  • TasksLocalDataSource:负责本地数据库增删改查操作

  • TasksRemoteDataSource:负责网络请求(该项目中用handler.postDelayed()延时来模拟网络请求)

  • TasksRepository:相当于整个Model层的门面,根据逻辑判断决定数据来自于本地数据库或是网络。Presenter层只与它打交道。

根据以上分析,可见对Model层的测试要完整的覆盖这三个类。

  1. 我们先看门面TasksRepository的测试,先看看这个类中有关获取待办任务列表的流程图:

    Android官方MVP项目单元测试_第6张图片

    TasksRepository流程图

    所以对于TasksRepository来讲,测试的内容主要是验证1,2,3的逻辑是否在相应的输入下覆盖到位,对于1,2,3的数据准确性无需关心,由各自DataSource去验证,因此它的测试与Android环境无关,用Junit+Mockito测试。要完整覆盖的话,需要多个测试case,篇幅有限,这里只讲第2种。这个测试类是TasksRepositoryTest,代码如下:

    @TestpublicvoidgetTasksWithDirtyCache_tasksAreRetrievedFromRemote(){//将数据设置为脏数据mTasksRepository.refreshTasks();//数据为脏数据,因此此时需要从网络获取mTasksRepository.getTasks(mLoadTasksCallback);//验证第2种情况:用TasksRemoteDataSource调用getTasks()获取数据后返回setTasksAvailable(mTasksRemoteDataSource,TASKS);//验证第1种情况没有发生verify(mTasksLocalDataSource,never()).getTasks(mLoadTasksCallback);//验证TasksRemoteDataSource执行了回调函数verify(mLoadTasksCallback).onTasksLoaded(TASKS);}

    其中,setTasksAvailable()代码如下:

    privatevoidsetTasksAvailable(TasksDataSourcedataSource,List<Task>tasks){//验证第2种情况:使用TasksRemoteDataSource调用getTasks()verify(dataSource).getTasks(mTasksCallbackCaptor.capture());//执行回调函数mTasksCallbackCaptor.getValue().onTasksLoaded(tasks);}
  2. 接下来是是TasksLocalDataSource的测试。该测试与数据库有关,因此依赖于Android环境,且要验证数据存取的准确性,因此需要做一些断言,使用AndroidJUnitRunner进行测试,这个类是TasksLocalDataSourceTest,代码如下:

    @TestpublicvoidgetTasks_retrieveSavedTasks(){//事先往DB中插入两条数据finalTasknewTask1=newTask(TITLE,"");mLocalDataSource.saveTask(newTask1);finalTasknewTask2=newTask(TITLE,"");mLocalDataSource.saveTask(newTask2);//执行获取数据列表的方法,并在回调函数中进行断言mLocalDataSource.getTasks(newTasksDataSource.LoadTasksCallback(){@OverridepublicvoidonTasksLoaded(List<Task>tasks){//断言数据非空,且有>=2条的Task数据assertNotNull(tasks);assertTrue(tasks.size()>=2);booleannewTask1IdFound=false;booleannewTask2IdFound=false;for(Tasktask:tasks){if(task.getId().equals(newTask1.getId())){newTask1IdFound=true;}if(task.getId().equals(newTask2.getId())){newTask2IdFound=true;}}//验证查询出的数据包含事先插入的数据assertTrue(newTask1IdFound);assertTrue(newTask2IdFound);}@OverridepublicvoidonDataNotAvailable(){fail();}});}
  3. 最后来看看跟网络请求相关的TasksRemoteDataSource的测试
    Google并没有对这个类本身进行测试,但是对其他层依赖网络请求数据进行测试的场景做了支持。试想一下,通过上面的分析,我们知道View层是真刀真枪的在模拟用户的操作进行测试,如果某个测试case需要发起网络请求,此时我们不知道何时才能返回数据,且由于网络状况等原因可能导致请求失败,种种不确定因素下,是不可能完成一个测试的,解决的办法很简单,就是对网络请求进行Fake,这个类是FakeTasksRemoteDataSource,原理便是当需要用到TasksRemoteDataSource时,不会真正使用该类,而是注入FakeTasksRemoteDataSource,返回事先定义好的数据。

    为此,这个项目在项目结构和代码方面提供了很多支撑,体现在:

    • 提供了mock和prod两种Flavors

    • 两种Flavor分别提供了Injection,注入Fake类或真实类

    • 所有与网络请求相关的测试代码存放在androidTestMock下

总结:Model层的测试时而在androidTest写UT,时而在test里写,时而在androidTestMock里,有点精神分裂的感觉。但是,真的好清晰,看起测试的结构来非常舒服。

MVP的单元测试架构总结

通过这个例子,我们已经了解了MVP各层之间的职责以及对应的测试内容,接下来做个总结,首先看下MVP测试架构图:

Android官方MVP项目单元测试_第7张图片

MVP测试架构图

  1. View层

    • 职责:MVP模式下,View层终于扬眉吐气了,View本身该做的事情都能做了,比如UI布局,数据渲染,点击按钮交互等等

    • 测试方式:以正常小QA的测试思维方法,就可以来定义这一层的测试方式,测试过程中需要真机或模拟器,并做真实的操作。

    • 测试选型:依赖于Android环境,用谷歌强大的Espresso+AndroidJUnitRunner,Espresso用于模拟和验证各种各样的UI操作,代码存放于AndroidTest中。

  2. Presenter层:

    • 职责:这一层是拉皮条的,负责M和V层的对接,所以有较少的处理输入输出的机会,他只用来控制逻辑,去调用相应的Model和View的逻辑。

    • 测试选型:他的职责决定了他很少去断言输入输出,测试逻辑覆盖的路径是否正确即可,因此他与Android环境无关,用Junit+Mockito测试即可,代码存放于test中。

  3. Model层

    • 职责:负责数据的存取,数据可能来自于网络、数据库和内存

    • 数据库增删改查:需测试数据存取的准确性,依赖Android环境进行测试,因此使用AndroidJUnitRunner,代码存放于androidTest中

    • 网络请求:不测试真实的网络请求,但提供了Fake供其他层调用测试。

    • 封装的门面类:决定了数据的来源和去向是来自于本地数据库 or 网络 or 内存,此为真正对其他层暴露的Model类。此类不做数据准确性的验证,只做mock测试,验证覆盖路径。UT选型Junit+Mockito,代码存放于test中。

最后

Android官方MVP架构示例项目在单元测试方面真是良心之作,分析测试用例远比分析MVP本身得到的收获多得多,感谢Google,感谢他粗壮的大腿,抱大腿的感觉真好。

此外,在做架构时,不能忽视在单元测试方面的架构,所以,好的架构是可以支撑代码的可测试性的,Google给我们做了非常棒的最佳实践,接下来就是各自的项目实践,不妨从某个模块开始,步步为营,写好MVP,补齐单元测试用例。

喜欢此文,觉得此文有用,请打赏^_^!

附录

『如何写有价值的测试用例』也是非常重要的话题,在todo-mvp中大大小小的测试用例也有几十个,所以耐心的看看测试代码,可以给我们带来很多思路和指导,由于这部分篇幅较长,且枯燥无味,因此另起一篇文章,有需要的请前往这里。



文/geniusmart(简书作者)
原文链接:http://www.jianshu.com/p/cf446be43ae8#

更多相关文章

  1. android ui线程和数据的分离
  2. android 自动化压力测试-monkey 1 实践
  3. Android 数据缓存-文件存储
  4. android 保存Json数据到本地
  5. Android ORMLite数据库简介
  6. adb shell 命令详解 在Android中查看和管理sqlite数据库
  7. Android SDK Web SDK 接口测试总结

随机推荐

  1. Android中实现SQLite数据库CRUD操作的两
  2. 基于netty 的android Socket 聊天室客户
  3. Android使用广播实现app开机自启动
  4. Android WebView显示广告
  5. android Cache——webview的缓存处理
  6. Android数据库中查找一条数据 query方法
  7. Android一种View动态折线绘制的实现实战
  8. Android9.0默认使用MTP模式
  9. Android 系统联系人操作
  10. android launcher壁纸滚动图片拉伸问题