转载

Android测试驱动开发(TDD)

本文介绍测试驱动开发(Test-Driven development)的流程、作者对其的思考,以及一些Java、Android的测试手段介绍。

什么是TDD?

测试驱动开发(TDD)是由敏捷开发派生而来,它描述的是这样一种开发流程:

Android测试驱动开发(TDD)

图片来自 FirebaseStudio

1. 添加测试用例

每一个功能点的添加,开发者都必须对它进行详细地分析,然后快速地、有针对地书写测试代码。它和正常的开发流程不同的是,开发者需要在 开发每一个功能点之前 ,仔细想清楚需求,先用测试用例描述出功能点的 需求异常情况

这要求我们遵循一定的设计原则, 先设计出接口行为,添加空的实现 ,然后将测试用例快速写出来。

2. 测试不通过

此时我们需要跑一遍测试,我们没有写任何功能的代码之前, 新增的测试用例 肯定会 失败 ,因为我们没有实现功能细节。

3. 添加代码

针对我们写下的测试用例,完善代码。注意,在此阶段,你的 所有目标 就是让代码通过步骤一加入的测试用例。你 不应该 添加跟步骤一所加入的测试中无关的代码。

4. 重复动作

当所有的测试用例都能运行通过时,你若要应该再增加、改变功能,必须从步骤1开始重新进行。

5. 代码优化

由于你在步骤3中是面向测试用例编程,所以可能会导致代码风格、结构有不妥当的地方。此时你应该重新审视一遍自己的代码,如果有不适的地方(如重复功能的类、语义不详的名称、混乱的工程结构等),你应该对其进行一定的重构。然后你需要重复运行写好的测试用例,测试通过让你对自己的改动具有信心。

基本原则

完善测试开发流程比测试技术本身要重要很多倍。

在由产品主导业务功能、项目迭代周期快、强大的手工QA团队的现状下,程序员自己写测试用例的必要性还有吗?答案是肯定有的。团队中肯定有写一些类库、公共组件,这些库往往是纯JAVA(如字符串、文件IO、线程库)或Android的sdk(网络、下载、账号、图片加载)等。它们与业务、UI关系不大,在界面上又很难以有所体现,手工QA团队很难验证。我相信在这时候,一个基于TDD的开发流程是会有一些帮助的。单元测试的通过让你有信心将这些库的改动引入到项目中。

在下面的介绍中,你会发现测试代码都是非常容易去写的,开源界也涌出了各类测试框架方便开发者写测试用例。但是在开始写测试用例之前,需要 想清楚为什么要做测试 。你可能不需要完全遵守TDD的流程,但是如果仅仅注重测试环节,而不思考更深层次的意义,可能导致:

  • 代码很难被测试;
  • 仅仅为了写测试而写测试,测试效果不大;
  • 测试通过了,仍然有问题!
  • 测试覆盖度很高,但是代码质量依然很低。

在这个过程中,你会发现TDD是非常有工程学上的借鉴意义的。我认为TDD的几个核心原则就是:

1. 分析

你需要对新增的功能点进行系统分析,拆分每个功能点里面的正常、异常情况。每一个功能点,都应该是有一个或多个对应的 触发动作期望结果 的。如果你连触发改动的动作与期望结果都无法准确地描述出来,那么只能说明你不熟悉新增的功能,或者这个功能点不完善。在系统分析之后,写下测试用例。

2. 依赖倒置原则(DIP)

很多人看到“先写测试”这一点的时候,都很难去想象如何在没有实现细节的时候去写测试。“我都没有代码,如何写测试?”,这可能是最直观的感受。

当然,完全不写代码,就写测试用例是不可能的。但是在只声明接口行为, 不写具体实现 ,此时去写测试用例,是很容易做到的。这时候 依赖倒置原则 就有比较大的用武之地,你的功能应该依赖上层接口,这样很容易造出一个空实现的架子,用于测试用例。

3. 快速写测试、运行测试

市面上有五花八门的测试技术与框架,各类的mock横行霸道。但是我们应该注意一点: 我们写的测试不能是脆弱、编写成本高的 。如果某个功能点,我们辛辛苦苦写了很多测试,但是下一个版本迭代,所有的测试用例都被推翻,那么我们可能就要考虑测试的必要性或者是不是测试的方法不对了。或者如果测试非常难写(相对于手工测试来说)、测试点是不可重复运行的,那都可以考虑不写测试。

关于项目与工程的应用上,更多还是应该结合项目自身来适应。

在此特别推荐一个博客系列: World-Class Testing Development Pipeline for Android 。讲述了 Karumi 团队在做Android 自动化测试时候的一些思考与碰到的问题,如果你希望写好测试用例,十分建议一读。

英语不好的同学可以读 Mark大神的翻译 。

单元测试简介

单元测试是指对软件中的最小可测试单元进行检查和验证,它满足:

  • 隔离性 单元测试只在一个小模块内进行,不与其他模块交互,也不影响其他单元测试。
  • 重复性 重复运行相同单元测试的结果一致。

单元测试主要的工作就是 验证行为的结果 。这个概念跟上文中说的 触发动作期望结果 是一致的。下面先介绍一些技术用语:

覆盖率

覆盖率(Coverage)即跑一遍单元测试,统计覆盖到的各类指标。简单来说有这么几种:

  • 函数覆盖率 一个类里面被单元测试调用到的函数在它所有函数中的占比。
  • 语句覆盖率分支覆盖率 一个类中,单元测试所覆盖的语句、分支在这个类的语句、分支中的占比。举个例子:
publicintfunc(inta,intb){
intresult =0;
if(a > b) {
 result = a;
 } elseif(a < b) {
 result = b;
 } elseif(a == b) {
 result = -1;
 }
returnresult;
}

//测试代码
publicinttestFunc(){
 Assert.assertEquals(func(1,0),1);
 Assert.assertEquals(func(1,1), -1);
}

上面的代码中我们只测试了(a > b) 和 (a = b)的情况,那么(a < b)这个if分支就没有走,并且它涉及到的语句也没有走。即它们没有被覆盖。

Java导出测试覆盖率的工具使用最多的是 Jacoco , Intelij Idea(Android Studio)与Eclipse都有相应的工具。在Eclipse上可以用 Eclmma ,而Intellij Idea已经将它内置为IDE的一部分。

值得一提的是AndroidTest无法直接用工具测试覆盖率,不过它的gradle插件内置了Jacoco。对于覆盖率,可以通过在 build.gradle 中添加

android {
 buildTypes {
 debug {
 testCoverageEnabled = true
 }
 }
}

之后运行命令行 ./gradlew createDebugCoverageReport 即可导出report到 build/report 路径下。

Mock

Mock是测试中非常经常使用到的一个术语,它的作用就如字面意思:“虚拟对象”。它有很多方面的作用,特别是在测试上。我们之前所说,测试就是需要列举出“触发动作”和验证”期望结果”。那么现在有两个问题:

  1. 通常我们在设计代码的时候,不会去保留操作结果,这会给程序带来非常多冗余的变量、代码逻辑。
  2. 触发动作经常是有一定上下文、环境依赖的,测试中难以模拟出这个情况。

为了应对这样的窘境,各种各样的Mock开源库出现了,举两个用得最多的例子:

  • Mockito 是一个开源的Mock库。提供了创建虚拟类(Mock)、局部扩展(Spy)、验证调用行为(Verify)的API。
  • PowerMock 是针对上述Mockito的拓展。它通过自定义的 ClassLoader ,能够达到它们无法做到的一些事情:针对静态类、final类、private方法的Mock与Verify。

这里需要注意,使用Mock之前应该仔细思考它的必要性,如果你花很多时间、大量代码来生成Mock、验证Mock对象的行为,这可能就偏离了测试的初衷,走入了“为测试而测试”的陷阱。你可能就要思考,为什么你的代码需要这么多Mock才能够被单元测试,会不会写的耦合度太高了?

好,言归正传,我们来简单看一下它们用法。想象这么一个场景,你的操作需要依赖时间,你要检查日期的改变、时间的流逝等相关的事情,那么你如果手工测试就非常尴尬,花费不必要的时间在等待时间上,而用PowerMock,你可以做到改变时间:

@RunWith(PowerMockRunner.class)
@PrepareForTest({System.class})
publicclassTimeUtilsTest{
@Before
publicvoidsetUp(){
 Random random = newRandom(RANDOM_SEED);

 PowerMockito.mockStatic(System.class);
 when(System.currentTimeMillis()).thenReturn(0l);//直接将System.currentTimeMillis设置为0.
 }
}

关于Mockito的更多用法,可以参考我另一篇文章 使用Mockito和Roboletric进行Android单元测试 。

Android 测试概览

在Android上的测试技术总的来说可以分为三种:

  • 本地测试(Local Unit test) 跑在JVM上的单元测试,在 /src/test/java 下的代码。测试用例与其他模块隔离,所有对其他模块的依赖均用Mock解决。配合 Robolectric 也可以跑部分Android相关的代码测试。容易书写,容易运行。
  • Instrument 测试 跑在Android设备上的测试,在 /src/androidTest/java 下的代码。默认使用junit3的写法,使用 AndroidJUnit4 可以使用junit4的单元测试(这也是官方推荐的做法)。(之间区别可以参考junit3 vs junit4)。

可以简单用一张图来总结:

Android测试驱动开发(TDD)

那来说说它们的各类应用场景吧。

1. 基础工具

上图中底层是各类测试用到的基础工具,用好它们能让你的测试写起来更加顺手。除了之前介绍的Mock&Verify的库之外,分别介绍一下其他的:

  • AssertJ 是一个开源的Assert语法的拓展库,它提供了各类方便的情景检查与链式调用的语法糖,易于拓展,用起来非常舒服。
  • AssertJ-Android 是Square针对AssertJ的一个拓展,提供了对Android各类控件的情景检查。
  • Harmcrest 是另一个开源的Assert语法拓展库,它提供了方便、强大的情景 匹配 API,不仅支持Java,也支持其他多种语言。

2. Local Unit 测试

单元测试通常用来测试单个模块的功能,它与其他模块隔离。本地单元测试通常用来测试一些与Android无关的、纯Java的代码(比如 StringUtils , ReflectionUtils , ***Utils 等)。配合Android Studio能够快速地运行起单个、多个的单元测试。

Robolectric 的出现让一些仅跟UI交互的Android代码变得非常容易测试,它能够让一些仅依赖UI、对于Android设备环境没有强需求的模块(GPS、电池状态、跨App请求等)能够直接在JVM上跑起测试代码。

3. Android Instrument 测试

Espresso & UIAutomator.

Android Testing Support库

其他

Robolectric+PowerMock

值得一提的是,Robolectric同样也用到了自定义ClassLoader,配合PowerMock使用时需要参考 Wiki 。

原文  http://blog.desmondyao.com/android-test/
正文到此结束
Loading...