Android单元测试(七):Robolectric,在JVM上调用安卓的类

今天讲讲Android上作单元测试的最后一个难点,那就是在JVM上没法调用安卓相关的类,否则的话,会报相似于下的错误: java.lang.RuntimeException: Method isEmpty in android.text.TextUtils not mocked.java

关于这个话题,其实我之前是写过的,也许今天我回过头来写这个话题,会采用不同的形式,不同的心态来写,然而,做为我写过的第一篇关于单元测试的文章,并且看看时间,是去年的6月15号,再过几天,恰好一周年。想一想这篇文章是在我刚开始探索,尝试在安卓上面写单元测试的时候,写的一篇文章,现在由于安卓单元测试的缘由,我认识了不少同行,甚至不时有人叫我“大牛大神”之类的,虽然知道你们是客气,我也受之有愧,但怎么滴内心也有点虚荣的开心,哈哈哈。。所以如今回过头去看看当时本身写的东西,不由以为有点那啥。。。所以,我决定把以前的文章稍做补充和修改,做为这个系列的第七篇。android

----------------------如下文字写于去年今天-----------------------git

做为一只本科非计算机专业的程序猿,手动写单元测试是我历来没接触过的东西,甚至在几个月前,我都不知道单元测试是什么东西。倒不是说没听过这个词,也不是不知道它的大概是什么东西——“用来测试一个方法,或者是一小块代码的测试代码”。然而真正是怎么作的?我并无一个概念,或者说并无一个感受。程序员

记得第一份工做在创新工场的时候,听当时的boss说,公司有个神级的程序员,他会写大量的单元测试,甚至50%以上的代码都是单元测试。当时崇拜之极,却仍然以为写单元测试是很麻烦的一件事情。github

扯远了,话说回来,当你接触多了国外的技术博客,视频以后,你会发现,单元测试甚至TDD,在国外是很是流行的事情。不少人甚至说离开了单元测试,他们便没有办法写代码。这些都让我对单元测试的好感度逐渐的上升。然而,真正让我下定决心,必定要研究一下这个东西的,是前段时间看大名鼎鼎的《重构:改善现有代码的艺术》里面的一段话:数据库

I've found that writing good tests greatly speeds my programming, even if I'm not refactoring. This was a surprise for me, and it is counterintuitive for many programmers...
--Martin Fowler 《Refactoring: Improving the Design of Existing Code》微信

是的,你没看错,他说单元测试能够节约时间,提升开发速度!!!身为一个无可救药的懒癌患者,看了这句话简直就像看到了一道神光似的!既然均可以节省时间,那确定是要看看的啊!网络

有趣的是,Martin Fowler在《重构》里面说他最初是由于 Dave Thomas说的一句话,让他走上了单元测试的不归路。而我这几天恰好又在看Dave Thomas写的《Programming Ruby 1.9 & 2.0》,也算是个巧合啊!架构

Martin Fowler在《重构》里面还解释了为何单元测试能够节省时间,大意是咱们写程序的时候,其实大部分时间不是花在写代码上面,而是花在debug上面,是花在找出问题到底出在哪上面,而单元测试能够最快的发现你的新代码哪里不work,这样你就能够很快的定位到问题所在,而后给以及时的解决,这也能够在很大程度上防止regression(相信QE和QA们必定很喜欢哈哈。。。),这也是个大部分程序员和测试都很痛恨的问题。 app

以后不久,就开始花了点时间了解了一下Android里面怎么作unit testing,结果却发现那是个很是难办的事情。。。

为何android unit testing很差作

咱们知道安卓的app须要运行在delvik上面,咱们开发Android app是在JVM上面,在开发以前咱们须要下载各个API-level的SDK的,下载的每一个SDK都有一个android.jar的包,这些能够在你的android_sdk_home/platforms/下面看到。当咱们开发一个项目的时候,咱们须要指定一个API-level,其实就是将对应的android.jar 加到这个项目的build path里面去。这样咱们的项目就能够编译打包了。然而如今的问题是,咱们的代码必须运行在emulator或者是device上面,说白了,就是咱们的IDE和SDK只提供了开发和编译一个项目的环境,并无提供运行这个项目的环境,缘由是由于android.jar里面的class实现是不完整的,它们只是一些stub,若是你打开android.jar下面的代码去看看,你会发现全部的方法都只有一行实现:

throw RuntimeException("stub!!");

而运行unit test,说白了仍是个运行的过程,因此若是你的unit test代码里面有android相关的代码的话,那运行的时候将会抛出RuntimeException("stub!!")。为了解决这个问题,如今业界提出了不少不一样的程序架构,好比MVP、MVVM等等,这些架构的优点之一,就是将其中一层抽出来,变成pure Java实现,这样作unit testing就不会遇到上面这个问题了,由于其中没有android相关的代码。

