当前位置: 首页 > 科技观察

Android单元测试——几个重要问题

时间:2023-03-13 20:34:37 科技观察

原文链接:http://www.jianshu.com/p/f5d197a4d83a一个月没写前言了。由于9月国庆出游计划,国庆前后旅游了14天,所以没时间写,哈哈。言归正传,上一篇《Android单元测试 - 如何开始?》介绍了几个单元测试框架,Junit&Mockito的基本使用,依赖隔离&Mock的概念,本文主要回答单元测试中的几个重要问题。在单元测试交流微信群里,很多新人都会有几个类似的问题。我们几个老鸟一连答了一遍(厚颜无耻算自己^_^),作者有点不耐烦了,然后就等其他同学回答了。。。其实大家提的问题都在归根到底是“依赖问题”,jvm依赖还是android依赖?使用native方法报错怎么办?如何解决静态方法?于是,笔者决定专门写一篇文章来解释这些问题。如何解决安卓依赖?隔离Native方法,解决内部new对象静态方法RxJava异步转同步1.如何解决Android依赖?小白:“Presenter中使用了TextUtils,运行junit报'java.lang.RuntimeException:MethodisEmptyinandroid.text.TextUtilsnotmocked'错误...要用robolectric吗?”别着急,现在还不是robolectric登场的时候!由于junit运行在jvm上,而jdk没有android源码,所以TextUtils在androidsdk中运行junit时不能引用该类。既然jdk不存在,那我们自己添加吧!在test/java目录下,创建android.text.TextUtils类packageandroid.text;publicclassTextUtils{publicstaticbooleanisEmpty(CharSequencestr){if(str==null||str.equals("")){returntrue;}returnfalse;}}关键是要有一个具有相同包名、相同名称和相同方法名的TextUtils。注意不要在main/java下创建,否则会提示Duplicateclassfoundinthefile....单元测试正常运行:原理很简单,jvm运行的时候会找到android.text。TextUtils类,然后找到isEmpty方法执行。学过java反射的同学都知道,只要知道包名和类名就可以得到Class,知道类的某个方法名就可以得到Method并执行。jvm也是类似的机制。只要我们给一个和androidsdk同包名和类名的类,写一个同方法名&参数&返回值的方法,jvm就可以编译执行。(提示:androidView也可以这样做。)2.隔离Native方法小白:“我用的是native方法,junit运行失败,robolectric不支持加载so文件,怎么办?”模型类:packagecom.测试单元;publicclassModel{publicnativebooleannativeMethod();}单元测试:publicclassModelTest{Modelmodel;@BeforepublicvoidsetUp()throwsException{model=newModel();}@TestpublicvoidtestNativeMethod()throwsException{Assert.assertTrue(model.nativeMethod());}}运行ModelTest...Errorjava.lang.UnsatisfiedLinkError:com.test.unit.Model.nativeMethod()这里使用了上篇文章《Android单元测试 - 如何开始?》讲的“依赖隔离”!改进单元测试:publicclassModelTest{Modelmodel;@BeforepublicvoidsetUp()throwsException{model=mock(Model.class);}@TestpublicvoidtestNativeMethod()throwsException{when(model.nativeMethod()).thenReturn(true);Assert.assertTrue(model.Model.java的全称是com.test.unit.Model.java;2).调用native方法nativeMethod()后,jvm会在C++层com_test_unit_Model.cpp中找到com_test_unit_Model_nativeMethod()方法并调用。在APP运行过程中,我们会将cpp编译成so文件,然后将APP加载到dalvik虚拟机中。但是在单元测试中,并没有加载对应的so文件,也没有编译cpp!大牛在单元测试的时候可能会尝试加载so文件,但是完全没有必要,也不符合单元测试的原则。因此,我们可以直接使用Mockito框架来mocknative方法。事实上,不仅native方法需要mock,很多依赖的方法和类也需要mock。下面将讨论更常用的场景。(参考《Android JNI原理分析》)3.解决内部新建对象小白:“我在Presenter中新建了一个Model,这个Model有很多依赖,可以做sql操作等等.....Presenter依赖在Model上返回结果,导致Presenter无法进行单元测试!请多多指教!”小白C的例子:Model:publicclassModel{publicbooleangetBoolean(){booleanbo=.......//一堆依赖,代码很复杂returnbo;}}Presenter:publicclassPresenter{Modelmodel;publicPresenter(){model=newModel();}publicbooleangetBoolean(){returnmodel.getBoolean());}}错误的单元测试:publicclassPresenterTest{Presenterpresenter;@BeforepublicvoidsetUp()throwsException{presenter=newPresenter();}@TestpublicvoidtestGetBoolean()throwsException{Assert.assertTrue(presenter.getBoolean());}}还是那句话:依赖隔离。我们隔离Model依赖项,即模拟Model对象,而不是newModel()。寻找上面的PresenterTest问题:PresenterTest完全不知道Model的存在,也就是说它不能mockModel。那么,我们就想办法把mockModel传递给Presenter——在Presenter的构造函数中传递参数!改进Presenter:publicclassPresenter{Modelmodel;publicPresenter(Modelmodel){this.model=model;}publicbooleangetBoolean(){returnmodel.getBoolean();}}正确的单元测试:publicclassPresenterTest{Modelmodel;Presenterpresenter;@BeforepublicvoidsetUp()throwsException{model=mock(Model.class);//mockModel对象presenter=newPresenter(model);}@TestpublicvoidtestGetBoolean()throwsException{when(model.getBoolean)()).thenReturn(true);Assert.assertTrue(presenter.getBoolean());}}就是这样。如果你觉得在Activity中直接使用默认的Presenter构造函数而不是构造函数newModel()更方便,那就保留默认构造函数。当然,使用dagger2时没有多个构造函数,都是构造并传递参数。4.静态方法小白:“大神,我在Presenter中使用静态方法....”作者:“好吧,我知道你想说什么了。”演示者:publicclassPresenter{publicStringgetSignParams(intuid,Stringname,Stringtoken){returnSignatureUtils。sign(uid,name,token);}}解决方法和上面的【解决内部新建对象】类似,核心思想是依赖隔离。1).将sign(...)更改为非静态方法;2).使用SignatureUtils作为成员变量;3).将构造方法传入SignatureUtils;4).单元测试时,将模拟SignatureUtils传递给Presenter。改进的Presenter:publicclassPresenter{SignatureUtilsmSignUtils;publicPresenter(SignatureUtilssignatureUtils){this.mSignUtils=signatureUtils;}publicStringgetSignParams(intuid,Stringname,Stringtoken){returnmSignUtils.sign(uid,name,token);}}5.RxJava异步转同步小白:《大神...》作者:“身为老师,我已经算过,预料到你会遭遇这场劫难。》小白:(传说从入门到出家?)publicclassRxPresenter{publicvoidtestRxJava(Stringmsg){Observable.just(msg).subscribeOn(Schedulers.io()).delay(1,TimeUnit.SECONDS)//延迟1第二//.observeOn(AndroidSchedulers.mainThread()).subscribe(newAction1(){@Overridepublicvoidcall(Stringmsg){System.out.println(msg);}});}}单元测试publicclassRxPresenterTest{RxPresenterrxPresenter;@BeforepublicvoidsetUp()throwsException{rxPresenter=newRxPresenter();}@TestpublicvoidtestTestRxJava()throwsException{rxPresenter.testRxJava("test");}}运行RxPresenterTest:你会发现没有输出“test”,为什么?因为在testRxJava中,Obserable.subscribeOn(Schedulers.io())将线程切换到io线程,延迟1秒,此时testTestRxJava()单元测试已经在当前线程运行完毕,我试过了,即使delay(1,TimeUnit.SECONDS)去掉了,还是不行会输出'test'可以看到作者注释掉了.observeOn(AndroidSchedulers.mainThread()),我们添加那段代码,然后运行testTestRxJava(),会报java.lang.RuntimeException:MethodgetMainLooperinandroid.os.Loopernotmocked.:这是因为jdk没有类android.os.Looper和相关的依赖项。要解决以上两个问题,我们只需要将Schedulers.io()&AndroidSchedulers.mainThread()切换为Schedulers.immediate()即可。RxJava开发组已经为大家想好了,提供了两个hook操作类,RxJavaHooks和RxAndroidPlugins。新建RxTools:publicclassRxTools{publicstaticvoidasyncToSync(){Func1schedulerFunc=newFunc1(){@OverridepublicSchedulercall(Schedulerscheduler){returnSchedulers.immediate();}};RxAndroidSchedulersHookrxAndroidSchedulersHook=newRxAndroidSchedulersHook(){@OverridepublicSchedulergetMainThreadScheduler(){returnSchedulers.immediate();}};RxJavaHooks.reset();RxJavaHooks.setOnIOScheduler(schedulerFunc);RxJavaHooks.setOnComputationScheduler(schedulerFunc);RxAndroidPlugins.getInstance().reset();RxAndroidPlugins.getInstance().registerHookerSchedulersHed();}}添加RxTools.asyncToSync();:publicclassRxPresenterTest{RxPresenterrxPresenter;@BeforepublicvoidsetUp()throwsException{rxPresenter=newRxPresenter();RxTools.asyncToSync();}...}在RxPresenterTest.setUp()()中再次运行testTestRxJava:终于输出“test”了,谢天谢地!(应该是对作者的打赏^_^)各位读者有没有发现RxTools.asyncToSync()里面加了一句RxJavaHooks.setOnComputationScheduler(schedulerFunc),这意味着将计算线程切换到即时线程。作者发现Obserablewithdelay只加RxJavaHooks.setOnIOScheduler(schedulerFunc)还是失败了,于是我切换了计算线程,就ok了。还有RxJavaHooks.reset()和RxAndroidPlugins.getInstance().reset()。作者发现当运行大量的单元测试时,有些会失败,但是单独运行失败的单元测试又会通过。百思不得其解后,加了那两句……就这样!(关于RxJavaHooks和RxAndroidPlugins的使用,在很久以前的文章《(MVP+RxJava+Retrofit)解耦+Mockito单元测试 经验分享》中已经提到了)总结作者:《小白同学,现在踩到的坑填好了吗?》小白:“方丈,啊不,我的天,上面的问题已经解决了,但是还有其他的问题。”作者:“不挖坑怎么填,以后再给大家讲讲单元测试的其他奥秘。”小白:“……”本文详细介绍了单元测试中几个重要问题的解决方法。读者不难发现,作者一直强调依赖隔离和依赖隔离,依赖隔离,这个概念在单元测试中非常重要。还不明白这个概念的同学,多看几遍《Android单元测试 - 如何开始?》(还有厚颜无耻的广告),同时在实践中不断复习这个概念。只要解决了这些问题,Presenter单元测试就不难了。还有本文没有提到的sqlite和SharedPreferences单元测试,会在后面的文章中介绍给读者。感谢各位读者一直以来对作者的支持,请点赞转发,好人终将平安。关于作者我是键盘侠。生活在广州,在一家创业公司工作,是一个猥琐的文艺码农。喜欢科学、历史,玩投资,偶尔一个人旅游。希望成为独立工程师。