测试 "这个词最初指的是"用于测定贵金属的小容器"。这意味着测试是一种确定金或银质量的方法。它也被用于提炼有价值的合金,如锡。
后来,这个术语被其他领域采用,如今在教育、医学或软件开发等背景下很常见。然而,其本质并没有改变:测试是用来提炼最终价值的。
我们在软件开发中使用测试来确保代码按预期运行。测试可以是手动或自动的。手动测试类似于汽车制造商撞车,以验证它们是否可以安全上路。它是有效的,但它的成本太高,不能经常做,所以它通常在生产周期的最后进行。这种方法的麻烦在于,在这个阶段发现的问题可能会使产品的推出推迟几个月。

自动化的软件测试有一个完全不同的成本结构。有一个初始的反转和定期的维护,但是一旦测试自动化到位,我们可以根据需要经常运行我们的测试--一分钱。

有了测试自动化,开发人员可以得到持续的反馈,使他们能够在生产周期的早期发现问题。快速迭代的结果是改进设计、提高质量和更安全的启动。
测试自动化的原则
整本书都是关于测试自动化的主题而写的。这是每个开发人员都需要在某个时候掌握的技能,而且最好是早做而不是晚做。
这里有六条原则来缓解学习曲线:
- 测试应该提高质量
- 测试应该减少引入失败的风险
- 测试有助于理解代码
- 测试必须易于编写
- 测试套件必须易于运行
- 一个测试套件应该需要最少的维护
原则1:测试自动化提高质量
质量是一个难以捉摸的概念。尽管我们可以尝试,但不可能用数字来定义它。然而,当我们看到它时,我们知道它。软件行业提出了许多衡量质量的指标:缺陷数量、代码覆盖率、CI错误率、测试失败率,等等。每一个指标都抓住了质量概念的某些方面。
自动测试通过持续运行成百上千个测试来提高质量指标;在缺陷进入生产之前发现缺陷,通知开发人员潜在的问题,并检查系统是否偏离了用户的期望。

Semaphore的测试报告显示了项目状态的高层视图
撇开指标不谈,我们知道坚实的设计是质量的先决条件。当测试驱动开发时,开发人员可以很容易地尝试不同的想法,并确定哪一个最有效。这一特点已经被测试驱动开发(TDD)和行为驱动开发(BDD)等实践所利用,取得了巨大的成功。
原则2:测试自动化降低风险
代码审查和同行编程,尽管是必要的和富有成效的,但不能依靠它来发现错误。经验表明,更多的眼球并不能转化为更少的错误。
可靠地发现错误的唯一方法是建立一个全面的自动化测试套件。测试可以从上到下检查整个应用程序。它们能在错误造成任何伤害之前捕捉到它们,发现回归,并在各种设备和环境中运行应用程序,否则手动测试的成本太高。
即使团队中的每个人都是一个非常聪明的开发者,不知何故从未犯过错误,第三方的依赖关系仍然会引入错误并带来风险。自动测试可以扫描项目中的每一行代码,查找错误和安全问题。

