测试原则

学习目标

  • 测试的目标及如何实现这些目标

  • 在团队中建立定期测试的实践

  • 按代码接近程度分类测试

  • 按代码内部知识分类测试

在实际介绍(如何测试一个功能)和核心概念的基本讨论(测试能够实现什么,哪种类型的测试有益等)之间存在一个差距。在深入学习教程之前,我们需要对测试的一些基本概念进行反思。

一个好的测试的特点

在编写测试时,你需要牢记测试的目标。你需要判断一个测试在这些目标方面是否有价值。

自动化测试具有几个技术、经济和组织上的好处。让我们选择一些对评估测试有用的好处:

  1. 测试能够节省时间和金钱。 测试试图在软件问题发展之初就予以解决。测试可以在错误造成实际损害之前预防它们,此时错误仍然可管理且处于控制之下。

    当然,质量保证本身需要花费时间和金钱。但相比让错误在软件发布中发生,它所需的时间更少,成本更低。

    当一个有缺陷的应用程序交付给客户,当用户遇到错误时,当数据丢失或损坏时,整个业务都可能面临风险。事故发生后,为了重新赢得用户的信任,分析和修复错误的成本很高。

    成本效益

    一个有价值的测试具有成本效益。 测试可以预防可能导致应用程序无法使用的错误。与它所预防的潜在损害相比,编写测试的成本更低。

  2. 测试将需求形式化和文档化。 测试套件是对代码应该如何行为的正式、可读性强的描述,可以被人类和机器理解。它帮助原始开发人员理解他们需要实现的需求。它帮助其他开发人员理解他们需要应对的挑战。

    描述性

    一个有价值的测试清晰地描述了实现代码的行为方式。 测试使用适当的语言与开发人员交流并传达需求。测试列举了实现必须处理的已知情况。

  3. 测试确保代码实现了需求并且没有错误。 测试会遍历代码的每个部分以发现缺陷。

    成功和错误案例

    一个有价值的测试涵盖了重要的场景 ——包括正确和错误的输入、预期情况和异常情况。

  4. 测试通过防止回归,使变更更加安全。 测试不仅验证当前实现是否符合需求,还验证在变更后代码是否仍然按预期工作。适当的自动化测试会减少意外的破坏,让开发新功能和代码重构更加安全。

    防止破坏

    一个有价值的测试会在关键代码被更改或删除时失败。 设计测试以在依赖行为更改时失败。如果更改的是无关的代码,测试应该仍然通过。

测试能够实现的目标

自动化测试是一种具有特定目的的工具。一个基本的概念是,测试有助于构建一个按照需求运行的应用程序。这是正确的,但是有一些微妙之处。

国际软件测试资格委员会(ISTQB) 提出了 七项测试原则 ,揭示了测试能够实现和不能实现的内容。我们不讨论每个原则,而是考虑其中的主要思想。

发现错误

测试的目的是 发现错误。如果测试失败,它证明存在一个错误(或者测试设置不正确)。如果测试通过,它证明 这个特定的测试设置 没有触发错误。它并不能证明代码是正确的和没有错误的。

测试高风险案例

那么,是否应该编写针对所有可能情况的自动化测试以确保正确性呢?ISTQB的原则表示:“穷尽测试是不可能的”。为所有可能的输入和条件编写测试既不可行,也不值得。相反,你应该 评估某种情况的风险 ,并首先为高风险案例编写测试。

即使覆盖所有情况是可行的,它也会给你一种虚假的安全感。没有哪个软件是没有错误的,即使经过全面测试的软件可能仍然是一个对用户来说难以使用的噩梦。

适应测试方法

另一个核心思想是,测试取决于其上下文,并且需要不断地进行适应以提供意义。在本指南中,特定的上下文是使用JavaScript编写的单页Web应用程序,采用Angular框架。这些应用程序需要特定的测试方法和工具,我们将了解它们。

在你学会并应用了这些工具后,你也不应该停下来。固定的工具链只能发现某些类型的错误。你需要尝试不同的方法来发现新的错误类别。同样,现有的测试套件需要定期更新,以便仍然能够发现回归错误。

调整测试方法

没有一种正确的测试方法。实际上,有几种竞争的思想流派和方法论。从他人的经验中学习,但要开发一种适合你的应用程序、团队、项目或业务的测试方法。

检查你的应用程序

在开始设置测试之前,你应该检查你的应用程序的当前情况:

  • 哪些是 关键功能?例如,登录、搜索记录和编辑表单。

  • 哪些是经常报告的 技术问题和障碍?例如,你的应用程序可能缺乏错误处理或跨浏览器兼容性。

  • 有哪些 技术要求?例如,你的应用程序需要从给定的后端API消费结构化数据。反过来,它需要公开某些URL路由。

开发过程