好奇的童鞋可能会问了,既然android.jar的实现是不完整的,那为何咱们能够编译这个项目呢?那是由于编译代码的过程并无真正的运行这些代码,它只会检查你的接口有没有定义,以及其余的一些语法是否是正确。举个简单的例子:

public class Test {
    public static void main(String[] argv) {
        
        testMethod();
    }
    public static void testMethod() {
        throw RuntimeException("stub!!");
    }
}

上面的代码你一样能够编译经过,但你运行的时候,就会抛出异常RuntimeException("stub!!")。当咱们的项目运行在emulator或者是device上面的时候,android.jar被替换成了emulator或者是device上面的系统的实现,那上面的实现是真正实现了那些方法的,因此运行起来没有问题。

话说回来,MVP、MVVM这些架构模式虽然解决了部分问题,能够测试项目中不含android相关的类的代码,然而一个项目中仍是有很大部分是android相关的代码的,因此上面那种解决方案,实际上是放弃了其中一大块代码的unit test。
固然,话说回来,android仍是提供了他本身的testing framework,叫instrumentation,可是这套框架仍是绕不开刚刚提到的问题,他们必须跑在emulator或者是device上面。这是个很慢的过程,由于要打包、dexing、上传到机器、运行起来界面。。。这个相信你们都有体会,尤为是项目大了之后,运行一次甚至须要一两分钟,项目小的话至少也要十几秒或几十秒。以这个速度是没有办法作unit test的。

那么怎么样便可以给android相关的代码作测试,又能够很快的运行这些测试呢?

Robolectric to the rescue

解决的办法就是使用一个开源的framework,叫robolectric,他们的作法是经过实现一套JVM能运行的Android代码,而后在unit test运行的时候去截取android相关的代码调用,而后转到他们的他们实现的代码去执行这个调用的过程。举个例子说明一下,好比android里面有个类叫TextView,他们实现了一个类叫ShadowTextView。这个类基本上实现了TextView的全部公共接口,假设你在unit test里面写到 String text = textView.getText().toString();。在这个unit test运行的时候,Robolectric会自动判断你调用了Android相关的代码textView.getText(),而后这个调用过程在底层截取了,转到ShadowTextViewgetText实现。而ShadowTextView是真正实现了getText这个方法的,因此这个过程即可以正常执行。

除了实现Android里面的类的现有接口,Robolectric还作了另一件事情,极大地方便了unit testing的工做。那就是他们给每一个Shadow类额外增长了不少接口,能够读取对应的Android类的一些状态。好比咱们知道ImageView有一个方法叫setImageResource(resourceId),然而并无一个对应的getter方法叫getImageResourceId(),这样你是没有办法测试这个ImageView是否是显示了你想要的image。而在Robolectric实现的对应的ShadowImageView里面,则提供了getImageResourceId()这个接口。你能够用来测试它是否是正确的显示了你想要的Image。

Talk is cheap. Show me the code!

下面简单的介绍一下使用Robolectric来作unit testing。注意:下面的配置方法指的是AndroidStudio上面的,Eclipse用户自行google一下配制方法。

要使用Robolectric,须要作几步配置工做。

  1. 首先须要将它和JUnit4加到你项目的dependencies里面,

    testCompile 'junit:junit:4.12'  
    testCompile ’org.robolectric:robolectric:3.0-rc3’

    其中的Robolectric的最新版本号可能会变,具体能够上jcenter查看一下当前的最新版本号。

  2. 若是你用的是AndroidStudio2.0一下的版本,须要将Build Variant里面的Test Artifact选择为Unit Test,若是你找不到Build Variant,能够在菜单栏选择View -> Tool Windows -> Build Variant. 正常状况下它会出如今左下角。AndroidStudio2.0以上的版本已经不须要了。

  3. 若是是Mac的话,还须要配置一个东西,菜单栏选择 Run -> Edit Configuration -> Defaults -> JUnit,在Configuration tab将working directory改为$MODULE_DIR$。这个配置是Robolectric官方文档提到的,但我用最新的AndroidStudio1.3实验的时候,忘了配置这个,貌似也能够正确运行,anyway,配置一下也无所谓。具体见Robolectric的官方文档,最下面那部分。

到这里,就能够开始code了。

测试代码是放在app/src/test下面的,test class的位置最好跟target class的位置对应,好比MainActivity放在app/src/main/java/com/domain/appname/MainActivity.java,那么对应的test class MainActivityTest最好放在 app/src/test/java/com/domain/appname/MainActivityTest.java

这里举个简单又稍微有点用的例子,假设app里面有两个Activity:MainActivitySecondActivityMainActivity 里面有一个TextView,点击一下这个TextView将跳转到 SecondActivityMainActivity里面的代码大概以下:

public class MainActivity extends AppCompatActivity {  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  

        TextView textView = (TextView)findViewById(R.id.textView1);  
        textView.setOnClickListener(new OnClickListener() {  
            @Override  
            public void onClick(View v) {  
                startActivity(new Intent(MainActivity.this, SecondActivity.class));  
            }  
        });  
    }  
}

