探索 Android TDD 开发方法

Python实战 群

Java实战 群

长按识别下方二维码,按需求添加

扫码关注添加客服

进Java 群

来自丨掘金

https://juejin.im/post/5e32fae3f265da3e12182ce7

前言

1、学习态度

过去当我遇到新知识时,我会问自己一个问题:“这个东西有很多人学吗没有的话我就不学。

但是现在回想一下,这种想法实在是不太理智了,难道股神巴菲特在投资股票时,会考虑这是不是一只热门股票吗p>

他不会,因为真理是掌握在少数人手里的。

我在《把时间当作朋友》中看到这么一个道理:

这个世界上有两种人,一种人是不知道这个知识有什么用,所以决定不学习。

另一种人则是不知道这个知识有什么用,所以决定去学习,随着时间的推移,两者的认知差距会越来越大。

看了这段话后,我就调整了一下自己的心态,对于新知识,我不再抱着怀疑的态度,而是抱着学一学、试一试,说不定真的有用的态度。

这篇文章的内容不是热门知识,但是学习了这门知识后,让我的开发水平有了很大的提升,所以我想分享给大家。

2、目标读者

如果你正在找工作,那这篇文章可能不适合你,因为这不是能让你在面试时拔高的内容。

之所以这么说,是因为就我的了解而言,大部分公司的开发者并不关心这个内容。

大部分的开发者的态度是这样的:

“什么是测试人员做的事情吗是测试人员负责的,我只负责实现。”

当然了,这只是说我接触过的公司和人,不是所有开发者都是这样的,比如我知道的实践单元测试的公司就有 Google 和 ThoughtWorks 。

有很多需求的实现的确是很有挑战的,比如抖音的各种特效。

但是大部分需求的实现是很简单的,所以在大多数时候我们都要考虑如何提升实现的质量。

如果你已经进入工作了,并且对于开发过程中,不断返工修 Bug 的现象感到非常沮丧,那这篇文章就是为你准备的。

3、工作经历

两年前,我在一家创业公司工作,进入那家创业公司后,觉得虽然公司只有几个人,但是他们都有大公司背景,工作态度都很积极。

尤其是后台,是个研究生,在腾讯和搜狗都做过,而且也创业过,他参加创业的那家公司,甚至做到了上亿的流水,很牛。

当时我就在想,说不定我进入这家公司后,很快我们公司就会像新闻上的创业公司一样,一年就融到 1 个亿,三年就上市,很快我就能迎娶白富美,走上人生巅峰。

然后我就工作非常努力,加班,没问题,通宵加班,也没问题。

但实际情况是这样的,老板之前已经招过几个安卓开发了,个个都是巨坑。

上一任的 Android 开发好像在华为做过,有很多年的工作经验,听上去很厉害的样子,实际上也是个大坑。

这里说的坑,指的是开发出来的 App 有各种 Bug ,连基本的使用都有问题,更别说什么用户体验了。

这次我进来,老板期待我能带给公司带来新希望,但我带来的不是新的希望,而是新的绝望。

我开发出来的 App 各种崩溃,怎么点怎么崩,那些修好了这里崩。

当时没有测试人员,开发出来的东西直接交给老板验收,老板看了之后也快崩了。

当时我们几个人针对这个问题开了好几次会,并没有找到什么解决方案,我自己也很纠结、很痛苦,但是当时懵懂无知的我并不知道怎么办。

在挣扎了一段时间后,我离开了这家公司,离开后我就一直在想,难道这真的是软件开发的宿命吗p>

难道软件开发就只能是不断修 Bug 吗什么微信几乎都没有 Bug p>

后面我就围绕这些问题看了很多本书,最后发现这并不是软件开发的宿命,我遇到的问题在几十年前就已经有人遇到了,而且这个坑已经被他们填上了。

前人已经提出了很多填坑的方法,其中一个方法就是这篇文章的主题:

测试驱动开发(TDD, Test-Driven Development)

如果把对知识的运用分为“不知道—知道—做到—做好”这四个水平的话,那么我在 TDD 上 ,也只达到了“做到”的水平。

而写这篇文章的其中一个原因,就是希望自己能通过重新回顾、学习这些知识,让自己进一步靠近“做好”这个水平。

除了找到返工问题的答案以外,这段工作经历还让我明白了另一个道理:

不要老想着我需要什么,多想下我能提供什么。

4. 内容概览

接下来的内容会分为 TDD 入门和 TDD 示例两个部分来讲。

  1. TDD 入门

    1.1 排雷

    1.2 软件内部质量

    1.3 TDD 周期

    1.4 避免回归

    1.5 小结

  2. TDD 示例

    2.1 基于断言的测试

    2.2 被动视图模式

    2.3 三个基本准则

    2.4 基于交互的测试

文中的代码在文章的最下方会有 GitHub 链接。

一、TDD简介

