文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

作为一个软件工程师,随着工作时间的增长,我对测试变得越来越着迷。我完全赞同将遗留代码定义为“没有经过自动测试套件测试的代码”。我坚信在一个测试套件里想要获得快速的进步,你最好是为测试做好设计,并且有一个快速、可靠且全面的测试套件。

但尽管如此, 我从来没有真正同意那些我遇到过的测试驱动开发的宣言或者实践。我并不笃信写代码前要先写测试,或者发誓要遵从任何特定的测试结构。

第 1 段(可获 1.26 积分)

然而最近这些天我确实尝试写了很多测试,并且我也确实在写代码的时候会考虑测试。我也逐渐意识到我的方式可以被认为是测试驱动开发。下面我将谈谈我是如何思考代码和测试的,特别是在全新的系统或组件里的。

每次只实现一个组件

(我在之前的文章里讨论了关于这一点的很多想法)

当我要研究如何构建一个新项目时,我会考虑如何将之架构成由不同的组件组成,是受到一个问题的启发,即“该如何设计可测的单元测试并使之尽可能少的依赖其他东西?”

第 2 段(可获 1.35 积分)

当我开始着手接手一个模块时,我会先尝试为那个模块里的代码编写测试代码然后才继续下去。

我很少在代码之前编写测试,除非是最简单的情形下。通常我只有在开始工作并尝试了一些方法之后,才会知道我要编写的实现和接口的细节。 如果书写测试(它们自己是接口的客户端) 能够让我感受到这些接口的存在,我可能会这么做,但是这当然并不是一个规则。

然而,我确实也会尝试同时编写模块代码和测试,同时 – 尽可能的 – 只有完成了它们之后才会开始另一个模块。 这种做法有助于确保我的模块是可测试的(而且能与系统的其他部分做到最低的耦合度),同时也确保了持续的测试覆盖率。

第 3 段(可获 1.76 积分)

这条规则(而且事实上,我在这里列出的所有规则)对我来说并不是硬性的规则。我有时候也会偷懒。但是我发现这个方法对我很有启发。

避免运行main方法

当开发一个新项目或者添加新功能时,人们总是很本能的去手动测试,也就是运行二进制文件,然后手动去测试新功能。

我极力抗拒这种冲动。

相反的,当我刚添加一个新功能的时候,如果我发现自己很手痒想要手动去尝试的话,我就会学着去识别这种冲动,暂停一下并且退后一步,然后写一个自动化测试,来实现那些我想通过手动执行的任何行为。 

第 4 段(可获 1.53 积分)

手工测试通常来说比较快速简单,而且容易满足要求 – 你可以直接测试你的应用,有的人可以立马就在你的屏幕上看到结果,而有的人则可以同应用进行“真正的”的交互,而不像在单元测试的脚本或模拟情况下,有时候会显得很尴尬。这是非常自然的本能。

然而,它也是在很大程度上白白浪费了精力!一个手动的测试只能验证代码库的当前状态。一旦你做了一个变更,那就会将之前的结果变成无效的了。不过,如果你能花点精力,将测试以代码的形式编码成一个自动化测试的话,那么它在将来会一直是有效的。

第 5 段(可获 1.34 积分)

用于测试的构建工具

当然,通常来说手工测试你的应用要比写一个测试来说要容易多了。动手点击总是要比试图协调推理来的简单得多。点击进入一个输入框并得到不断交互的反馈,比起努力思索编写一大段脚本来说要容易些。

如果用代码测试你的应用有些困难或者很令你感到沮丧,那么花点精力去投资引入一款能使这变得容易一些的并适合你的测试工具是很值得的!测试代码是你代码库中很重要的一个部分,而你也不会仅仅因为他们实现起来有难度而错过一些重要的功能(的测试)!

第 6 段(可获 1.21 积分)

以下是我发现并用于日常工作中的一些具体的技术,可以使得编写测试代码更加轻松且更高效。

编写大量的fakes

似乎没有人能在“fakes”,“mocks”, “stubs”,以及其他一些不同的测试概念之前达成一致。 但我将会采用“fake”这个单词,它是我从一个很棒的PyCon演讲里学来的,这是一个可以在多个测试间重用的组件接口的独立实现,而且它还是可以独立测试的。(顺便说下,那是个不错的演讲,他对我的思维观念影响很大。值得一看。)

第 7 段(可获 1.31 积分)

在某些情况下,一个fake可能是一个将要完成的基于内存的后端实现。比如说,如果你的应用依赖于S3,你就可能实现一个基于内存的blob存储,让它来实现你的内部S3的接口,然后将它用于你的所有测试中。再举另外一个具体的例子,在测试中时Django会将它内部的email的实现替换成一个基于内存的email发件箱,可以让你在不修改代码的情况下运行它,然后再观察其输出。

如果你的接口过于复杂,你还可以提供一个“可编程的”fake来让测试以适当的方式配置或者插入各种行为,教它学会如何对各种输入进行响应。这样的fake可以通过结构化测试和对接口实现的较低级别的辅助性的细节的分享,来提供有用的价值。

第 8 段(可获 1.53 积分)

Go语言的Kafka客户端“sarama”实现了一个可编程的fake,叫做kafka broker,它在该类库自身的测试中被重复使用,并且也可以被希望构造更多端到端测试的消费者所使用。 