Trivy扫描项目中的安全问题。
原则3:测试帮助你了解系统
太频繁了,开发人员回到几天前才写好的代码,却发现他们已经完全忘记了它是如何工作的。当开发者不得不处理其他人写的代码时,这种情况就更糟糕了。
通常情况下,阅读测试是了解一个系统的最好的地方,因为它们通过实例展示了事情是如何运作的。因此,当有疑问时,开发人员可以参考测试套件。
例如,测试可以向另一个开发者展示一个API应该如何响应,使他们可以跳过看文档。
ctx := context.Background()
result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts {
Name: "test",
ServerType: &ServerType{ID: 1},
Image: &Image{ID: 2},
SSHKeys: []*SSHKey{
{ID: 1},
{ID: 2},
},
})
if err != nil {
t.Fatalf("Server.Create failed: %s", err)
}
if result.Server == nil {
t.Fatal("no server")
}
if result.Server.ID != 1 {
t.Errorf("unexpected server ID: %v", result.Server.ID)
}
if result.RootPassword != "" {
t.Errorf("expected no root password, got: %v", result.RootPassword)
}
if len(result.NextActions) != 1 || result.NextActions[0].ID != 2 {
t.Errorf("unexpected next actions: %v", result.NextActions)
}
不确定某一行代码是否有必要?把它注释出来,看看哪个测试失败。有一个改进功能的想法?需要重构一段代码?试试吧,然后运行自动测试。你会惊讶地发现,你可以从一个系统的测试中了解到很多东西。
原则4:自动化测试应该容易编写
有些测试是以手工测试开始的,并在以后得到自动化。但是,更多的时候,这导致了过度复杂、缓慢和笨拙的测试。当测试和代码有一定的协同作用时,会有最好的结果。编写测试的行为促使开发人员产生更多的模块化代码,这反过来又使测试更简单、更细化。
测试的简单性很重要,因为为测试而写测试是不现实的。代码也应该是直截了当的读和写。否则,我们有可能引入测试本身的故障,导致假阳性和松散性。
许多测试框架使用领域专用语言(DSL),以简单的英语定义测试。也许最明显的例子是Gherkin,Cucumber测试框架使用的语言:
Feature: Is it Friday yet?Everybody wants to know when it's FridayScenario: Sunday isn't Friday Given today is Sunday When I ask whether it's Friday yet Then I should be told "Nope"
总而言之,在编写测试时,坚持几个基本原则是个好主意:
- 每个测试只写一个断言
- 保持代码与测试分离,即生产代码不应包括测试
- 保持测试相互独立,因为依赖关系可以迅速地滚雪球,变成令人头痛的混乱
- 将测试重叠保持在最低限度,也就是说,不要对相同的代码进行两次测试
- 不要破坏被测试代码的封装。因此,只测试外部接口
原则5:测试应易于运行
如果开发人员需要打开一个检查表以开始测试运行,你的测试就不会像它们应该的那样经常运行。
理想情况下,每次代码改变时,测试都会运行**,不需要任何干预**。我们在这里很幸运,因为开发者工具是相当复杂的。大多数现代IDE可以检测到文件的变化并自动启动测试套件,同样可以通过命令行程序实现,如nodemon、live reload、fswatch或testmon。

图示:VS Code在后台运行测试
为了使测试易于运行,必须满足一些条件:
- Idempotency:测试不应该有副作用。副作用包括写到文件,保存到数据库,或一般改变数据。开发人员应该能够安全地运行相同的测试的任何次数。
- 确定性:在相同的输入下,测试应该总是给出相同的结果。当测试需要开发者无法控制的外部数据时,如日期/时间或来自API的响应,这些应该用mock或stubs进行伪造。
- 独立:测试应该是相互独立的,而且开发人员必须能够以任何顺序运行它们。
- 轻量级:测试必须足够轻量级,以便在合理的时间内在开发者的机器上运行。
- 粒度:开发人员必须能够零散地运行测试套件。
在开发人员的机器上运行测试只是等式的一部分。测试也必须在你的持续集成管道中进行。你的CI/CD管道就像一个质量门;它在每次提交时运行测试套件,提供即时反馈,并允许开发人员检测何时引入了故障:

原则6:自动化测试套件应该要求低维护性
最后一条原则是前五条的推论。也就是说,如果你很好地履行其他原则,你就可以免费得到它。不过,它还是很重要的,所以最好能把它说出来。
开发人员希望做有创造性和有价值的工作。自动化让机器来处理测试的苦差事。当测试很容易写并且经常被执行时,就会产生一个积极的反馈回路。开发人员倾向于欣赏自动化如何使他们的生活更轻松,因此,激励他们编写和维护测试。
当然,需要一些定期的维护来保持你的测试处于良好状态。下面是编写和维护测试套件的四个建议:
- 编写足够多的测试,使之有效(但不多)。如果错误不断,你需要更多的测试。反之,如果你发现测试在小的变化中出现故障,你需要删除一些测试。
- 选择最适合情况的测试类型,单元测试是快速和激光聚焦的,而端到端测试涵盖了UI,是沉重和更全面的。遵循测试金字塔的测试套件有健康的各种测试。

测试金字塔
- 保持测试的可靠性:当代码正确时,测试失败被称为假阳性。有时没有明显原因而失败的测试被称为片状测试。这两种情况在测试套件中都会造成问题,因为它们是巨大的时间浪费者和挫折的来源。
- 保持测试快速:一个缓慢的测试套件会给开发带来障碍。
结论
那些认为测试很贵的人并没有充分认识到质量差的代价。单独来看,错误和缺陷对产品价值的影响可能很难衡量,但如果不加以解决,会很快失去控制。幸运的是,你可以通过建立和完善你的自动化测试套件来防止这种情况,作为一个伟大的开发者体验和杰出的高质量软件的基础。