文档结构  
可译网翻译有奖活动正在进行中,查看详情 现在前往 注册?
原作者:Nelson Elhage    来源:blog.nelhage.com [英文]
班纳睿    计算机    2017-01-09    0评/577阅
翻译进度:67%   参与翻译: 班纳睿 (10)

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

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

第 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 积分)

My favorite example of this technique is David Benjamin’s DER ASCII project. David is half of the BoringSSL project, and as such spends a lot of time dealing with X.509 certificates and messages, which are encoded in ASN.1 DER, a moderately gnarly binary format. He wrote DER ASCII to provide a simple textual representation of these structured binary strings, allowing him to represent, generate, and modify them for testing purposes much more easily than he otherwise could.

Design a testing minilanguage or “zoo”

Often, it’s worth going one step forward and building a completely data-driven test harness that lets you express common types of test cases entirely as data files in a directory, without adding any additional code. Done right, this lets you add new test cases directly in a familiar format, and even directly translate bug reports or interactive tests into test cases.

第 11 段(可获 1.85 积分)

(A coworker calls this pattern a “zoo”, because you get one directory containing all of your exotic animals, and I’ve adopted that nickname)

This technique is well-known among compiler developers (probably because compilers have the luxury of a very clear input format: simple text files); Take a look at a sample gcc test case. You needn’t read it in detail, but skim over it, and note some key techniques and details:

  • This is a self-contained test case. Someone created this test by adding this file, which is in a format very familiar to any GCC developer (C source), and by doing no additional work (e.g. there’s no secondary list of these, or configuration)
  • Note the {dg-* …} comments. In order to make these tests self-contained, and able to express many varieties of tests (correctness, error messages, warnings, etc), GCC developers have built a custom annotation/directive system (what I call a “testing minilanguage”) to instruct the test harness how to interpret this test case, and what to check for.
第 12 段(可获 2.14 积分)

Building a runner for these test cases is a bit of upfront investment, but it’s well worth it; By making it easier to write tests, you’ll ensure that you and other contributors write more tests, and your future selves (and other future developers!) will thank you for the effort.

This technique is easiest for programs with simple self-contained textual inputs (such as compilers), but it can be adopted more widely. At my work, I worked on an internal HTTP proxy application; We built out a test harness so that we could ultimately paste entire HTTP requests and the expected responses into files, and perform automated testing by replaying and comparing those. This required stubbing out various pieces of entropy and having a “standard” well-known initial database state for these tests, but the end result was very easy regression testing – most bugs we found could be encoded as regression tests by just copy/pasting from logs and/or tcpdump with some slight modification.

第 13 段(可获 2.04 积分)

Regression tests, Regression tests, Regression tests

My final, and perhaps more important, advice is to always write regression tests. Encode every single bug you find as a test, to ensure that you’ll notice if you ever encounter it again.

This doesn’t have to just mean “bugs reported by a user” or anything similar; Any bug you find in the course of development is worth writing a test for. Even if you find yourself half way into typing a line of code, realize you’ve made a mistake, and backspace half the line: Pause, and ask yourself “If I had made that mistake, is there a test I could have written to demonstrate it?”

第 14 段(可获 1.44 积分)

The goal is, essentially, to ensure a ratchet on the types of possible bugs in your system. There are probably nigh-infinitely many bugs you could write, but the set of desired behaviors of your system is relatively finite, and the set of bugs you’re likely to write is similarly limited, and if you turn every bug into a test on one of those behaviors, you’ll eventually converge on testing for most of the important failure modes of your system.

At least, that’s the optimistic take. Even if it doesn’t converge, though, there’s a lot to be said for only having any specific bug once, ever.

第 15 段(可获 1.38 积分)

文章评论