我是一个非常粗心、编写代码时考虑问题非常不周到的一个人。

在一次工作经历中,我遇到了一位思维比我严谨很多的 iOS 开发者。

当时我问了他一个问题:为什么你能想到我想不到的事情呢p>

他说:这都是经验,等你项目做多了,你也能想到的。

而我想的是,怎么让一个没什么经验的人,在写代码时也能做到周全地考虑问题TDD 就是这个问题的答案。

TDD 用一句话说就是:

写代码只为修复失败的测试。

有了测试作保障,我们可以逐步改进代码的结构,写出可读、可测试的代码、避免过度设计。

而且不用担心优化代码会破坏已有功能,导致软件回归。

回归,指的是软件的功能回到了以前的状态,比如一个功能本来是可以用的,一改就用不了了。

1.1 排雷

TDD 中的测试,不是指手工测试,不是指用手对着 App 点点点,而是指单元测试。

软件开发中的单元测试,和我们学校里的单元测试是非常相似的。

为什么这么说呢p>

学校里的单元测试的目的,就是为了验证我们是否真的理解并记住了书上的知识。

而软件开发中的单元测试的目的,则是为了验证我们是否真的理解我们的代码。

可能大家听到这里会觉得很奇怪,什么是我自己写的,我怎么可能不理解p>

下面给大家看一个例子。

但实际上它可能的执行路径还有另外两条。

在软件内部质量上的主要两个问题是:缺陷多、维护难。

1、缺陷多

软件缺陷,也就是 Bug ,会导致软件不稳定、行为不可预测、完全无法使用,甚至让软件带来的破坏远超过创造的价值。

如果一家餐厅做出来的菜里有蟑螂,那问题很大概率是在厨房,而不是在服务员身上。

但是在软件行业,当做出来的软件有 Bug 时,大家却很有可能把矛头指向测试人员(服务员),而不是开发人员(厨师)。

但问题的根源在软件的内部,软件的外部行为是由内部的一个个函数相互调用而产生的结果。

只有这一个个函数都是健壮的时候,软件的外部行为才有可能按预期工作。

那什么是健壮呢p>

有的时候我会听到一些开发者说这样的话:那是后台给的数据有问题,我的应用才会闪退的。

又或者是:那是前端提交的参数有问题,才会提示异常的。

之所以他们会说这样的话,估计是并不了解软件健壮性的定义:

软件的健壮性,指的是在异常和危险情况下,系统生存的能力。

比如输入参数有误、磁盘故障、 络过载以及有意攻击等行为下,能否不死机、不崩溃。

说白了就是它可以空,你不能崩。

而建立单元测试,用各种方式给我们自己写的函数“找茬”,就可以提升这些函数的健壮性。

传统的测试方法,是在需求开发完成后,再进行黑盒测试。

黑盒测试,就是测试是在不了解软件的内部工作机制的情况下进行的,比如手工测试、使用 Selenium 等自动化测试工具测试。

黑盒测试的问题就在于有很多内部的“雷”,光靠外部的点点点是点不出来的,因为这些类往往是在数据异常的时候才会“爆炸”。

有 Bug 的软件是不能交付的,我们在寻找和修复 Bug 上投入的时间越多,也就意味着我们的开发能力越低。

比如计划用 10 天开发一个模块,结果中途遇到了非常多的 Bug ,修 Bug 花了 5 天,最后开发出来花了 15 天甚至更长的时间。

2、维护难

只有写出可维护性高的代码,我们才能迅速响应业务需求的变化。

好的代码有很多优点,比如良好的设计、各个部分的功能和职责都是清晰的、没有冗余、重复的代码。

而 Bug 通常是由低质量的代码引起的,维护这些代码、基于这些代码进行扩展简直是一场噩梦。

比如重复的代码会让 Bug 的修复变得困难,改完一个地方,还要改其他 4、5 个甚至更多的地方。

当项目中充斥着烂代码时,我们按时交付的压力会越来越大,导致我们写出质量更差的代码,形成恶性循环。

1.3 TDD 周期

一般开发流程是:

设计—实现—(手工)测试。

而 TDD 流程是:

建立测试—编写代码—重构代码。

也就是先建立单元测试、编写实现代码让测试通过,然后再对实现进行重构优化。

1.3.1 TDD 周期

第一步是建立测试而不是编写生产代码,是因为这样可以提高我们代码的可用性。

在还没有写生产代码前,我们能以用户的身份看这个函数好不好用,不用考虑这个函数好不好写。

就像是产品人员在根据需求设计功能时,可以暂时忽略技术可行性,把全部精力用在思考怎么让用户用得更爽。

又比如我们客户端开发者,对于后台提供的接口好不好用会很敏感,后台要求的参数会不会太麻烦,后台返回的字段好不好用,我们都能够快速给出反馈。

但是当面对我们自己写的代码时,我们往往会变成了当局者的身份,只考虑到怎么实现比较容易,而不是怎么实现比较好用。