这种技术评估和对你的开发团队的调查一样重要:

  • 整体对 测试的态度 是什么?例如,一些开发人员重视测试,而另一些人认为测试无法有效避免错误。

  • 目前的 测试实践 是什么样的?例如,开发人员只是偶尔编写测试,但不是每天都进行。

  • 编写测试的经验 如何?例如,一些开发人员已经有多个项目的测试编写经验,而另一些人对基本概念有所了解,但还没有实践过。

  • 阻碍良好测试例程的 障碍 是什么?例如,开发人员没有接受过测试工具的培训。

  • 测试是否被 很好地整合 到你的开发工作流程中?例如,一个持续集成服务器会在每个变更集上自动运行测试套件。

一旦你回答了这些问题,你应该设定一个测试目标,并采取步骤来实现它。

回报率

一个好的开始是从经济的角度思考。编写测试的回报率是多少?选择低成本的目标。找到业务关键功能,并确保它们被测试覆盖。编写测试需要很少的工作量,但能覆盖大部分代码。

规范测试

同时,将测试整合到团队的工作流程中:

  • 确保每个人都有相同的基本专业知识。

  • 提供正式的培训工作坊,并将经验丰富的程序员与对测试不太熟悉的团队成员进行搭档。

  • 指定测试质量和测试基础设施的维护人员和联系人。

  • 如果可能,雇佣专职软件测试人员。

编写自动化测试对于团队成员来说应该是 简单而有趣的。消除任何使测试变得困难或低效的障碍。

适量的测试

关于适量的测试存在激烈的争论。过少的测试会成为问题:功能没有得到正确的规范,错误未被发现,出现回归问题。但过多的测试会消耗开发时间,无法带来额外的利润,并在长期内拖慢开发速度。

因此,我们需要找到一个平衡点。如果你的测试实践偏离了这个平衡点,就会遇到问题。如果增加的测试过多,你会发现收益微乎其微。

有意义的测试

测试在价值和质量上存在差异。有些测试比其他测试更有意义。如果这些测试失败,你的应用实际上就无法使用。这意味着 测试的质量比数量更重要

测试的一个常见度量指标是 代码覆盖率。它统计了你的代码中被测试调用的行数。它告诉你哪些代码部分被执行了。

这个测试指标是 有用但也存在严重缺陷的,因为测试的价值无法自动量化。代码覆盖率告诉你某段代码是否被调用,而不管它的重要性。

找到未覆盖的代码

代码覆盖率报告可能会指出一些重要的行为尚未被测试覆盖,但应该被覆盖。它并不能告诉你现有的测试是否有意义并且能够正确地进行期望。你只能推断出在测试条件下,代码没有抛出异常。

关于是否应该追求100%的代码覆盖率存在争议。虽然覆盖100%的某些业务关键代码是可行的,但要覆盖Angular和TypeScript编写的应用程序的所有部分需要付出巨大的努力。

覆盖主要功能

如果你从用户的角度为应用程序的主要功能编写测试,你可以达到60-70%的代码覆盖率。每增加1%的收益都需要更多的时间,并且可能导致奇怪而扭曲的测试,这些测试不能反映你的应用程序的实际使用情况。

我们将在后面讨论 代码覆盖工具的实际使用

测试的级别

我们可以通过测试的视角和与代码的接近程度来区分自动化测试。

端到端测试

模拟真实使用

一些测试以 高层次、鸟瞰 的视角看待应用程序。它们模拟用户与应用程序的交互:导航到一个地址、阅读文本、点击链接或按钮、填写表单、移动鼠标或键盘输入。这些测试对用户在浏览器中所看到和阅读的内容进行期望。

从用户的角度来看,你的应用程序是使用Angular实现的并不重要。代码的内部结构等技术细节并不相关。前端和后端之间,代码的各个部分之间没有区别。整个体验都被测试。

端到端测试

这些测试被称为 端到端(E2E)测试,因为它们将应用程序的所有部分从一个端(用户)集成到另一个端(后端的最深处)。端到端测试也是 验收测试 的自动化部分,因为它们告诉你应用程序是否对用户有效。

单元测试

其他测试以 低层次、蠕虫视角 看待应用程序。它们选择一个小的代码片段并对其进行全面测试。从这个角度来看,实现细节很重要。开发人员需要设置一个适当的测试环境来触发所有相关的情况。

隔离一个部分

目光短浅的蠕虫只能看到眼前的事物。这个视角试图切断被测试代码与其依赖项的联系。它试图 隔离 代码以进行检查。

单元测试

这些测试被称为 单元测试。一个单元是一个可以合理进行测试的小代码片段。

集成测试

紧密的组合

在这两个极端视角之间,有一些测试针对特定的代码部分,但测试 紧密的组合。它们摒弃实现细节,尝试从用户的角度出发。

集成测试

这些测试被称为 集成测试,因为它们测试各个部分如何 集成 到组合中。例如,可以将一个功能的所有部分一起进行测试。集成测试证明各个部分正常地协同工作。

测试工作的分配

所有级别的测试都是必要且有价值的。不同类型的测试需要结合起来创建一个全面的测试套件。

但我们应该如何分配注意力?我们应该在哪个级别花费最多的时间?我们应该专注于端到端测试,因为它们模拟了用户如何与应用程序进行交互?同样,这是测试专家之间有争议的问题。

速度