无论什么时候,当你想要在特设基础上关闭外部应用的时候,你都该退一步想想,并问问自己你是否应该写一个可以重用的伪实现来代替。这会花费不少额外的工作,但是这会将降低编写未来写测试的成本,并增加你的测试的可维护性,两个都是很重要的目标。

为测试的输入输出设计简化格式

通常在写测试时,你会想构造一个领域对象或者带有特定属性的某种输入到你的系统中 – 一定的内容、模式,或者其他什么。往往很容易发现你写一长串的代码来构造对象,或者复制了不透明的内部数据结构的的表述方式。

第 9 段(可获 1.81 积分)

利用强大的技术可以设计并实现一种用来展示数据(内部状态对象或结构化输入)的可以别轻易读写的小文本格式。然后增加你的测试和应用,以便于你可以利用这种格式很轻松的输入或打印对象。现在,当你需要一个特定形状的对象时,你就可以使用这种简单的文本展示方式,根据你的需要对它进行编辑,并从这里开始你的测试。

你可以经常使用这种格式来使手工测试变得更容易重现。手动构造一些特定的输入或者内部状态,然后配置应用将它存储成微型格式(miniformat)。现在你就可以轻易的将它编码放进测试里面,并可以在任何地方进行回放。

第 10 段(可获 1.61 积分)

我最喜欢的应用这个技术的例子是David Benjamin的DER ASCII项目。David参与过BoringSSL项目的一半的工作,在该项目中他花了大量时间与X.509证书以及消息打交道,这种证书是以一种中等粗糙的二进制格式 ASN.1 DER编码的。他编写了DER ASCII用来展示这些结构化的二进制字符串的简单的文本格式,使他在测试时能够更轻易的展示,生成并修改它们。

设计一个测试指令语言或“动物园”

通常情况下,很有必要往前走一步,来构建一个完全由数据驱动的测试工具,可以使你在不用加入任何额外代码的情况下,就能用一个文件夹的数据文件来表达常见类型的测试用例。如果正确书写的话,你可以直接使用一种熟悉的文件格式来添加新的单元测试,还甚至可以直接将bug报告或交互测试翻译成单元测试。

第 11 段(可获 1.85 积分)

(我有个同事称这种模式为“动物园”,因为我们有一个包含了所有奇异动物的目录,因此我就接受了这个昵称。)

这种技术对于编译器开发人员来说是很熟悉的(大概是因为编译器都有比较奢侈的比较清晰的输入格式:简单文本文件);我们可以看看一个简单的gcc的测试用例。你不需要仔细阅读,只是大概浏览下即可,注意一些关键的技术和细节:

  • 这是一个自包含的测试用例。某些人通过添加这个文件创建了这个测试,这个文件的格式与GCC开发人员(C源代码)的格式很相似,而且不需要额外的工作(比如没有二级列表,或者配置)
  • 注意{dg-* …} 注释。为了能使这些测试变成自包含的,并且可以表达很多类型的测试(正确,错误消息,警告等),GCC开发人员已经构建了一个可定制的注解/指令系统(我称之为“测试指令语言”)来执导测试工具如何解释这种测试用例,以及该检查什么。
第 12 段(可获 2.14 积分)

为这些测试用例构建一个运行器(runner)是有点前期投入的意味,但是是很值得的;如果写测试能够更为方便,这将会使得你和其他贡献者编写更多的测试,而你以后的自我(以及其他将来的开发人员!)也会感谢你作出了这些努力。

这项技术对于拥有简单的自包含的问自行输入(比如编译器)来说是最简单的,但是它可以在更大范围内试用。在我的工作中,我参与维护一个内部的HTTP代理应用;我们构建出了一个测试工具以便于我们最终可以将整个HTTP请求以及期望的响应结果存到文件中,然后通过替换和比较他们来执行自动化测试。这需要删除一些各种各样的熵片段并且为这些测试准备一个“标准的”并为大多数人所知的初始化数据库状态,但是最终的结果缺失非常简单的回归测试 – 我们发现的大多数bug都可以通过从日志文件或tcpdump中复制/粘贴并进行简单的修改,就可以编码成回归测试。

第 13 段(可获 2.04 积分)

回归测试,回归测试,还是回归测试

我的最后一个也许是更重要的建议是,总是要写回归测试。将每一个bug都用测试来实现,可以保证如果你再次遇到它时能够注意到。

这并不一定就意味着“由用户报告的bug”或其他类似的任何东西;在开发过程中你发现的任何bug都值得为它写一个测试。即使你在写代码写到一半时,突然意识到你犯了错误,而你删掉了半行代码:停下来,问下你自己“如果你犯了那个错误,我是否可以写一个测试来证明它”?

第 14 段(可获 1.44 积分)

这样做的目标本质上是为了确保对你的系统里可能出现的bug类型做一个全面的了解(ratchet)。你可能会写出几乎无限多的bug,但是你系统的期望行为的集合却是相对有限的,而你可能会写出的bug的集合同样也是有限的,如果你将每一个bug针对每一种行为都转换成一个测试,最终在测试收敛以后你就可以得到你系统里大部分重要的失败的模式。

至少这是比较乐观的看法。即使它不会收敛,很多情况下每一个特定的bug只会出现一次。

第 15 段(可获 1.38 积分)

文章评论