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

移动开发 Android
上一篇文章《Android单元测试 - 如何开始?》介绍了几款单元测试框架、Junit & Mockito基本用法、依赖隔离 & Mock概念,本篇主要解答单元测试中几个重要问题。

[[173976]]

原文链接:http://www.jianshu.com/p/f5d197a4d83a

前言

已经一个月没写文章了,由于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类

  1. package android.text; 
  2.  
  3. public class TextUtils { 
  4.  
  5.     public static boolean isEmpty(CharSequence str) { 
  6.         if (str == null || str.equals("")) { 
  7.             return true
  8.         } 
  9.         return false
  10.     } 

关键是要个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类:

  1. package com.test.unit; 
  2.  
  3. public class Model { 
  4.     public native boolean nativeMethod(); 
  5.  

单元测试:

  1. public class ModelTest { 
  2.  
  3.     Model model; 
  4.  
  5.     @Before 
  6.     public void setUp() throws Exception { 
  7.         model = new Model(); 
  8.     } 
  9.  
  10.     @Test 
  11.     public void testNativeMethod() throws Exception { 
  12.         Assert.assertTrue(model.nativeMethod()); 
  13.     } 
  14.  

run ModelTest... 报错java.lang.UnsatisfiedLinkError: com.test.unit.Model.nativeMethod()

 

上篇文章《Android单元测试 - 如何开始?》讲述的“依赖隔离”,这里要用到了!

改进单元测试:

  1. public class ModelTest { 
  2.  
  3.     Model model; 
  4.  
  5.     @Before 
  6.     public void setUp() throws Exception { 
  7.         model = mock(Model.class); 
  8.     } 
  9.  
  10.     @Test 
  11.     public void testNativeMethod() throws Exception { 
  12.         when(model.nativeMethod()).thenReturn(true); 
  13.  
  14.         Assert.assertTrue(model.nativeMethod()); 
  15.     } 

 再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:

  1. public class Model { 
  2.     public boolean getBoolean() { 
  3.         boolean bo = ....... // 一堆依赖,代码很复杂 
  4.         return bo; 
  5.     } 
  6.  

Presenter:

  1. public class Presenter { 
  2.  
  3.     Model model; 
  4.  
  5.     public Presenter() { 
  6.         model = new Model(); 
  7.     } 
  8.  
  9.     public boolean getBoolean() { 
  10.         return model.getBoolean()); 
  11.     } 
  12.  

错误的单元测试:

  1. public class PresenterTest { 
  2.  
  3.     Presenter presenter; 
  4.  
  5.     @Before 
  6.     public void setUp() throws Exception { 
  7.         presenter = new Presenter(); 
  8.     } 
  9.  
  10.     @Test 
  11.     public void testGetBoolean() throws Exception { 
  12.         Assert.assertTrue(presenter.getBoolean()); 
  13.     } 
  14.  

还是那句话:依赖隔离。我们隔离Model依赖,即mock Model对象,而不是new Model()。

找找以上PresenterTest的问题吧:PresenterTest完全不知道Model的存在,意思是无法mock Model。那么,我们就想办法把mock Model传给Presenter——在Presenter构造函数传参!

改进Presenter:

  1. public class Presenter { 
  2.  
  3.     Model model; 
  4.  
  5.     public Presenter(Model model) { 
  6.         this.model = model; 
  7.     } 
  8.  
  9.     public boolean getBoolean() { 
  10.         return model.getBoolean(); 
  11.     } 
  12.  

正确的单元测试:

  1. public class PresenterTest { 
  2.     Model     model; 
  3.     Presenter presenter; 
  4.  
  5.     @Before 
  6.     public void setUp() throws Exception { 
  7.         model = mock(Model.class);// mock Model对象 
  8.  
  9.         presenter = new Presenter(model); 
  10.     } 
  11.  
  12.     @Test 
  13.     public void testGetBoolean() throws Exception { 
  14.         when(model.getBoolean()).thenReturn(true); 
  15.  
  16.         Assert.assertTrue(presenter.getBoolean()); 
  17.     } 
  18.  

事情就这么解决了。如果你觉得在Activity直接用默认Presenter构造函数,在构造函数new Model()比较方便,那就保留默认构造函数呗。当然使用dagger2就不存在多个构造函数了,都是构造传参。

4.静态方法

小白:“大神,我在Presenter用到静态方法....”笔者:“行了,知道你要说什么。”

Presenter:

  1. public class Presenter { 
  2.  
  3.     public String getSignParams(int uid, String name, String token) { 
  4.         return SignatureUtils.sign(uid, name, token); 
  5.     } 
  6.  

解决方法跟上面【解决内部new对象】大同小异,核心思想还是依赖隔离。

1).把sign(...)改成非静态方法;

2).把SignatureUtils作为成员变量;

3).构造方法传入SignatureUtils;

4).单元测试时,把mock SignatureUtils传给Presenter。

改进后Presenter:

  1. public class Presenter { 
  2.     SignatureUtils mSignUtils; 
  3.  
  4.     public Presenter(SignatureUtils signatureUtils) { 
  5.         this.mSignUtils= signatureUtils; 
  6.     } 
  7.  
  8.     public String getSignParams(int uid, String name, String token) { 
  9.         return mSignUtils.sign(uid, name, token); 
  10.     } 
  11.  

5.RxJava异步转同步

小白:“大神...”

笔者:“为师掐指一算,料汝会遇此劫难。”

小白:(传说中从入门到出家?) 

  1. public class RxPresenter { 
  2.  
  3.     public void testRxJava(String msg) { 
  4.         Observable.just(msg) 
  5.                   .subscribeOn(Schedulers.io()) 
  6.                   .delay(1, TimeUnit.SECONDS) // 延时1秒 
  7. //                  .observeOn(AndroidSchedulers.mainThread()) 
  8.                   .subscribe(new Action1<String>() { 
  9.                       @Override 
  10.                       public void call(String msg) { 
  11.                           System.out.println(msg); 
  12.                       } 
  13.                   }); 
  14.     } 
  15.  

单元测试

  1. public class RxPresenterTest { 
  2.  
  3.     RxPresenter rxPresenter; 
  4.  
  5.     @Before 
  6.     public void setUp() throws Exception { 
  7.         rxPresenter = new RxPresenter(); 
  8.     } 
  9.  
  10.     @Test 
  11.     public void testTestRxJava() throws Exception { 
  12.         rxPresenter.testRxJava("test"); 
  13.     } 
  14.  

运行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:

  1. public class RxTools { 
  2.     public static void asyncToSync() { 
  3.         Func1<Scheduler, Scheduler> schedulerFunc = new Func1<Scheduler, Scheduler>() { 
  4.             @Override 
  5.             public Scheduler call(Scheduler scheduler) { 
  6.                 return Schedulers.immediate(); 
  7.             } 
  8.         }; 
  9.  
  10.         RxAndroidSchedulersHook rxAndroidSchedulersHook = new RxAndroidSchedulersHook() { 
  11.             @Override 
  12.             public Scheduler getMainThreadScheduler() { 
  13.                 return Schedulers.immediate(); 
  14.             } 
  15.         }; 
  16.  
  17.         RxJavaHooks.reset(); 
  18.         RxJavaHooks.setOnIOScheduler(schedulerFunc); 
  19.         RxJavaHooks.setOnComputationScheduler(schedulerFunc); 
  20.  
  21.         RxAndroidPlugins.getInstance().reset(); 
  22.         RxAndroidPlugins.getInstance().registerSchedulersHook(rxAndroidSchedulersHook); 
  23.     } 
  24.  

在RxPresenterTest.setUp()加一句RxTools.asyncToSync();:

  1. public class RxPresenterTest { 
  2.     RxPresenter rxPresenter; 
  3.  
  4.     @Before 
  5.     public void setUp() throws Exception { 
  6.         rxPresenter = new RxPresenter(); 
  7.  
  8.         RxTools.asyncToSync(); 
  9.     } 
  10.     ... 
  11.  

再跑一次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单元测试、在后面的文章会给读者介绍下。

感谢读者对笔者一直以来的支持,麻烦点赞&随手转发,好人一世平安。

关于作者

我是键盘男。在广州生活,在创业公司上班,猥琐文艺码农。喜欢科学、历史,玩玩投资,偶尔独自旅行。希望成为独当一面的工程师。

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2012-10-29 09:45:52

单元测试软件测试测试实践

2016-11-23 16:18:22

物联网产品问题

2022-10-31 13:31:15

云迁移云计算

2018-11-20 14:35:35

边缘计算物联网云计算

2010-02-07 15:42:46

Android单元测试

2017-01-14 23:42:49

单元测试框架软件测试

2010-01-28 15:54:19

Android单元测试

2017-02-21 10:30:17

Android单元测试研究与实践

2011-06-01 15:49:00

Android 测试

2017-01-14 23:26:17

单元测试JUnit测试

2017-01-16 12:12:29

单元测试JUnit

2019-01-31 08:00:50

2020-08-18 08:10:02

单元测试Java

2017-03-23 16:02:10

Mock技术单元测试

2021-05-05 11:38:40

TestNGPowerMock单元测试

2023-07-26 08:58:45

Golang单元测试

2011-07-04 18:16:42

单元测试

2020-05-07 17:30:49

开发iOS技术

2011-05-16 16:52:09

单元测试彻底测试

2023-12-11 08:25:15

Java框架Android
点赞
收藏

51CTO技术栈公众号