毋庸置疑的是,端到端测试等高级别测试是昂贵且耗时的,而集成测试和单元测试等低级别测试则更便宜且更快。

可靠性

由于其固有的复杂性,端到端测试往往不可靠。即使软件没有错误,它们经常会失败。有时它们会毫无明显原因地失败。当您再次运行相同的测试时,它们突然通过了。即使测试正确地失败,也很难找到问题的根本原因。您需要遍历整个堆栈以定位错误。

设置成本

端到端测试使用真实的浏览器,并针对完整的软件堆栈运行。因此,测试设置非常庞大。您需要将前端、后端、数据库、缓存等部署到测试机器上,然后使用机器运行端到端测试。

相比之下,集成测试更简单,单元测试则更简单。由于它们的移动部分较少且依赖性较少,它们运行更快,结果可重现。设置相对简单。集成测试和单元测试通常在一台机器上针对被测试代码的构建运行。

成本与收益

划分测试工作的关键问题是:哪些测试产生的投资回报最高?维护测试与其收益之间的工作量有多大?

从理论上讲,端到端测试的效益最高,因为它们能指示应用程序是否对用户有效。但在实践中,它们不可靠、不精确且难以调试。集成测试和单元测试的商业价值被认为更高。

分配

因此,一些专家认为,您应该编写少量的端到端测试、适量的集成测试和大量的单元测试。如果将这种分配可视化,它看起来像一个金字塔:

pyramid

这些比例被称为 测试金字塔。在各个领域、平台和编程语言中,它们在软件测试中被广泛认可。

然而,这种常见的分配也引起了一些争议。特别是,关于单元测试的价值,专家们持不同意见。

设计指南

一方面,单元测试精确而廉价。它们非常适合详细说明共享模块的所有细节。它们帮助开发人员设计小型、可组合的模块,这些模块“做一件事,并且做得很好”。这种测试水平迫使开发人员重新考虑模块如何与其他模块进行交互。

信心

另一方面,单元测试过于低级,无法检查特定功能对用户是否有效。它们对于验证应用程序的可靠性提供的信心有限。此外,单元测试可能增加每次代码更改的成本。

单元测试存在复制或反映实现细节的风险。这些细节会由于其他地方的新需求或内部重构而经常发生变化。如果你在某个地方更改了一行代码,一些远程的单元测试突然失败。

如果你触及了共享类型或共享逻辑,这是有道理的,但这可能只是一个虚警。你必须为技术原因修复这个失败的测试,而不是因为出现了问题。

中间地带

集成测试提供了更好的权衡。这些中级测试不考虑实现细节,涵盖了一组代码单元,并提供更多的信心。如果你重构组内的代码,它们不太可能失败。

这就是为什么一些专家认为集成测试更有价值,并建议您将大部分测试工作集中在这个层级上的原因。

在Angular中,单元测试和集成测试之间的区别有时是微妙的。单元测试通常专注于单个Angular组件、指令、服务、管道等。依赖项将被替换为模拟对象。而集成测试则涵盖了一个组件及其子组件以及可能连接的服务。还可以编写一个集成测试,将Angular模块的所有部分进行整合。

Table 1. 软件测试级别的比较
级别 端到端 集成 单元

覆盖率

全面

较高

较低

性能

较慢

较快

最快

可靠性

最不可靠

可靠

最可靠

隔离故障

困难

较为容易

容易

模拟真实用户

(表格改编自Mike Wacker在 Google Testing Blog 上的一篇文章。)

黑盒测试与白盒测试

一旦你确定了要测试的代码块,你就需要决定如何进行适当的测试。一个重要的区别是测试是将实现视为封闭、不透明的盒子——黑盒测试,还是将其视为开放、透明的盒子——白盒测试。在这个比喻中,待测试的代码就像是一个带有输入和输出接口的盒子里的机器。

黑盒测试

黑盒测试 对内部结构不做任何假设。它将一些值输入到盒子里,并期望得到一些输出值。测试与公开的、文档化的 API 进行交互。不会检查内部状态和运行方式。

black box
内部

白盒测试 打开盒子,照亮内部,并通过伸手进入盒子进行测量。例如,白盒测试可能调用不属于公共 API 的方法,但仍然是技术上可触及的。然后它检查内部状态,并期望它相应地发生了变化。

不相关的内部

虽然两种方法都有其价值,但本指南 建议尽可能编写黑盒测试。你应该检查代码对用户和其他代码部分的行为。对此,代码内部的具体实现方式并不重要。对内部做出假设的测试在将来可能会在实现略微改变时出现故障。

相关行为

更重要的是,白盒测试有可能忘记检查真实的输出。它们进入盒子,转动一些轮子,拨动一些开关,并检查特定的状态。它们只是假设输出而没有实际检查。因此,它们无法涵盖重要的代码行为。

公共 API

对于 Angular 的组件、指令、服务、管道等,黑盒测试传入某个特定的输入,并期望得到适当的输出或测量副作用。测试仅调用在 TypeScript 代码中标记为 公共 的方法。内部方法应标记为 私有