文章: Android中的单元测试

标签: 文章 android 单元测试 | 发表时间:2012-07-30 13:00 | 作者:
出处:http://pipes.yahoo.com/pipes/pipe.info?_id=10560380f804c7341f042a2b8a03e117

随着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中文站投稿或者参与内容翻译工作,请邮件至 [email protected]。也欢迎大家通过新浪微博( @InfoQ)或者腾讯微博( @InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

相关 [文章 android 单元测试] 推荐:

文章: Android中的单元测试

- - InfoQ cn
随着Agile的普及,以及开发人员对测试重要性的认识逐步加深,单元测试已经成了越来越多软件项目开发中不可缺少的一部分. 无论项目是不是采用TDD的形式来进行开发,单元测试都能够为项目的修改和重构提供一定的保障. 有奖参与:天翼伦敦会,上传应用,为中国队加油. QClub七月技术沙龙(太原/北京/上海/厦门/西安 7月21/28/29日 免费报名中.

Android单元测试

- - CSDN博客推荐文章
    单元测试不管对于初学编程还是已经工作了很久的开发者来说,都不乐意花时间去写认为没用的代码进行测试,只要交给测试人员就行了,虽然这样也能把软件改出来,但也许你要花上几倍的时间去修改问题,如果在开发的过程中花点时间去写单元测试代码,把尽可能出问题的地方都测试一遍,把问题扼杀在最开始的地方,这样你就不必为后来找问题出处而烦恼.

Android单元测试研究与实践

- - 美团点评技术团队
处于高速迭代开发中的Android项目往往需要除黑盒测试外更加可靠的质量保障,这正是单元测试的用武之地. 单元测试周期性对项目进行函数级别的测试,在良好的覆盖率下,能够持续维护代码逻辑,从而支持项目从容应对快速的版本更新. 单元测试是参与项目开发的工程师在项目代码之外建立的白盒测试工程,用于执行项目中的目标函数并验证其状态或者结果,其中,单元指的是测试的最小模块,通常指函数.

Android单元测试与模拟测试

- - 神刀安全网
考虑可读性,对于方法名使用表达能力强的方法名,对于测试范式可以考虑使用一种规范, 如 RSpec-style. 不要使用逻辑流关键字(If/ese、for、do/while、switch/case),在一个测试方法中,如果需要有这些,拆分到单独的每个测试方法里. 测试真正需要测试的内容,需要覆盖的情况,一般情况只考虑验证输出(如某操作后,显示什么,值是什么).

Android公共库选型 单元测试 依赖管理等调研

- - Trinea
抱歉,最近一个多月一直比较忙,博客许久未更新. 后续更新周期会慢一些,不过依旧会陆续分享一些原创. 最近在调研一些事情,欢迎大家留言告诉我自己公司的一些情况、经验及想法. 测试辅助框架选型,Quality Tools for Android, android-test-kit, robolectric, Android FEST指标同上.

Hadoop之MapReduce单元测试

- - ITeye博客
通常情况下,我们需要用小数据集来单元测试我们写好的map函数和reduce函数. 而一般我们可以使用Mockito框架来模拟OutputCollector对象(Hadoop版本号小于0.20.0)和Context对象(大于等于0.20.0). 下面是一个简单的WordCount例子:(使用的是新API).

springboot单元测试技术

- - 海思
整个软件交付过程中,单元测试阶段是一个能够最早发现问题,并且可以重复回归问题的阶段,在单元测试阶段做的测试越充分,软件质量就越能得到保证. 具体的代码参照 示例项目 https://github.com/qihaiyan/springcamp/tree/master/spring-unit-test.

“单元测试要做多细?”

- - 酷壳 - CoolShell.cn
这篇文章主要来源是StackOverflow上的一个回答——“ How deep are your unit tests?”. 一个有13.8K的分的人( John Nolan)问了个关于TDD的问题,他说——. “TDD需要花时间写测试,而我们一般多少会写一些代码,而第一个测试是测试我的构造函数有没有把这个类的变量都设置对了,这会不会太过分了.

迈出单元测试的第一步

- - 酷勤网-挖经验 [expanded by feedex.net]
单元测试不仅是软件行业的最佳实践,在敏捷方法的推动下,它也成为了可持续软件生产的支柱. 年度敏捷调查,70%的参与者会对他们的代码进行单元测试. 单元测试和其他敏捷实践密切相关,所以开始编写测试是组织向敏捷转型的踏脚石. 我将在本文介绍符合要求的小技巧,以及在开发周期里进行单元测试的步骤. 没有自动化,单元测试的习惯也不会持续太久.

iOS开发进阶之单元测试

- - 博客园_首页
本文侧重讲述如何在iOS程序的开发过程中使用单元测试. 使用Xcode自带的OCUnit作为测试框架. 单元测试作为敏捷开发实践的组成之一,其目的是提高软件开发的效率,维持代码的健康性. 其目标是证明软件能够正常运行,而不是发现bug(发现bug这一目的与开发成本是正相关的,虽然发现bug是保证软件质量的一种手段,但是很显然这与降低软件开发成本这一目的背道而驰).