随着 Agile 的普及,以及开发人员对测试重要性的认识逐步加深,单元测试已经成了越来越多软件项目开发中不可缺少的一部分。无论项目是不是采用 TDD 的形式来进行开发,单元测试都能够为项目的修改和重构提供一定的保障。
Android 作为主要的移动平台之一,吸引了无数的开发人员。但面对 Android 平台和环境的种种限制,很多开发人员往往有心无力,很难为其项目添加全面有效的单元测试。
Android 平台的开发环境中集成了一个测试框架 ( Instrumented Test ),用于支持其单元测试和验收测试。 Robotium 同样提供一个类似于 Selenium 的测试框架,使得开发人员可以对应用的功能进行验证。这两种方式提供的测试环境都类似于集成测试,它们的测试用例都需要运行在模拟器上,通过对模拟器的操作或者 mock,来触发函数调用,进而对其结果进行验证。这种方法通常粒度较大,测试的编写和维护较为困难,而最为重要的是,由于测试需要运行于模拟器或测试机器上,我们在运行前需要将测试和应用打包,进行部署安装,并最终运行在模拟器或测试机器的 Delvik 虚拟机上,其运行速度较普通的单元测试要慢许多,如果使用 TDD 来进行开发,根本无法达到快速开发的要求。
之所以这些框架的测试用例都需要在模拟器中运行,是因为我们平时在开发时所使用的 Andorid.jar 是被精简过的,只是用于日常开发的,它只是一个 placeholder,使得我们在开发时能够不出编译错误,它完全是一个 stub 包,其中所有的类都只是 Android 平台接口的一个 stub,如果在代码中运行这个 Android.jar,它们所有的方法都只会抛出一个 java.lang.RuntimeException(“Stub!”) 异常。所以,一旦测试代码需要真正调用 Android 平台相关的类或接口,它们就必须运行于真正实现了 Android 的环境,如模拟器或者是测试机器。
我们的另外一个选择是只对 POJO 进行单元测试,如果遇到 Android 相关的代码,就使用 Mock 框架对其进行模拟。这种方式一定程度上可以解决我们的问题,但这意味着我们需要大量的在测试环境中使用 mock 和 stub。另外,虽然 Android 中界面的布局通常使用 XML 来实现,但项目的代码中还是会存在各种对界面的操作和更新,UI 和逻辑的耦合使测试更加不易。
而且即使这样,由于 Android 平台的复杂性(static 方法,final 方法和类,Context 和 Resources 的管理),我们也很难对 Android 相关的代码进行测试,以保证测试率。
那么如何能够在不增加开发成本的情况下,有一个稳定快速的单元测试环境呢?
我们目前的选择是使用 MVP 模式和 Robolectric
Android 的体系结构非常适合于使用 MVP 模式进行开发,与 MVC 模式不同,Android 中的 Activity 并不是一个标准的 Controller,它的首要职责是加载应用的布局和初始化用户界面,并接受并处理来自用户的操作请求,进而作出响应。随着界面及其逻辑的复杂度不断提升,Activity 类的职责不断增加,以致变得庞大臃肿。当我们将其中复杂的逻辑处理移至另外的一个类(Presneter)中时,Activity 其实就是 MVP 模式中 View,它负责 UI 元素的初始化,建立 UI 元素与 Presenter 的关联(Listener 之类),同时自己也会处理一些简单的逻辑(复杂的逻辑交由 Presenter 处理)。如图所示:
通过这种职责的分离,一方面代码的可读性得到了提高,另一方面我们可以更为方便地通过 mock Activity 的方式对各种逻辑(Presenter 中的方法)进行测试。
对于测试环境的搭建和测试 Android 相关的代码,我们则借助于 Robolectric 的帮助。
Robolectric 在其所提供的测试框架中,完全模拟了 Android SDK 的 jar 文件(不会再有恼人的 stub 异常),它使得我们的测试可以运行于 JVM 之上(速度得到大幅度的提升),因此我们可以用它对 Android 应用进行测试驱动开发。Roblectric 同时实现了 Android 中对 XML 的解析,模拟了 View,Layout,以及资源的加载,它使得 Android 的环境对于开发人员来说更像是一个黑盒,从而使开发人员不用大量使用 mock,就可以方便的对资源状态和 Android 相关的代码进行测试。
Robolectric 是如何做到这点的呢?
Robolectric 使用了 javassist 在运行时动态修改 Android.jar 中类的 byte code,Robolectric 会在 JVM 加载 Android.jar 包的时候,重写其中类的方法。Roblectroic 会让这些方法有返回值(null 或是 0) 而不是抛出异常 ,或者将这些方法调用转向 Shadow Objects 来模拟 Android SDK 的实现。Shadow Objects 是 Robolectric 在运行时插入到 Android.jar 包相应的类中的,它们会实际处理方法的调用,并记录相应的状态,以备在 assert 的时候进行查询。如图所示。Robolectric 提供了大量的 Shadow Objects,覆盖了测试开发过程中绝大多数逻辑功能的需要 。
Robolectric 的使用
基于 Robolectric 的测试需要使用其特定的 test runner(RobolectricTestRunner)来运行,我们可以通过扩展 RobolectricTestRunner 来创建一个自己的 test runner,并在其构造函数中设定需要加载的 AndroidManifest.xml 和 resource 目录 。如:
public class MyTestRunner extends RobolectricTestRunner { public MyTestRunner(Class<?> testClass) throws InitializationError { super(testClass, new RobolectricConfig(new File("my_app/AndroidManifest.xml"), new File("my_app/res"))); } }
有了自己的 test runner, 我们可以来写一个简单的 Robolectric 测试了
1 @RunWith(MyTestRunner.class) public class SignInScreenTest { @Test public void should_start_intent_when_click_registration_button() { 2 Activity activity = new Activity(); SignInScreen signInScreen = new SignInSceen(activity); 3 TextView textView = (TextView) signInScreen.findViewById(R.id.sign_in_registration); textView.performClick(); 4 ShadowActivity shadowActivity = Robolectric.shadowOf(activity); Intent nextStartedActivity = shadowActivity.getNextStartedActivity(); ShadowIntent shadowIntent = Robolectric.shadowOf(nextStartedActivity); assertThat((Class<WebPageActivity>) shadowIntent.getIntentClass(), equalTo(WebPageActivity.class)); } }
在这段测试代码中:
- (1)声明了测试运行的 test runner;就像普通的单元测试,它也分为了 set up, method invoke,以及 assert 三个阶段。
- 在(2)中,测试初始化了一个 Activity 用于提供 Context,并使用这个 Activity 对象生成了一个 SignInScreen 实例;
- 第二个阶段,也是就(3)中,代码在生成的登录界面中找到注册按钮,并进行点击。最为有意思的第三个阶段需要验证注册按钮的点击触发了我们期望的事件,即使用 Implicit Intent 来打开 WebPageActivity。
- 为了进行这个验证,(4)中首先通过 Robolectric 的静态方法 shadowOf 来获取 activity 对象相应的 Shadow Object ,而通过这个 Shadow Object, 代码获得了 activity 对象的所开启的 Intent 对象。最后通过 Intent 对象的 Shadow Object ,我们可以获得其 intent class 并进行验证。
通过这个测试我们可以看到,有了 Robolectric 的帮助,我们可以轻松的生成 Activity 实例,加载 xml 布局文件,进行组件上的方法调用。通过 shadow 对象,我们则可以获取 Android 相关类的对象状态信息,来对测试的结果进行验证。实际上除了 Intent,我们还可以对通过使用 Robolectric 对代码中的 Dialog,HTTP 请求,数据库操作等各个方面进行测试。
Robolectric 并没有为 Android SDK 中的所有类都定义 shadow 对象,你可以通过调用 Robolectric.getDefaultShadowClasses() 方法来查看你所需要的类是否已经被注册到了需要被 shadow 的类列表中。如果没有你可能就需要对其进行定制和扩展。关于如何添加 Shadow Objects 而增加 Robolectric 的功能,在 Robolectric 的网站文档中有详细的描述。
由于Robolectric 的测试是可以脱离Android 的SDK 运行于JVM 上,我们就可以像运行普通的jUnit 测试一样在IDE 中或者在终端使用构建脚本运行我们的测试。
由于Robolectric 的更新并不是很频繁,我们在平时也遇到了一些需要定制的情况,如支持Android4.0,使用sonar 进行项目质量分析等等。所以我们在github 上fork 了Robolectric 的工程,并以git submodule 的方式将其加入到我们的工程管理中来,这样,我们就可以根据自己的需要来对Robolectric 进行修改和扩展。由于我们对Robolectric 的修改频率非常的低,在每一次修改后,可以将其编译打包成一个jar 文件,将这个jar 文件加入到我们的工程管理中,让我们的测试代码仍然依赖于这个jar 文件,这样可以免去在运行测试中不必要的对Robolectric 的重复编译,加快测试代码的运行速度。
我们在当前的项目中也进行了一定的关于验收测试方面的尝试,由于测试脚本是开发人员与BA 以及QA 进行沟通的一种重要途径,也是开发人员和QA 进行人工测试的基准,因此我们仍然选用了cucumber 作为我们编写脚本的工具,再使用cuke4duke 和jRuby 对其进行解析和执行。但目前这种测试方式似乎并不成熟,我们在这种尝试和实践的过程中遇到了种种的问题,主要在于测试编写和维护上的困难,这也导致了我们验收测试的覆盖率并不高。我们也会在这一方向上进行更多的尝试,如果大家有更好的关于验收测试自动化方面的实践,也希望能够得到你们的帮助和指正。
关于作者
张磊,ThoughtWorks 程序员,在J2EE, RoR, Android 和iOS 平台有开发经验,喜欢漂亮的代码和解决方案
感谢张凯峰对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论