探索 Android TDD 开发方法
前言
1. 学习态度
过去当我遇到新知识时,我会问自己一个问题:“这个东西有很多人学吗?”,没有的话我就不学。
但是现在回想一下,这种想法实在是不太理智了,难道股神巴菲特在投资股票时,会考虑这是不是一只热门股票吗?
他不会,因为真理是掌握在少数人手里的。
我在《把时间当作朋友》中看到这么一段话:
这个世界上有两种人,一种人是不知道这个知识有什么用,所以决定不学习。
另一种人则是不知道这个知识有什么用,所以决定去学习,随着时间的推移,这两种人的认知差距会越来越大。
看了这段话后,我就调整了一下自己的心态,对于新知识,我不再抱着怀疑的态度,而是抱着学一学、试一试,说不定真的有用的态度。
这篇文章的内容不是热门知识,但是学习了这门知识后,让我的开发水平有了很大的提升,所以我想分享给大家。
2. 目标读者
如果你正在找工作,那这篇文章可能不适合你,因为这不是能让你在面试时拔高的内容。
之所以这么说,是因为就我的了解而言,大部分公司的开发者并不关心这个内容。
大部分的开发者的态度是这样的:
“什么?测试?那不是测试人员做的事情吗?质量是测试人员负责的,我只负责实现。”
当然了,这只是说我接触过的公司和人,不是所有开发者都是这样的,比如我知道的实践单元测试的公司就有 Google 和 ThoughtWorks 。
如果你已经进入工作了,并且对于开发过程中,不断返工修 Bug 的现象感到非常沮丧,那这篇文章就是为你准备的。
3. 工作经历
两年前,我在一家创业公司工作,进入那家创业公司后,觉得虽然公司只有几个人,但是他们都有大公司背景,工作态度都很积极。
尤其是后台,是个研究生,在腾讯和搜狗都做过,而且也创业过,他创业的那家公司甚至做到了上亿的流水,很牛。
当时我就在想,说不定我进入这家公司后,很快我们公司就会像新闻上的创业公司一样,一年就融到 1 个亿,三年就上市,很快我就能迎娶白富美,走上人生巅峰。
然后我就工作非常努力,加班,没问题,通宵加班,也没问题。
但实际情况是这样的,老板之前已经招过几个安卓开发了,个个都是巨坑。
上一任的 Android 开发好像在华为做过,有很多年的工作经验,听上去很厉害的样子,实际上也是个大坑。
这里说的坑,指的是开发出来的 App 有各种 Bug ,连基本的使用都有问题,更别说什么用户体验了。
这次我进来,老板期待我能带给公司带来新希望,但我带来的不是新的希望,而是新的绝望。
我开发出来的 App 各种崩溃,怎么点怎么崩,那些修好了这里崩。
当时没有测试人员,开发出来的东西直接交给老板验收,老板看了之后也快崩了。
当时我们几个人针对这个问题开了好几次会,并没有找到什么解决方案,我自己也很纠结、很痛苦,但是当时懵懂无知的我并不知道怎么办。
在挣扎了一段时间后,我离开了这家公司,离开后我就一直在想,难道这真的是软件开发的宿命吗?
难道软件开发就只能是不断修 Bug 吗?那为什么微信几乎都没有 Bug ?
后面我就围绕这些问题看了很多本书,最后发现这并不是软件开发的宿命,我遇到的问题在几十年前就已经有人遇到了,而且这个坑已经被他们填上了。
前人已经提出了很多填坑的方法,其中一个方法就是这篇文章的主题:
测试驱动开发(TDD, Test-Driven Development)
在 TDD 上 ,如果把对知识的运用分为“不知道—知道—做到—做好”这四个水平的话,我也只达到了“做到”的水平。
而写这篇文章的其中一个原因,就是希望自己能通过重新回顾、学习这些知识,让自己进一步靠近“做好”这个水平。
4. 内容概览
接下来的内容会分为 TDD 入门和 TDD 实例两个部分来讲。
-
TDD 入门
1.1 排雷
1.2 软件内部质量
1.3 TDD 周期
1.4 避免回归
1.5 小结
-
TDD 实例
2.1 基于断言的测试
2.2 被动视图模式
2.3 三个基本准则
2.4 基于交互的测试
文中的代码在文章的最下方会有 GitHub 链接。
1. TDD 简介
我是一个非常粗心、编写代码时考虑问题非常不周到的一个人。
在一次工作经历中,我遇到了一位思维比我严谨很多、写代码时考虑问题比我周到的 iOS 开发者。
当时我问了他一个问题:为什么你能想到我想不到的事情呢?
他说:这都是经验,等你项目做多了,你也能想到的。
而我想的是,怎么让一个没什么经验的人,在写代码时也能做到周全地考虑问题?而 TDD 就是这个问题的答案。
TDD 用一句话说就是:
写代码只为修复失败的测试。
有了测试作保障,我们可以逐步改进代码的结构,写出可读、可测试的代码、避免过度设计。
而且不用担心优化代码会破坏已有功能,导致软件回归。
回归,指的是软件的功能回到了以前的状态,比如一个功能本来是可以用的,一改就用不了了。
1.1 排雷
TDD 中的测试,不是指手工测试,不是指用手对着 App 点点点,而是指单元测试。
软件开发中的单元测试,和我们学校里的单元测试是非常相似的。
为什么这么说呢?
学校里的单元测试的目的,就是为了验证我们是否真的理解并记住了书上的知识。
而软件开发中的单元测试的目的,则是为了验证我们是否真的理解我们的代码。
可能大家听到这里会觉得很奇怪,什么?代码是我自己写的,我怎么可能不理解?
下面给大家看一个例子。
image可能你认为上面这个函数的执行路径是这样的。
执行路径 1.png但实际上它可能的执行路径还有另外两条。
执行路径 2.png一个这么简单的函数,都能有两个“雷”等着我们排,如果下面这个更复杂的函数,就更不用说了。
code2.png而 TDD 就是一个帮助我们思考程序可能的执行路径的方法,是一个帮助我们“排雷”的方法。
我前面说到了我之前做的应用怎么点怎么崩,为什么呢?就是因为这样那样的判断我没有加上。
刚刚提到的这两个低级错误,是大多数程序中的大多数函数都可能出现的错误。
只要我们能把这些“雷”都排掉,我们应用即使达不到零 Bug 的水平,稳定性也能有巨大的提升。
1.2 软件内部质量
虽然软件开发行业已经发展了几十年了,但是大多数开发者开发出来的软件依然有非常多的质量问题。
这里说的质量分为两种,一种是外部质量,一种是内部质量。
外部质量,指的是软件对与需求的符合程度。
内部质量,指的是软件内部代码的健壮性、可读性、可维护性。
关于提升软件外部质量的方法,会在我的下一篇讲 BDD 的文章中进行介绍讲到。
在这一篇文章,我们主要看下怎么提升内部质量。
使用 TDD ,我们要写很多小型的自动化测试,这些测试最终会形成一个有效的预警系统,防止软件出现回归的情况。
这些测试就像是烟雾报警器,能让我们在“火灾”蔓延之前进行灭火。
TDD 能让我们写出质量更高的代码,把过去用于打断点、跑断点、不断调试的时间,用在实现新功能和优化代码上。
使用 TDD 后,当测试失败时,因为只有最近写的几行代码才有可能破坏测试,所以我们能在更短的时间内找到问题。
假如运行一次程序要 2 分钟,1 个断点等待 10 秒钟,找到对应的变量又是 10 秒钟,那么断点多的时候,这个过程估计就是十几分钟,一不小心打错断点了,就要重来一次。
如果 Bug 多了,需要不断调试,那我们就不用开发新功能了。
如果我们能快速完成新功能,返工修 Bug 的次数很少,甚至是零返工,那么我们就可以花更多的时间学习新技术、优化我们的代码,形成一个良性循环。
代码质量循环图.png在软件内部质量上的主要两个问题是:缺陷多、维护难。
1. 缺陷多
软件缺陷,也就是 Bug ,会导致软件不稳定、行为不可预测、完全无法使用,甚至让软件带来的破坏远超过创造的价值。
如果一家餐厅做出来的菜里有蟑螂,那问题很大概率是在厨房,而不是在服务员身上。
但是在软件行业,当做出来的软件有 Bug 时,大家却很有可能把矛头指向测试人员(服务员),而不是开发人员(厨师)。
但问题的根源在软件的内部,软件的外部行为是由内部的一个个函数相互调用而产生的结果。
只有这一个个函数都是健壮的时候,软件的外部行为才有可能按预期工作。
那什么是健壮呢?
有的时候我会听到一些开发者说这样的话:那是后台给的数据有问题,我的应用才会闪退的。
又或者是:那是前端提交的参数有问题,才会提示异常的。
之所以他们会说这样的话,估计是并不了解软件健壮性的定义:
软件的健壮性,指的是在异常和危险情况下,系统生存的能力。
比如输入参数有误、磁盘故障、网络过载以及有意攻击等行为下,能否不死机、不崩溃。
说白了就是它可以空,你不能崩。
而建立单元测试,用各种方式给我们自己写的函数“找茬”,就可以提升这些函数的健壮性。
传统的测试方法,是在需求开发完成后,再进行黑盒测试。
黑盒测试,就是测试是在不了解软件的内部工作机制的情况下进行的,比如手工测试、使用 Selenium 等自动化测试工具测试。
黑盒测试的问题就在于有很多内部的“雷”,光靠外部的点点点是点不出来的,因为这些类往往是在数据异常的时候才会“爆炸”。
有 Bug 的软件是不能交付的,我们在寻找和修复 Bug 上投入的时间越多,也就意味着我们的开发能力越低。
比如计划用 10 天开发一个模块,结果中途遇到了非常多的 Bug ,修 Bug 花了 5 天,最后开发出来花了 15 天甚至更长的时间。
2. 维护难
只有写出可维护性高的代码,我们才能迅速响应业务需求的变化。
好的代码有很多优点,比如良好的设计、各个部分的功能和职责都是清晰的、没有冗余、重复的代码。
而 Bug 通常是由低质量的代码引起的,维护这些代码、基于这些代码进行扩展简直是一场噩梦。
比如重复的代码会让 Bug 的修复变得困难,改完一个地方,还要改其他 4、5 个甚至更多的地方。
当项目中充斥着烂代码时,我们按时交付的压力会越来越大,导致我们写出质量更差的代码,形成恶性循环。
1.3 TDD 周期
一般开发流程是:
设计—实现—(手工)测试。
而 TDD 流程是:
建立测试—编写代码—重构代码。
也就是先建立单元测试、编写实现代码让测试通过,然后再对实现进行重构优化。
1.3.1 TDD 周期
TDD 周期.png下面我们来看一下 TDD 的具体步骤。
1. 建立测试
TDD 周期也叫“红—绿—重构”,当我们做第一步建立测试时,测试会失败。
测试失败,表明系统不具备我们期望的功能,而 IDE 会用红色表示失败的测试,比如下面这样。
image第一步是建立测试而不是编写生产代码,是因为这样可以提高我们代码的可用性。
在还没有写生产代码前,我们能以用户的身份看这个函数好不好用,不用考虑这个函数好不好写。
就像是产品人员在根据需求设计功能时,可以暂时忽略技术可行性,把全部精力用在思考怎么让用户用得更爽。
又比如我们客户端开发者,对于后台提供的接口好不好用会很敏感,后台要求的参数会不会太麻烦,后台返回的字段好不好用,我们都能够快速给出反馈。
但是当面对我们自己写的代码时,我们往往会变成了当局者的身份,只考虑到怎么实现比较容易,而不是怎么实现比较好用。
有的人甚至会把自己写的代码,和自己的尊严关联在一起。
如果被测试人员找出了问题,而且很不给面子的说出来了,就感觉下不了台。
如果我们把自己的身份从当局者转变为旁观者的话,我们就能更理性、更客观地看待自己的代码,从而更好地找出实现可能存在的问题。
另外在建立测试时,我们要注意建立的测试粒度要小,要写“刚好失败”的测试,而不是一下子写出整个模块的测试,然后花几天写代码让测试通过。
一般建立一个测试需要的时间在几秒钟到几分钟之间,编写生产代码的时间也应该在这个时间区间内。
这个标准是在你熟悉如何编写测试代码后才可以参考的,在你还没熟悉前,可能你要花半天甚至更多的时间来熟悉怎么编写一个好的测试。
如果你熟悉了建立测试的方法后,你编写测试代码或生产代码的时间超过了这个区间,那说明你测试的粒度太大了, 要把测试范围和生产代码中的函数进行拆分。
2. 编写代码
而第二步编写代码,就是为了让测试从失败的状态变为通过的状态,这时 IDE 会用绿色来表示测试结果,比如下面这样。
imageTDD 的第二步是编写代码,编写“刚好能通过测试”的代码。
新建立的测试失败表明系统缺少了预期的功能,我们应该只投入几分钟就能实现这个功能,测试失败的状态不应该持续太久。
TDD 的基本理念是让测试指出下一步该做的事情,使用这种方式比在我们的代码里嵌入 // TODO 要更清晰,每一次通过测试,我们都知道工作取得了进展。
在这一步我们的目的是让测试尽快通过,所以不要投入太多时间寻找最佳实现。
等功能实现、测试通过后,我们可以再回过头来优化这个实现。
这么说的前提是功能容易实现,而且产品人员提出的功能大部分都是容易实现的。
如果遇到了一个比较难实现的功能,需要很长时间来实现,而且更换实现非常复杂,那就要先考虑好如何实现,否则返工的时间会让我们吃不消的。
3. 重构优化
而最后一步重构,也就是优化现有代码的结果,改变代码内部结构的同时,由于有测试的保护,我们不会破坏代码的外部行为。
重构本身是一件很危险的事情,如果优化代码的带来的是用户投诉,那还不如不优化。
如果有测试作保障,我们不仅能对代码进行优化,把技术债给还上,而且也不会破坏用户体验。
重构是一种用于改进设计、消除重复的开发方法,通过持续重构可以逐渐提升软件的内部质量。
重构优化是 TDD 周期的最后一步,在这一步我们回过头来审视现有的代码结果,并找出更好的实现。
使用 TDD 而不进行重构,会导致迅速产生大量的劣质代码、技术债,无论我们的测试有多充分,都还不上这些技术债。
在用测试驱动、小步前进的过程中,我们会不断扩展当前的设计,以便支持新功能的开发,也会不断抛弃旧的概念。
而这种编码方式一不小心就会变成“修修补补”,最终把代码的逻辑搞得自己也看不明白。
想了解更多重构手法的话,可以看 Martin Fowler 大神的书:《重构》。
1.4 避免回归
如果想保证从项目开发的第一天起就能交付软件,就要不断重构代码,而且还要保证重构后代码的外部行为没有被破坏。
在没有人监督、验证我们开发的软件时,我们可能会偷懒,懒得去验证自己的实现是否正确、是否有漏洞。
TDD 中测试不仅能帮助我们设计和开发软件,还可以督促我们以正确的方式进行开发,避免出现偷懒的情况。
手工测试非常麻烦,所以开发人员甚至是专业的测试人员都不喜欢做测试,会跳过某些功能的验证,自以为地认为某个功能应该没问题。
而解决这个问题的方法,就是把手工测试转化为自动化测试。
回归.png在自动化测试中,测试套件就像是一个模具,能套进去的代码就是正常、可工作的代码。
而当我们修改测试代码或生产代码,破坏了测试套件或生产代码的功能后,也就表明软件出现了“回归”的情况。
而为了测试软件是否出现了回归情况的测试,就叫回归测试。
回归测试如果是由手工来执行,会非常麻烦非常复杂,效率非常低,是开发周期中占了非常多时间的一部分工作。
如果能把这部分工作自动化,就能在减少很多测试时间的同时,提升回归测试的质量。
1.5 小结
-
使用 TDD ,通过自己给“找茬”的方式,我们能把程序中大部分的“雷”都排掉。
-
TDD 的三个周期分别是建立测试、编写代码和重构优化。
要注意的是建立的测试粒度要小,最初的实现不需要是最好的实现。
-
使用 TDD 能快速地执行回归测试。
当我们建立了测试集后,测试集就能像烟雾报警器一样为我们工作,让我们能在“火灾”发生前就把火扑灭。
2. TDD 示例
2.1 基于断言的测试
常见的两种测试方式是基于断言的测试和基于交互的测试,我们先来看基于断言的测试。
2.1.1 第一个断言
1. 建立测试
假设我们现在有这样一条业务规则:手机号必须要是 11 位的,否则要有错误提示。
我们接下来根据这条业务规则来建立一个测试。
下面是用 Kotlin ,以 MVP 的形式建立的登录页,首先建立的是 Presenter 的测试类。
image我们在这里可以看到,IDE 编译失败,因为 LoginPresenter 类不存在。
这里的 assert() 是 Kotlin 提供的一个断言方法,接收的参数是布尔值,当 assert() 方法接收到的参数为 false 时,就会报一个 AssertionError 。
可能有的朋友会觉得奇怪,为什么要把手机号的判断放在 Presenter,为什么不放在 Activity 里呢?
如果放在 Activity 的话,就意味着要运行应用或者是用 Robolectric 沙盒模拟 Android 环境。
和直接在 JVM 上运行比起来,这两种方式都比较耗时,尤其是打包、安装 APK。
而如果我们把对手机号的判断独立地放在一个函数中,我们就可以快速地验证它的有效性,快速“排雷”。
2. 通过编译
下一步就是解决这个编译时错误,建立 LoginPresenter 类。
isPhoneValid13. 运行测试
由于我们刚才并没有做真实的实现,而是直接返回 true ,所以运行测试的结果肯定是失败的。
之所以在明知会失败的情况下,还运行测试,是因为失败的测试结果是一种反馈,是有意义的。
就像是你没做过蛋炒饭,然后你想尝试一下,结果很难吃,这也是一种反馈。
有了反馈,你就可以不断地调整你的做法(实现),最终达到好吃的程度(目标)。
image2.1.2 第二个断言
当我们把 isPhoneValid() 中的返回值改为 false 后,testIsPhoneValid() 就能通过测试了。
但这还不能代表 isPhoneValid() 是有效的,我们需要用更多参数测试这个方法。
在下面可以看到,由于把返回值改为硬编码的 false ,所以现在失败的是第 29 行。
image下面我们用真实的实现替换掉原有的硬编码,替换后,测试就通过了。
image image2.1.3 质量底线
可能有的朋友看到这里会觉得好麻烦,这么多步骤,还要写这么多代码,还不如我直接在 Activity 里面判断,一下就搞定了,这么做效率太低了。
但是如果把“效率”放在更长的时间段来看的话,你就会发现直接在 Activity 里面判断的效率才是最低的。
假如项目中现在有一个函数,这个函数没有注释,命名也不清晰,写了几百行。
而以前写这段代码的人已经离开了,那现在你要以什么为依据进行重构呢?
哪怕这个函数只有几行,命名清晰,如果项目的其他地方发生了变更,影响到了这个函数,手工测试没有发现异常,结果一上线就出问题了。
紧接而来的不是客户投诉就是用户数量下降,那这样看哪种方式的效率更高呢?
还有的朋友可能遇到的情况就是老板不允许,比如老板说项目这个星期要上线,我不管你怎么弄,你只要上线就行了。
那这时候是不是就应该放弃质量呢?
在我看来不是的,因为有坑的代码上线后,有多少坑你就要背多少锅。
用户体验被破坏,意味着我们的工作从为用户创造价值,变成了给用户带来麻烦,最终是搬起石头砸自己的脚罢了。
一名有职业素养的开发人员,应该坚守质量底线,而且一家不顾产品质量的公司,是不可能有竞争力,不可能有什么长远发展的。
如果是一两次那很正常,但是如果长期是这样,就要争取跟上级沟通,说明其中的利害关系,实在不行,就应该考虑换一家公司。
当你坚守质量底线后,换来的就是一个易于维护、易于扩展的项目。
也就是接下来你不用再投入大量的时间“救火”,可以把时间用于开发新功能、学习新技术上,从而提升后续的开发效率。
2.2 被动视图模式
image在 1996 年,Taligent 公司提出了经典 MVP 模式。
在最初的设计中,View 会把接收到的用户输入事件转交给 Presenter 处理。
同时 View 还会用观察者模式监听数据的变化,当 Model 发生变化后,View 会自动更新自己的状态。
后续基于经典 MVP 模式衍生了另外两个 MVP 模式的变体:
-
监督控制者(Supervising Controller)模式
-
被动视图(Passive View)模式
其中被动视图模式是使用最多一种,而它流行的主要原因是因为可测试性强。
在被动视图模式中,View 要把用户输入事件传递给 Presenter,View 不能与数据层有关联,不需要自动更新视图,而是听 Presenter 的吩咐。
这句话是什么意思呢?就是说你不能在 Activity 中读取 SharedPreferences 、数据库,甚至连时间什么的数据也要由 Presenter 给 Activity 。
直接在 Activity 里获取 Calendar 多方便啊?为什么不能这么做?
这是因为在 TDD 中,重点测试的对象是 Presenter 中的业务逻辑。
如果把数据全都放在 Activity 了,Acitivty 自己把活都干完了,那还测什么 Presenter ,还分什么层,直接全写在 Activity 里面完事了。
如果跳过 Presenter 直接测试 Activity 的话,就意味着你想验证一个 if 语句都要经历打包、安装 APK 这个过程,测试成本会大幅提升,测试效率会大幅下降。
而且就连 Presenter 中都不能直接读写数据,而是要通过 Model 接口来操作数据。
因为真实数据是导致测试不稳定的一个重要因素,我们要把这部分用接口进行隔离,以便后续可以用 Mock(模拟)生成模拟数据。
为什么说真实数据是不稳定因素呢?
假如现在需要测试订单列表,订单列表是真实数据,这样的话每一个修改订单状态的测试,都会让下一次的测试失败。
结果就是测试的稳定性就非常差,每一次测试都要重置数据或调整测试代码。
这是我踩过的一个大坑,在刚开始实践 TDD 的时候,我写了大量的测试后面都报废了,就是因为这些测试是基于真实数据的,后续运行全都失败了。
2.3 三个基本准则
要想提升生产代码的可测试性,我们在设计代码结构时就要注意下面三个准则:
-
使用组合替代继承
-
避免静态成员和单例
-
依赖注入
1. 使用组合替代继承
使用继承的缺陷就在于,如果现在有一个抽象的基类,这个基类我们无法实例化它。
如果我们想测试这个基类的方法,就要在它的子类中进行,但如果它有几个子类,我们到底要在哪个子类中测试这些方法比较合适呢?
如果使用组合的话,我们就可以独立地测试这些方法,不用纠结这个问题。
2. 避免静态变量和单例
静态变量和单例对象会在多个地方被使用,这就意味着哪怕它们通过测试后,也不能保证后续不会出现问题。
因为在真实环境中,到底哪里还会修改它们的值有很大的不确定性。
尽量把金泰变量改为局部变,这样测试的结果会稳定很多,测试结果的可靠性也更高。
3. 依赖注入
控制反转是面向对象编程的一种设计原则,可用于降低代码之间的耦合度,控制反转的常见方式就是反射和依赖注入。
反射和依赖注入的区别就在于,依赖注入可以通过容器来创建实例,我们可以弄一个假的实例来替换真正的实现,具体有什么用下面会用一个例子来说明。
2.4 基于交互的测试
前面我们看了基于断言的测试,下面我们来看下基于交互的测试,也就是测试 Presenter 与 View 和 Model 之间的交互是否以期望的方式运行。
1. 添加依赖
使用模拟数据的优点是我们能稳定地测试 App 中的业务逻辑,但缺点就是我们无法用测试与后台联调。
实际上我遇到的大多数“联调”,不过是客户端帮后台踩坑罢了,因为后台人员不懂 TDD ,不知道满足什么条件后,接口才算是完成了。
由于完整的登录逻辑需要验证 Presenter 与 View 和 Model 之间的交互,而我们不要真实的数据和视图,所以我们需要用模拟对象来替换真实的实现。
在这里我们使用 MockK 来模拟数据,MockK 是一个 Kotlin 版的 Mockito,而且比 Mockito 要强大。
关于 MockK 用法的更多介绍,可以看我另一篇文章,这里就不详述了。
image2. 模拟响应
image上面代码中的 mockk() 方法,是用于创建模拟对象的。
view 的 mockk() 方法中,多了一个 relaxed = true ,表示该模拟对象的方法有默认返回值,不需要显式指定。
之所以不需要显式指定 View 的方法的返回值,是因为我们在这里不关心视图到底显示有没有把活干好,我们只管 Presenter 有没有叫 View 干活。
在这里只管 Presenter 有没有调用 View 的方法,而 View 也就是 UI 层的测试,会在我的下一篇文章讲。
我们现在只需要控制数据层的响应,在一般的情况下,我们只能被动地等待服务器出现正常或异常的响应,而现在我们自己来制作响应。
这样的话,不论后台返回的是什么数据,我们都能够用假数据稳定地测试 Presenter 的逻辑。
我们如果在编写代码的时,由于服务器只返回了正常的数据,导致我们只可能考虑到正常路径。
而使用模拟响应的话,我们就可以用各种乱七八糟的异常数据,验证程序的各个执行路径是否存在问题。
3. 建立测试
由于两个测试有重复的代码,所以把抽取代码抽取到一个私有函数中。
image4. 实现 Presenter
Presenter 的实现比较简单,大家看看就好了。
这里 onLogin() 之所以用 on 开头,是因为 View 只能通知 Presenter ,而不是叫 Presenter 干活,在函数的命名上也要体现这一点。
而且在这里还给 isPhoneValid() 方法加了一个 @VisibleForTesting 注解,有了这个注解,我们就可以测试这个私有函数,而 View 是无法调用这个函数的。
image5. 查看报告
每一个测试,在 AS 中代码编辑器的左侧都有一个执行按钮,点击这个按钮就可以直接执行测试。
比如下面这样的。
image除了直接执行测试以外,我们还可以通过 Gradle 任务执行全部测试,执行完毕后会生成一份测试报告。
image点击 test 运行测试任务,测试运行完成后,把目录视图从 Android 切换到 Project,并打开 app/build/reports/tests 目录。
右键 index.html ,选择用浏览器打开。
image打开后我们就能看到一份测试结果报告。
image点击包名后可以看到各个测试的测试结果和运行时间,下面是 LoginPresenterTest 的测试结果。
image结语
只有实践才能把知识转换为切切实实的价值,我是在看了不少 TDD 的资料后,才开始实践的。
实践后我才发现,TDD 不仅让开发者自己写测试,而且还要在没有生产代码之前就写测试的方式。
这是反直觉的开发方式,也是一种美妙的开发方式。