前言
已经一个月没写文章了,由于9月份在plan国庆旅行计划,国庆前前后后去了14天旅行,所以没时间写,哈哈。
言归正传,上一篇文章《Android单元测试 - 如何开始?》介绍了几款单元测试框架、Junit & Mockito基本用法、依赖隔离 & Mock概念,本篇主要解答单元测试中几个重要问题。
在单元测试交流微信群,很多新进来的小伙伴,都会几个大同小异的问题。我们几个老鸟们答完一次又一次(厚颜无耻地把自己算上_),笔者是有点不耐烦了,后来就等其他同学回答他们.....其实大家提的问题,归根到底就是“依赖问题”,jvm依赖还是android依赖?用到native方法报错怎么办?静态方法怎么解决?
于是呢,笔者决定专门写一篇文章,来讲解这几个问题。
- 如何解决Android依赖?
- 隔离Native方法
- 解决内部new对象
- 静态方法
- RxJava异步转同步
1.如何解决Android依赖?
小白:“Presenter中用到TextUtils,运行junit时报'java.lang.RuntimeException: Method isEmpty in android.text.TextUtils not mocked'错误... 是不是要用robolectric?”
别急,还未到robolectric出场的时候呢!
由于junit运行在jvm上,而jdk没有android源码,所以TextUtils这些在android sdk中的类,运行junit时就引用不上了。既然jdk没有,我们就自己加呗!
在
test/java
目录下,创建android.text.TextUtils
类
package android.text;
public class TextUtils {
public static boolean isEmpty(CharSequence str) {
if (str == null || str.equals("")) {
return true;
}
return false;
}
}
关键是要个TextUtils
同包名、同类名、同方法名。注意不是在main/java
下创建,不然会提示Duplicate class found in the file...
。单元测试运行妥妥的:
原理很简单,jvm运行时会找android.text.TextUtils
类,然后找isEmpty
方法执行。学过java反射的同学都知道,只要知道包名类名,就可以拿到Class
,知道该类某方法名,就可以获取Method
并执行。jvm也是类似的机制,只要我们给一个包名类名与android sdk相同的类,写上方法名&参数&返回值相同的方法,jvm就能编译并执行。
(提示:android的View之类也能这么搞噢)
2.隔离Native方法
小白:“我用到native方法,junit运行失败,robolectric也不支持加载so文件,怎么办?”
Model类:
package com.test.unit;
public class Model {
public native boolean nativeMethod();
}
单元测试:
public class ModelTest {
Model model;
@Before
public void setUp() throws Exception {
model = new Model();
}
@Test
public void testNativeMethod() throws Exception {
Assert.assertTrue(model.nativeMethod());
}
}
run ModelTest
... 报错java.lang.UnsatisfiedLinkError: com.test.unit.Model.nativeMethod()
上篇文章《Android单元测试 - 如何开始?》讲述的“依赖隔离”,这里要用到了!
改进单元测试:
public class ModelTest {
Model model;
@Before
public void setUp() throws Exception {
model = mock(Model.class);
}
@Test
public void testNativeMethod() throws Exception {
when(model.nativeMethod()).thenReturn(true);
Assert.assertTrue(model.nativeMethod());
}
}
再run
一下,pass了:
这里稍微讲讲java查找native方法的过程:
1.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框架mock native方法就行啦。实际上,不仅仅是native方法需要mock,很多依赖的方法、类都要mock,下面会讲到更常用的场景。
(参考《Android JNI原理分析》)
3.解决内部new对象
小白:“我在Presenter里new Model,Model依赖比较多,会做sql操作,等等.....Presenter依赖Model返回结果,导致Presenter没法单元测试啦!求大神指点!”
小白C的例子:
Model:
public class Model {
public boolean getBoolean() {
boolean bo = ....... // 一堆依赖,代码很复杂
return bo;
}
}
Presenter:
public class Presenter {
Model model;
public Presenter() {
model = new Model();
}
public boolean getBoolean() {
return model.getBoolean());
}
}
错误的单元测试:
public class PresenterTest {
Presenter presenter;
@Before
public void setUp() throws Exception {
presenter = new Presenter();
}
@Test
public void testGetBoolean() throws Exception {
Assert.assertTrue(presenter.getBoolean());
}
}
还是那句话:依赖隔离。我们隔离Model
依赖,即mock Model对象
,而不是new Model()
。
找找以上PresenterTest
的问题吧:PresenterTest
完全不知道Model
的存在,意思是无法mock Model
。那么,我们就想办法把mock Model
传给Presenter
——在Presenter
构造函数传参!
改进Presenter
:
public class Presenter {
Model model;
public Presenter(Model model) {
this.model = model;
}
public boolean getBoolean() {
return model.getBoolean();
}
}
正确的单元测试:
public class PresenterTest {
Model model;
Presenter presenter;
@Before
public void setUp() throws Exception {
model = mock(Model.class);// mock Model对象
presenter = new Presenter(model);
}
@Test
public void testGetBoolean() throws Exception {
when(model.getBoolean()).thenReturn(true);
Assert.assertTrue(presenter.getBoolean());
}
}
事情就这么解决了。如果你觉得在Activity直接用默认Presenter
构造函数,在构造函数new Model()
比较方便,那就保留默认构造函数呗。当然使用dagger2就不存在多个构造函数了,都是构造传参。
4.静态方法
小白:“大神,我在Presenter用到静态方法....”
笔者:“行了,知道你要说什么。”
Presenter:
public class Presenter {
public String getSignParams(int uid, String name, String token) {
return SignatureUtils.sign(uid, name, token);
}
}
解决方法跟上面【解决内部new对象】大同小异,核心思想还是依赖隔离。
方案1,改成非静态
1.把sign(...)
改成非静态方法;
2.把SignatureUtils
作为成员变量;
3.构造方法传入SignatureUtils
;
4.单元测试时,把mock SignatureUtils
传给Presenter
。
改进后Presenter
:
public class Presenter {
SignatureUtils mSignUtils;
public Presenter(SignatureUtils signatureUtils) {
this.mSignUtils= signatureUtils;
}
public String getSignParams(int uid, String name, String token) {
return mSignUtils.sign(uid, name, token);
}
}
方案2,Spy
(2017.4.20补充)
public class PresenterTest {
Presenter presenter;
@Before
public void setUp() throws Exception {
presenter = spy(new Presenter(...));
}
@Test
public void testXXX() throws Exception {
// 注意不是用when().thenReturn()
doReturn("...").when(presenter).getSignParams(...);
...
}
}
关于Spy用法,请自行脑补_.
5.RxJava异步转同步
小白:“大神...”
笔者:“为师掐指一算,料汝会遇此劫难。”
小白:(传说中从入门到出家?)
public class RxPresenter {
public void testRxJava(String msg) {
Observable.just(msg)
.subscribeOn(Schedulers.io())
.delay(1, TimeUnit.SECONDS) // 延时1秒
// .observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1() {
@Override
public void call(String msg) {
System.out.println(msg);
}
});
}
}
单元测试
public class RxPresenterTest {
RxPresenter rxPresenter;
@Before
public void setUp() throws Exception {
rxPresenter = new RxPresenter();
}
@Test
public void testTestRxJava() throws Exception {
rxPresenter.testRxJava("test");
}
}
运行RxPresenterTest
:
你会发现没有输出"test",为什么呢?
由于testRxJava
里面,Obserable.subscribeOn(Schedulers.io())
把线程切换到io线程,并且delay
了1秒,而testTestRxJava()
单元测试早已在当前线程跑完了。笔者试过,即使去掉delay(1, TimeUnit.SECONDS)
,还是不会输出‘test’
。
可以看到笔者把.observeOn(AndroidSchedulers.mainThread())
注释掉了,我们把那句代码加上,再跑一下testTestRxJava()
,会报java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked.
:
这是由于jdk没有android.os.Looper这个类及相关依赖。
解决以上两个问题,我们只要把Schedulers.io()
&AndroidSchedulers.mainThread()
切换为Schedulers.immediate()
就可以了。RxJava开发团队已经为大家想好了,提供了RxJavaHooks
和RxAndroidPlugins
两个hook操作的类。
新建RxTools
:
public class RxTools {
public static void asyncToSync() {
Func1 schedulerFunc = new Func1() {
@Override
public Scheduler call(Scheduler scheduler) {
return Schedulers.immediate();
}
};
RxAndroidSchedulersHook rxAndroidSchedulersHook = new RxAndroidSchedulersHook() {
@Override
public Scheduler getMainThreadScheduler() {
return Schedulers.immediate();
}
};
RxJavaHooks.reset();
RxJavaHooks.setOnIOScheduler(schedulerFunc);
RxJavaHooks.setOnComputationScheduler(schedulerFunc);
RxAndroidPlugins.getInstance().reset();
RxAndroidPlugins.getInstance().registerSchedulersHook(rxAndroidSchedulersHook);
}
}
在RxPresenterTest.setUp()
加一句RxTools.asyncToSync();
:
public class RxPresenterTest {
RxPresenter rxPresenter;
@Before
public void setUp() throws Exception {
rxPresenter = new RxPresenter();
RxTools.asyncToSync();
}
...
}
再跑一次testTestRxJava()
:
总算输出"test",感谢上帝啊!(应该打赏下笔者吧_)
读者有没发现RxTools.asyncToSync()
多加了一句RxJavaHooks.setOnComputationScheduler(schedulerFunc)
,意思将computation线程切换为immediate线程。笔者发现,仅仅添加RxJavaHooks.setOnIOScheduler(schedulerFunc)
,对于有delay
的Obserable
还是未通过,于是顺手把computation线程也切换了,于是就可以了。
还有RxJavaHooks.reset()
和RxAndroidPlugins.getInstance().reset()
,笔者发现,当运行大量单元测试时,有些会失败,但单独运行失败的单元测试,又通过了。百思不得其解后,添加了那两句.....可以了!
(关于RxJavaHooks
和RxAndroidPlugins
的使用,在很久前的文章 《(MVP+RxJava+Retrofit)解耦+Mockito单元测试 经验分享》已经提及过)
小结
笔者:“小白同学,现在你踩过的坑,填好未?”
小白:“方丈,啊不,大神,上面几个问题是解决了,不过还有其他问题。”
笔者:“不挖坑,怎么填坑呢?以后再给你讲讲其他单元测试的玄机。”
小白:“......”
本文详述了几个单元测试重要问题的解决方法,读者不难发现,笔者一直强调 依赖隔离、依赖隔离、依赖隔离,这个概念在单元测试中相当重要。还搞不懂这个概念的同学,看多几次《Android单元测试 - 如何开始?》(又厚颜无耻地广告),同时在实践中不断回顾这个理念。
只要解决好这几个问题,Presenter单元测试就不难了。还有本文未提及的sqlite、SharedPreferences单元测试、在后面的文章会给读者介绍下。
感谢读者对笔者一直以来的支持,麻烦点赞&随手转发,好人一世平安。
关于作者
我是键盘男。
在广州生活,在互联网公司上班,猥琐文艺码农。喜欢科学、历史,玩玩投资,偶尔独自旅行。