(MVP+RxJava+Retrofit)解耦+Mockito单元测试 经验分享
来源:互联网 发布:linux 格式化硬盘 编辑:程序博客网 时间:2024/06/06 02:17
前言
首先,对于MVP、RxJava还不了解的同学,请先阅读这几篇文章:
- Android MVP模式 简单易懂的介绍方式
- RxJava快速入门
- 给 Android 开发者的 RxJava 详解
了解 Retrofit、okHttp,直接看Squre官网
- Retrofit
- OkHttp
之所以说解耦,很大程度是 MVP、Rxjava、Retrofit 在 java工程 就能使用,本身不依赖Android SDK。这一点对Android单元测试至关重要(会在下一篇《Mockito单元测试》介绍)。
MVP & RxJava在2015年已经很火了,加上2016年发布正式版的 OkHttp3.0 & Retrofit2.0 火上浇油,全世界简直炸开了锅,Android开发有了质的飞跃(代码层面)。
国内Android开发者逐渐成熟,翻墙越来越方便,国外的技术在国内使用顺理成章。目前,国内状况是,Android开发者不缺,缺的是大量Android中级开发者。因此,学会使用MVP、RxJava、Retrofit、Mockito单元测试势在必行。
逆水行走,不进则退。
请求User数据,并在显示
User
bean:
public class User { public int uid; public String name;}
UserView
,网络加载完User数据,回调onUserLoaded(user)
:
public interface UserView { void onUserLoaded(User user);}
UserService
,Retrofit
代理的请求接口:
public interface UserService { @GET("user/{uid}.json") Observable<User> loadUser(@Path("uid") int uid);}
与View (Activity)
交互的UserPresenter
接口、以及实现UserPresenterImpl
:
public interface UserPresenter { void loadUser(int uid);}
public class UserPresenterImpl implements UserPresenter { UserService userService; UserView userView; public UserPresenterImpl(UserView userView) { this.userView = userView; userService = new Retrofit.Builder().baseUrl("http://**.com/") .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build() .create(UserService.class); } @Override public void loadUser(int uid) { // 异步网络请求User数据,并在onNext(user)返回 userService.loadUser(uid) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<User>() { @Override public void onNext(User user) { userView.onUserLoaded(user); } ...... }); }}
MainActivity
:
public class MainActivity extends Activity implements UserView { UserPresenter userPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); userPresenter = new UserPresenterImpl(this); } @Override public void onUserLoaded(User user) { textView.setText(user.toString()); }}
为何解耦?
前言中提及,RxJava
与Retrofit
是不依赖Android SDK
的独立的第三方库,MVP模式通过接口编程,把依赖Android SDK
的View层(Activity)
与Presenter、Model
隔离。这里说的Model
是指retrofit
和代理的Service
网络请求接口,对于Model
层依赖与Android SDK
的DAO、sqlite
,之后会讨论。
当P层、网络M层不依赖Android SDK
,我们就可以用JUnit
写单元测试,并直接运行在JVM
上了。
JUnit4+Mockito单元测试
很多Android开发的同学,不了解单元测试。对于不是测试专业出身、又没技术大牛调教过的程序猿,缺乏单元测试知识,比比皆是。要了解单元测试,推荐阅读:
美团点评技术团队 的《Android单元测试研究与实践》
邹小创 《Android单元测试(二):再来谈谈为什么》
老实说,我也是2016年初,才真正接触单元测试。2016年3月才正式对项目写单元测试。写了一个多月,越来越意识到单元测试的重要性。单元测试达到的目的,总结成两点:
- 快速开发
- 提高代码质量
你没看错,确实是“快速”!
对于需求“请求User,并显示”,噼里啪啦写完Presenter、Service、Activity
,需要 编译、运行 在真机or模拟器才能debug,如果写错了,修改代码后,还要编译、运行在真机....还写错,修改、编译、运行.... 小型项目怎么也要40s~1分钟吧,还要花几分钟时间手动操作界面...用Android Sutdio debug或Log.....这个过程太漫长了!!
如果你学会写Junit
单元测试,可以直接对单个Presenter、Service
编译运行,不需要关心是否受到其他类的代码or网络环境、服务器是否正常的影响。运行一下就几秒钟,Junit
和Mockito
的错误提示,还让你快速定位问题。
瞎逼逼了那么久,该上代码。
Presenter单元测试
打开UserPresenter
,对着类名 右键 -> Go To -> Test
创建OK之后,你会得到UserPresenterTest.java
:
public class UserPresenterTest { @Before public void setUp() throws Exception { } @Test public void testLoadUser() throws Exception { }}
要对UserPresenterImpl
进行单元测试,还需要做一点点改进:
public class UserPresenterImpl implements UserPresenter { UserService userService; UserView userView; // 让外部传入UserService & UserView public UserPresenterImpl(UserService userService, UserView userView) { this.userService = userService; this.userView = userView; } ...}
import static org.mockito.Mockito.mock;public class UserPresenterTest { UserPresenter userPresenter; UserView userView; UserService userService; @Before public void setUp() throws Exception { RxUnitTestTools.openRxTools(); // 生成mock对象 userView = mock(UserView.class); userService = mock(UserService.class); userPresenter = new UserPresenterImpl(userService, userView); }}
注意,这里import static org.mockito.Mockito.mock
,静态引用org.mockito.Mockito
的mock()
静态方法。我们不用自己敲这句import
,通过代码补全提示就可以自动生成了,如图:
这里有行RxUnitTestTools.openRxTools()
到底是什么?
public class RxUnitTestTools { private static boolean isInitRxTools = false; /** * 把异步变成同步,方便测试 */ 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(); } }; RxAndroidPlugins.getInstance().registerSchedulersHook(rxAndroidSchedulersHook); RxJavaPlugins.getInstance().registerSchedulersHook(rxJavaSchedulersHook); }}
这个类是让RxJava&RxAndroid
的Schedulers.io()
和AndroidSchedulers.mainThread()
转换成Schedulers.immediate()
,从而让Obserable
从异步变同步。
然后,写testLoadUser()
:
@Test public void testLoadUser() throws Exception { User user = new User(); user.uid = 1; user.name = "kkmike999"; when(userService.loadUser(anyInt())).thenReturn(Observable.just(user)); userPresenter.loadUser(1); ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class); verify(userService).loadUser(1); verify(userView).onUserLoaded(captor.capture()); User result = captor.getValue(); // 捕获的User Assert.assertEquals(result.uid, 1); Assert.assertEquals(result.name, "kkmike999"); }
让我解析一下:
when...thenReturn...
when(userService.loadUser(anyInt())).thenReturn(Observable.just(user));
当调用userService.loadUser(...)
,参数为任意int,返回Observable.just(user)
对象。
verify
verify(userService).loadUser(1);
,验证 userService.loadUser(...)
是否被调用,并校验传入参数uid==1
。
这一步很重要,这个loadUser(uid)
参数比较少,当方法参数多时(例如loadXXX(int,int,int,int...String,String....)
),特别容易搞错。当后端接口修改了,service
相应也要修改,这时多参数的方法很容易出问题。
verify(userView).onUserLoaded(captor.capture());
,验证userView.onUserLoaded(...)
是否被调用,并捕获传入的user参数
ArgumentCaptor
顾名思义参数捕获器,就是捕获传入参数。当userService.loadUser()
执行完并返回Observable<User>
,在onNext(user)
回调User
传给userView.onUserLoaded(...)
,但我们不确定回调的user
是否正确。因此我们需要捕获user
参数,并校验其正确性。
如果参数是List<T>
类型,ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class)
即可,不需要写List泛型参数。
assertEquals
这个不用说了吧.....
Service(Model层)单元测试
测试这一层的目的,是验证从服务器返回的数据,是否解析成正确的对象。单元测试时,应该模拟服务器返回json数据。由于UserService
被Retrofit
代理过,所以单元测试需要一点技巧。
写一个MockRetrofitHelper
:
public class MockRetrofitHelper { public <T> T create(Class<T> clazz) { OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new MockInterceptor()) .build(); Retrofit retrofit = new Retrofit.Builder().baseUrl("http://api.***.com") .client(client) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build(); return retrofit.create(clazz); } private String path; public void setPath(String path) { this.path = path; } private class MockInterceptor implements Interceptor{ @Override public Response intercept(Chain chain) throws IOException { // 模拟网络数据 String content = AssestsReader.readFile(path); ResponseBody body = ResponseBody.create(MediaType.parse("application/x-www-form-urlencoded"), content); Response response = new Response.Builder().request(chain.request()) .protocol(Protocol.HTTP_1_1) .code(200) .body(body) .build(); return response; } }}
解释一下,MockInterceptor
的职责,读取本地数据,并直接返回。因此,OkHttpClient
并没有真正请求网络数据,而是用了本地数据。
对OkHttp Interceptor
不熟悉的同学,参考:
学习OkHttp --Interceptors
然后,写UserServiceTest
单元测试:
public class UserServiceTest { UserService userService; MockRetrofitHelper retrofit; @Before public void setUp() throws Exception { retrofit = new MockRetrofitHelper(); userService = retrofit.create(UserService.class); } @Test public void testLoadUser() throws Exception { retrofit.setPath(".../User.json"); TestSubscriber<User> testSubscriber = new TestSubscriber<>(); userService.loadUser(1) .toBlocking() .subscribe(testSubscriber); User user = testSubscriber.getOnNextEvents() .get(0); Assert.assertEquals(user.uid, 1); Assert.assertEquals(user.name, "kkmike999"); }}
当Observalbe<User>
调用subscribe(...)
时,TestSubscriber
会捕获onNext(user)
参数,并放进List<User>
事件队列。我们通过testSubscriber.getOnNextEvents()
获取事件队列,从这个队列获取User
,并验证正确性。
小结
文章已经到尾声。对于mockito、retrofit、okhttp intercepor熟悉的你,本文并没有太多难点。
为新功能写代码时,应该先写Presenter
或者Service
,不急着运行,再写PresenterTest
和ServiceTest
,在JVM上验证代码是否正确。写完单元测试后,再让Activity
调用Presenter
。
对Activity、Service
单元测试感兴趣的同学,不妨了解Robolectric(发音比较坑爹,重音在l
而不是R
)。它可以让你在JVM
运行Activity单元测试,比真机调试快多了。
各位同学,千万不要觉得 很麻烦、项目很赶 就不写单元测试,这些都是业界大牛的经验之谈,有益无害!当你发现代码无法单元测试,证明代码本身有问题,应该去改进,而不是放弃单元测试。
原文链接:http://www.jianshu.com/p/cdfeb6c3d099
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
- (MVP+RxJava+Retrofit)解耦+Mockito单元测试 经验分享
- Android-使用Mockito、Robolectric和RxJava及Retrofit进行单元测试
- 分享一个基于MVP+Retrofit+RxJava+MaterialDesign的App
- 分享一个基于MVP+Retrofit+RxJava+MaterialDesign的App
- MVP+Dagger2+Retrofit+Rxjava
- Retrofit实践(MVP+RxJava)
- Rxjava+MVP+Retrofit强强联合
- MVP、Retrofit、RxJava、RxBus
- Rxjava+retrofit+mvp整合
- MVP+retrofit+rxjava
- Android MVP +Retrofit+RxJava
- 最新Retrofit + RxJava + MVP
- MVP+Retrofit+Rxjava实战
- RxJava+Retrofit+MVP+Dagger2
- MVP+Retrofit+RxJava
- Retrofit+RxJava+Fresco+MVP
- MVP+Retrofit+RxJava
- MVP+RxJava+Retrofit
- [HDU 2066] 多个最短路比较大小
- Tomcat服务器+MySQL数据库+MyBatis持久层框架的简单使用
- 用NginX+keepalived实现高可用的负载均衡
- uefi制作启动盘
- 文件的分割与合并
- (MVP+RxJava+Retrofit)解耦+Mockito单元测试 经验分享
- springMVC搭建总结
- Android自定义视图动画(一)
- Opencv3.1使用教程(一)ubuntu 14.04 安装Opencv3.1.0 (包含opencv_contrib模块)
- UVA - 11100 The Trip, 2007
- spark 自带的例子在eclipse下运行的方法
- AndroidStudio 快速集成fastjson和注解
- 2016"百度之星" - 测试赛 1002 列变位法解密 (模拟)
- 取数游戏 贪心