对应的测试类,MainActivityTest的代码:

@RunWith(RobolectricGradleTestRunner.class)  
@Config(constants = BuildConfig.class, sdk = 21)  
public class MainActivityTest {  
    @Test  
    public void testMainActivity() {  
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);  
        mainActivity.findViewById(R.id.textView1).performClick();  

        Intent expectedIntent = new Intent(mainActivity, SecondActivity.class);  
        ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity);  
        Intent actualIntent = shadowActivity.getNextStartedActivity();  
        Assert.assertEquals(expectedIntent, actualIntent);  
    }  
}

上面的代码测试的就是当用户点击textView的时候,程序会正确的跳转到SecondActivity。其中@RunWith(RobolectricGradleTestRunner.class)表示用Robolectric的TestRunner来跑这些test,这就是为何Robolectric能够检测到你调用了Android相关的类,而后截取这些调用,转到他们的Shadow类的缘由。此外,@Config用来配置一些东西。

  • 代码中的 MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class); 用来建立MainActivity的instance,或者说,用来启动这个Activity,当Robolectric.setupActivity返回的时候,这个Activity已经完成了onCreate、onStart、onResume这几个生命周期的回调了。

  • mainActivity.findViewById(R.id.textView1).performClick();用来触发点击事件。

  • ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity);用来获取mainActivity对应的ShadowActivity的instance。

  • shadowActivity.getNextStartedActivity();用来获取mainActivity调用的startActivity的intent。这也是正常的Activity类里面不具备的一个接口。

最后,调用Assert.assertEquals来assert启动的intent是咱们指望的intent。

运行这个unit test,启动命令行,cd到项目的根目录,运行 ./gradlew test,几秒钟后,你将看到测试运行的结果

...  
:app:processDebugJavaRes UP-TO-DATE
:app:compileDebugJava UP-TO-DATE
:app:preCompileDebugUnitTestJava
:app:preDebugUnitTestBuild UP-TO-DATE
:app:prepareDebugUnitTestDependencies
:app:processDebugUnitTestJavaRes UP-TO-DATE
:app:compileDebugUnitTestJava
:app:compileDebugUnitTestSources
:app:mockableAndroidJar UP-TO-DATE
:app:assembleDebugUnitTest
:app:testDebug

BUILD SUCCESSFUL

Total time: 12.884 secs

在个人机器上(MacBook Air 2013款,8G内存,算比较低的配置),运行这个test只须要不到12秒钟,若是直接在AndroidStudio里面运行的话,这个速度会更快,通常能够再10秒以内完成,或许没有达到普通JUnit的秒级速度,然而相对于用Instrumentation来讲已是极大的提高了。

注:第一次运行可能须要下载一些library,或者是gradle自己,可能须要花一点时间,这个跟unit test自己没关。

整个项目已经放到github上面:robolectric-demo

小结

整体来讲,Robolectric是个很是强大好用的unit testing framework。虽然使用的过程当中确定也会遇到问题,我我的就遇到很多问题,尤为是跟第三方的library好比Retrofit、ActiveAndroid结合使用的时候,会有很多问题,但瑕不掩瑜,咱们依然能够用它完成很大部分的unit testing工做。

--------------------------原文结束--------------------------

今天回过头来看,我想强调的是,Robolectric到底应该充当什么样的一个角色。在没有Robolectric的pure JUnit世界,咱们是很难对一整个流程进行测试的,由于上层的界面是安卓的类,底层的数据库和Preference等等是安卓的类。所以,咱们没有办法对一整个流程作一个完整的测试。然而有了robolectric之后,咱们就能够这么作了:启动activity,向网络或数据库请求数据,更新界面。。。所以,有了这个东西之后,咱们的第一反应可能就是去测试这整个app流程。因此常常有小伙伴问我,Robolectric究竟是作单元测试的框架,仍是作集成测试,甚至UI测试的框架?

这就是我想强调的,须要避免的陷阱。对于上面的问题,个人回答是:Robolectric就是一个可以让咱们在JVM上跑 测试 时够调用安卓的类的框架,至于咱们是拿它来作单元测试仍是集成测试,彻底取决于咱们本身。而回到咱们强调的 单元测试,测一个小的独立的代码单元,Robolectric的角色,应该是一个让咱们在作 单元测试 的过程当中,可以调用安卓的类,测试安卓的类,把安卓的类当作普通的纯java类的一个framework,仅此而已。
这点,谨记。

最后,若是你也对安卓单元测试感兴趣的话,欢迎加入咱们的交流群:


(微信群已满100人,请先关注公众号,从公众号的菜单获取加入方法)

有任何意见或建议,或者发现文中任何问题,欢迎留言评论!

做者 小创 更多文章 | Github | 公众号

相关文章
相关标签/搜索