有的人甚至会把自己写的代码,和自己的尊严关联在一起。

如果被测试人员找出了问题,而且很不给面子的说出来了,就感觉下不了台。

如果我们把自己的身份从当局者转变为旁观者的话,我们就能更理性、更客观地看待自己的代码,从而更好地找出实现可能存在的问题。

另外在建立测试时,我们要注意建立的测试粒度要小,要写“刚好失败”的测试。

而不是一下子写出整个模块的测试,然后花几天写代码让测试通过。

当你熟悉建立测试的方法后,一般建立一个测试需要的时间在几秒钟到几分钟之间,编写生产代码的时间也应该在这个时间区间内。

如果我们编写测试代码或生产代码的时间超过了这个区间,那说明测试的粒度太大了, 要把测试范围和生产代码中的函数进行拆分。

2、编写代码

而第二步编写代码,就是为了让测试从失败的状态变为通过的状态,这时 IDE 会用绿色来表示测试结果,比如下面这样。

在自动化测试中,测试套件就像是一个模具,能套进去的代码就是正常、可工作的代码。

而当我们修改测试代码或生产代码,破坏了测试套件或生产代码的功能后,也就表明软件出现了“回归”的情况。

而为了测试软件是否出现了回归情况的测试,就叫回归测试。

回归测试如果是由手工来执行,会非常麻烦非常复杂,效率非常低,是开发周期中占了非常多时间的一部分工作。

如果能把这部分工作自动化,就能在减少很多测试时间的同时,提升回归测试的质量。

1.5 小结

  1. 使用 TDD ,通过自己给“找茬”的方式,我们能把程序中大部分的“雷”都排掉。

  2. TDD 的三个周期分别是建立测试、编写代码和重构优化。

    要注意的是建立的测试粒度要小,最初的实现不需要是最好的实现。

  3. 使用 TDD 能快速地执行回归测试。

    当我们建立了测试集后,测试集就能像烟雾 警器一样为我们工作,让我们能在“火灾”发生前就把火扑灭。

二、TDD示例

2.1 基于断言的测试

常见的两种测试方式是基于断言的测试和基于交互的测试,我们先来看基于断言的测试。

2.1.1 第一个断言

1、建立测试

假设我们现在有这样一条业务规则:手机 必须要是 11 位的,否则要有错误提示。

我们接下来根据这条业务规则来建立一个测试。

下面是用 Kotlin ,以 MVP 的形式建立的登录页,首先建立的是 Presenter 的测试类。

3、运行测试

由于我们刚才并没有做真实的实现,而是直接返回 true ,所以运行测试的结果肯定是失败的。

之所以在明知会失败的情况下,还运行测试,是因为失败的测试结果是一种反馈,是有意义的。

就像是你没做过蛋炒饭,然后你想尝试一下,结果很难吃,这也是一种反馈。

有了反馈,你就可以不断地调整你的做法(实现),最终达到好吃的程度(目标)。

下面我们用真实的实现替换掉原有的硬编码,替换后,测试就通过了。

2.1.3 质量底线

有的朋友可能遇到的情况是时间上不允许做这件事,比如上级说项目这个星期要上线,我不管你怎么弄,你只要上线就行了。

那这时候是不是就应该放弃质量呢p>

在我看来不是的,因为有坑的代码上线后,有多少坑你就要背多少锅。

用户体验被破坏,意味着我们的工作从为用户创造价值,变成了给用户带来麻烦。

一名有职业素养的开发人员,应该坚守质量底线,而且一家不顾产品质量的公司,是不可能有竞争力,不可能有什么长远发展的。

如果是一两次那很正常,但是如果长期是这样,首先要争取跟上级沟通,说明其中的利害关系。

实在不行,就应该考虑换一家公司。

当你坚守质量底线后,换来的就是一个易于维护、易于扩展的项目。

也就是接下来你不用再投入大量的时间“救火”,可以把时间用于开发新功能、学习新技术上,从而提升后续的开发效率。

2.2 被动视图模式

2、模拟响应

4、实现 Presenter

下面的代码是 LoginPresenter 中的实现,这里的实现比较简单,大家看看就好了。

这里 onLogin() 之所以用 on 开头,是因为 View 只能通知 Presenter ,而不是叫 Presenter 干活,在函数的命名上也要体现这一点。

而且在这里还给 isPhoneValid() 方法加了一个 @VisibleForTesting 注解,有了这个注解,我们就可以测试这个私有函数,而 View 是无法调用这个函数的。

点击 test 运行测试任务,测试运行完成后,把目录视图从 Android 切换到 Project,并打开 app/build/reports/tests 目录。

右键 index.html ,选择用浏览器打开。

点击包名后可以看到各个测试的测试结果和运行时间,下面是 LoginPresenterTest 的测试结果。

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2020年3月25日
下一篇 2020年3月25日

相关推荐