本文介绍测试驱动开发(Test-Driven development)的流程、作者对其的思考,以及一些Java、Android的测试手段介绍。
测试驱动开发(TDD)是由敏捷开发派生而来,它描述的是这样一种开发流程:
图片来自 FirebaseStudio
每一个功能点的添加,开发者都必须对它进行详细地分析,然后快速地、有针对地书写测试代码。它和正常的开发流程不同的是,开发者需要在 开发每一个功能点之前 ,仔细想清楚需求,先用测试用例描述出功能点的 需求 与 异常情况 。
这要求我们遵循一定的设计原则, 先设计出接口行为,添加空的实现 ,然后将测试用例快速写出来。
此时我们需要跑一遍测试,我们没有写任何功能的代码之前, 新增的测试用例 肯定会 失败 ,因为我们没有实现功能细节。
针对我们写下的测试用例,完善代码。注意,在此阶段,你的 所有目标 就是让代码通过步骤一加入的测试用例。你 不应该 添加跟步骤一所加入的测试中无关的代码。
当所有的测试用例都能运行通过时,你若要应该再增加、改变功能,必须从步骤1开始重新进行。
由于你在步骤3中是面向测试用例编程,所以可能会导致代码风格、结构有不妥当的地方。此时你应该重新审视一遍自己的代码,如果有不适的地方(如重复功能的类、语义不详的名称、混乱的工程结构等),你应该对其进行一定的重构。然后你需要重复运行写好的测试用例,测试通过让你对自己的改动具有信心。
在由产品主导业务功能、项目迭代周期快、强大的手工QA团队的现状下,程序员自己写测试用例的必要性还有吗?答案是肯定有的。团队中肯定有写一些类库、公共组件,这些库往往是纯JAVA(如字符串、文件IO、线程库)或Android的sdk(网络、下载、账号、图片加载)等。它们与业务、UI关系不大,在界面上又很难以有所体现,手工QA团队很难验证。我相信在这时候,一个基于TDD的开发流程是会有一些帮助的。单元测试的通过让你有信心将这些库的改动引入到项目中。
在下面的介绍中,你会发现测试代码都是非常容易去写的,开源界也涌出了各类测试框架方便开发者写测试用例。但是在开始写测试用例之前,需要 想清楚为什么要做测试 。你可能不需要完全遵守TDD的流程,但是如果仅仅注重测试环节,而不思考更深层次的意义,可能导致:
在这个过程中,你会发现TDD是非常有工程学上的借鉴意义的。我认为TDD的几个核心原则就是:
你需要对新增的功能点进行系统分析,拆分每个功能点里面的正常、异常情况。每一个功能点,都应该是有一个或多个对应的 触发动作 和 期望结果 的。如果你连触发改动的动作与期望结果都无法准确地描述出来,那么只能说明你不熟悉新增的功能,或者这个功能点不完善。在系统分析之后,写下测试用例。
很多人看到“先写测试”这一点的时候,都很难去想象如何在没有实现细节的时候去写测试。“我都没有代码,如何写测试?”,这可能是最直观的感受。
当然,完全不写代码,就写测试用例是不可能的。但是在只声明接口行为, 不写具体实现 ,此时去写测试用例,是很容易做到的。这时候 依赖倒置原则 就有比较大的用武之地,你的功能应该依赖上层接口,这样很容易造出一个空实现的架子,用于测试用例。
市面上有五花八门的测试技术与框架,各类的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开源库出现了,举两个用得最多的例子:
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上的测试技术总的来说可以分为三种:
/src/test/java
下的代码。测试用例与其他模块隔离,所有对其他模块的依赖均用Mock解决。配合 Robolectric 也可以跑部分Android相关的代码测试。容易书写,容易运行。 /src/androidTest/java
下的代码。默认使用junit3的写法,使用 AndroidJUnit4
可以使用junit4的单元测试(这也是官方推荐的做法)。(之间区别可以参考junit3 vs junit4)。 可以简单用一张图来总结:
那来说说它们的各类应用场景吧。
上图中底层是各类测试用到的基础工具,用好它们能让你的测试写起来更加顺手。除了之前介绍的Mock&Verify的库之外,分别介绍一下其他的:
单元测试通常用来测试单个模块的功能,它与其他模块隔离。本地单元测试通常用来测试一些与Android无关的、纯Java的代码(比如 StringUtils
, ReflectionUtils
, ***Utils
等)。配合Android Studio能够快速地运行起单个、多个的单元测试。
Robolectric 的出现让一些仅跟UI交互的Android代码变得非常容易测试,它能够让一些仅依赖UI、对于Android设备环境没有强需求的模块(GPS、电池状态、跨App请求等)能够直接在JVM上跑起测试代码。
Espresso & UIAutomator.
Android Testing Support库
值得一提的是,Robolectric同样也用到了自定义ClassLoader,配合PowerMock使用时需要参考 Wiki 。