跳到主要内容

测试

基础知识​

常规测试模式​

手动测试与自动化测试​

  • 手动测试(Manual testing):这是由真人执行的典型测试方法。QA 工程师会在应用中点击操作,检查应用是否正常工作,同时尝试找出破坏应用的方法。最常见的方式是探索性测试(exploratory testing),即工程师利用自己对应用的了解,沿着预定的路径或检查清单对应用进行排查。
  • 自动化测试(Automated testing):这是由计算机执行的测试。QA 工程师通过编写代码来实现自动化,以取代那些重复且枯燥的测试工作。

本系列指南将主要聚焦于自动化测试。但是,不应该只依赖单一的测试方式。即使自动化能节省大量时间和精力,人工和手动测试依然扮演着至关重要的角色。相反,测试自动化应该把人们从繁琐中解放出来,专注于探索性测试和创造性的问题解决。例如,确保用户体验的质量,或保护高风险的业务逻辑。换句话说,自动化是坚实的后盾。

黑盒 vs 白盒​

  • 黑盒测试(Opaque box testing):该方法基于分析组件或系统的功能性或非功能性需求(规格说明),而不考虑其内部结构。
  • 白盒测试(Clear box testing):这是一种考虑了“盒子”内部结构的流程。换句话说,就是基于应用程序底层的运作原理来进行测试。

测试自动化类型​

单元测试(Unit testing)​

单元测试是一种测试类型,用于将应用程序中较小的可测试部分或单元进行独立测试,以验证操作是否正确。这些单元的范围可以从函数、类或接口,一直延伸到服务或完整的组件。主要特点是:执行速度快、相互隔离、易于维护。

信息

单元测试的工具包括但不限于 Vitest。

集成测试(Integration testing)​

集成测试侧重于组件或系统之间的交互。换句话说,就是测试组件或系统协同工作的效果。集成测试的典型例子是 API 测试或组件测试。

信息

集成测试使用的工具通常与单元测试相同,也可以包括提供组件测试的框架,例如 WebdriverIO 和 Cypress。

端到端测试(End-to-end testing, E2E testing)​

通常被称为 UI 测试,这个名称甚至能更好地解释端到端测试的功能。这些测试会与应用程序的 UI 进行交互,涵盖完整的应用技术栈,从头到尾对应用程序进行全面测试。

如果参考质量保证理论,端到端测试类似于系统测试。这些测试模拟真实用户及其交互。由于涉及整个系统,端到端测试需要更多的运行时间,而更长的运行时间意味着需要更多的计算资源。因此,这种额外的投入会导致更高的维护成本。

信息

用于端到端测试的工具包括但不限于 WebdriverIO、Cypress、Playwright 和 Selenium。

视觉 UI 测试(Visual UI testing)​

视觉测试是 UI 测试中一个有趣的子类别。这些测试是端到端测试的延伸,提供了一种验证应用程序可见输出的手段。这种测试会在发生变化后截取一张屏幕截图,再截取一张包含“当前状态”(或基准文件/golden file)的截图,然后将这些结果提供给人工审查员进行检查。换句话说,视觉测试有助于发现页面外观上的“视觉 Bug”,而不仅仅是纯粹的功能性 Bug,且这些视觉问题通常不会被明确写进断言(assertions)中。

静态分析(Static analysis)​

这里还要介绍一个概念:静态分析。严格来说,静态分析不是教科书意义上的测试类型。然而,在后续的质量保证策略中,静态分析将是一个至关重要的方面。可以将静态分析想象成拼写检查功能:静态分析在不运行程序的情况下扫描代码,寻找更严重的缺陷和语法错误,从而检测出代码风格问题。这个简单的措施可以预防许多 Bug。

信息

用于静态分析的工具包括但不限于 ESLint 和 Stylelint

测试策略​

测试目标​

在测试方面,实现高测试覆盖率通常被视为开发人员的终极目标。在决定测试策略时,可能还有另一个关键因素需要考虑——满足用户的需求。

作为一名开发人员,也会使用许多其他的应用程序和设备。从这个角度来说,开发人员就是那个依赖所有这些系统“正常工作”的用户。反过来,依赖无数开发人员尽其所能地让他们开发的应用程序和设备正常运行。将心比心,作为一名开发人员,努力不辜负这份信任。因此,首要目标应该始终是交付可运行的软件并服务于用户。这也同样适用于为确保应用程序质量而编写的测试。Kent C. Dodds 在他的《前端应用的静态测试、单元测试、集成测试与端到端测试》一文中对此做了很好的总结:

“测试越接近软件的实际使用方式,它们能带来的信心就越多。” —— Kent C. Dodds

Kent 将其描述为“在测试中获得信心”。选择的测试类型越贴近用户,就越能相信测试结果是有效的。

经典基石:测试金字塔 (The Testing Pyramid)​

测试金字塔

在讨论现代测试策略之前,必须提及由 Mike Cohn 提出的经典“测试金字塔”。金字塔自下而上分为三层:单元测试(底座,数量最多) -> 服务/集成测试(中间) -> UI/端到端测试(塔尖,数量最少)。

金字塔的核心理念是:越底层的测试运行越快、成本越低,应该大量编写;越顶层的测试运行越慢、越容易脆弱(Flaky),应尽量少写。然而,随着现代前端框架的发展,这种传统的比例分配在很多场景下显得不再适用。

现代 Web 的改良:测试奖杯(Testing Trophy)​

测试奖杯

测试奖杯(Testing Trophy)是由 Kent C. Dodds 提出的一种改良模型。这种改良方案在保持对集成测试关注的同时,将静态分析纳入了考量。该模型的核心理念被总结为一句话:

编写测试。不要写太多。主要写集成测试。

Write tests. Not too many. Mostly integration.

测试奖杯之所以呈中间大、两头小的"奖杯"形状,是因为该模型重新评估了不同测试类型带来的"信心(Confidence)"与"成本"的投资回报率。该模型包含四个层级(自下而上):

  • 静态分析(Static analysis):在这个类比中起着至关重要的作用,仅通过运行已经概述的调试步骤,就能捕获拼写错误、样式错误和其他 Bug。
  • 单元测试(Unit tests):单元测试确保最小单元得到了适当的测试,但测试奖杯不会像测试金字塔那样过度强调单元测试。
  • 集成测试(Integration):这是主要焦点,因为集成测试与其他改良方案一样,以最佳方式平衡了成本与更高的信心。
  • UI 测试(UI tests):包括端到端测试和视觉测试,这些测试位于测试奖杯的顶部,类似于在测试金字塔中的角色。
信息

如需详细了解测试奖杯,请参阅 Kent C. Dodds 的文章。

以 UI 为导向的变体:测试螃蟹(Testing Crab)​

测试螃蟹

虽然测试自动化非常有价值,但必须记住,手动测试依然不可或缺。自动化测试应当减轻常规任务的负担,让质量保证(QA)工程师能够专注于关键领域。自动化不应取代手动测试,而应作为手动测试的补充。

测试螃蟹(Testing Crab) 是一种更加前卫、极度侧重于 UI 的测试策略模型。随着现代 Web 框架(如 Next.js)的普及,开发者编写的底层业务逻辑变少了,大量的工作变成了"将数据绑定到 UI 上"的"管道工程(plumbing)"。在这种背景下,底层的单元测试带来的价值进一步缩水。

测试螃蟹的形状特点是:拥有巨大的两只蟹钳(E2E 测试和视觉测试),中等大小的身体(集成测试),以及极其细小的腿(单元测试)。

测试螃蟹更加侧重于这些以 UI 为导向的测试方式。测试螃蟹的优势在于能提供极高的信心,但由于测试执行速度较慢,成本自然也会更高。

这种测试策略包含了一个额外的维度:测试螃蟹不仅验证应用程序的功能,还验证应用外观是否良好。测试螃蟹凸显了前文定义的"视觉测试"的重要性。在这里,集成测试(分为组件测试和 API 测试)退居次要地位,而单元测试的作用则更加边缘化。

尽管这种测试策略成本较高,但该策略也有其用武之地:例如,在需要较少测试、或无需覆盖大量复杂性的较小项目中。在这种情况下,全面铺开、以集成测试为核心的测试策略可能会显得过度设计(over-engineered)。

为什么选择“测试螃蟹”?​

  1. 视觉即产品:在许多现代应用中,“UI 看起来正确并且能点通”就是应用的核心价值。视觉测试(如 Percy, Chromatic)配合强大的 E2E 框架(如 Playwright, Cypress)可以直接断言页面的最终呈现,这比在代码中校验状态有效得多。
  2. 重度依赖第三方服务:如果应用大量整合了第三方服务(如 Auth0 登录、Stripe 支付),单元测试很难验证这些集成是否有效,只能通过端到端的浏览器自动化来真实演练。
  3. 极高的信心:测试螃蟹提供的是所有模型中最高的信心,因为测试螃蟹是 100% 站在最终用户的视角来审视应用的。

测试螃蟹的权衡​

尽管信心极高,但这种策略的成本非常昂贵。

  • 执行缓慢:大量的浏览器启动、截图比对需要漫长的 CI/CD 运行时间。
  • 脆弱性(Flakiness)高:网络波动、第三方服务延迟都可能导致测试随机失败。
  • 适用场景:测试螃蟹最适合小型项目、重前端展示的 SaaS 产品、或者那些业务逻辑并不复杂但对视觉呈现要求极高的应用。对于包含海量复杂状态计算的大型企业级系统,测试螃蟹可能会因为维护成本过高而成为负担。
小结:如何选择?
  • 如果应用包含大量复杂的业务逻辑和数据处理,经典金字塔或测试奖杯(重集成)是首选。
  • 如果应用主要是将 API 数据漂亮地展示在页面上,且对视觉要求极高,测试螃蟹(重视觉与 E2E)会带来意想不到的安稳。

测试用例​

在软件开发中,测试用例是一系列设计好的操作或条件,用于确认软件程序或应用程序的有效性。测试用例旨在确保软件按预期运行,且所有特性和功能均表现正确。软件测试人员或开发人员通常会创建这些测试用例,以保证软件满足指定的需求和规格。

当运行测试用例时,软件会执行一系列检查以确保产生预期的结果。在此过程中,测试会完成以下两项核心任务:

  • 验证(Verification):彻底检查软件以确保其无错误运行的过程。确定所构建的产品是否满足所有必要的非功能性需求并成功实现其预期目的至关重要。验证过程回答的问题是:“是否正确地构建了产品?”
  • 确认(Validation):确保软件产品满足必要标准或高层需求的过程。确认过程涉及检查实际产品是否与预期产品一致。本质上,是在回答这个问题:“是否为用户需求构建了正确的产品?”

假设程序未能提供预期的结果,此时测试用例就会充当“信使”——报告一个不成功的结果,开发人员或测试人员则需要调查问题并寻找解决方案。

无论测试类型如何,都可以将这些检查和操作视为计算机遵循的路径。用于检查的一组输入数据或条件被称为“等价类(equivalence classes)”。等价类应该使被测系统产生相似的行为或结果。测试内部执行的具体路径可能会有所不同,但应与测试中执行的操作和断言相匹配。

测试路径:测试用例的典型类型​

快乐路径(The happy path)​

这是最广为人知的一种。快乐路径也被称为“快乐日场景(happy day scenario)”或“黄金路径(golden path)”。快乐路径定义了功能、应用程序或变更最常见的用例——即对客户而言最理想的运作方式。

可怕路径(The scary path)​

测试用例中需要覆盖的第二个最关键的路径往往会被遗漏,因为就像可怕路径的名字所暗示的那样,这条路径让人不太舒服。可怕路径是“负面测试”。这条路径针对的是导致代码行为异常或进入错误状态的场景。如果用例非常脆弱,对利益相关者或用户构成高风险,那么测试这些场景就显得尤为重要。

此外,还需要了解并考虑使用其他一些路径,但这些路径通常不太常用。下表对其进行了总结:

测试路径解释
愤怒路径(Angry path)导致错误,但是是预期内的错误;例如,想确保错误处理机制正常工作
犯罪路径(Delinquent path)处理应用程序需要满足的任何与安全相关的场景
荒凉路径(Desolate path)测试应用程序在数据不足以运行时的场景,例如测试空值(null values)
健忘路径(Forgetful path)测试应用程序在资源不足时的行为,例如触发数据丢失
犹豫路径(Indecisive path)测试用户尝试执行操作或遵循应用程序中的用户故事,但尚未完成这些工作流时的行为。例如,用户中断了其工作流
贪婪路径(Greedy path)尝试使用海量输入或数据进行测试
高压路径(Stressful path)尝试用一切可能的方式给应用程序施加负载,直到应用程序无法运行(类似于负载测试)

可以通过多种方法对这些路径进行分类。两种常见方法如下:

  • 等效分区。 一种测试方法,用于将测试用例分类为组或分区,从而有助于创建等效类。这一方法基于以下假设:如果分区中的某个测试用例发现了缺陷,则同一分区中的其他测试用例也可能会发现类似的缺陷。由于特定等价类中的所有输入都应具有相同的行为,因此可以减少测试用例的数量。详细了解等价分区。
  • 限制分析。 一种测试方法,也称为边界值分析,用于检查输入值的极限或极端值,以发现系统功能或约束条件限制范围内可能出现的任何潜在问题或错误。

编写测试用例​

由测试人员编写的经典测试用例会包含特定的数据,帮助掌握想要进行的测试内容,当然,也是为了执行测试。典型的测试人员会在表格中记录他们的测试工作。在这个阶段,可以使用两种模式来构建测试用例的结构,并在以后构建测试代码本身时继续使用:

  • Arrange, Act, Assert(AAA) 模式:这种"准备、执行、断言"(也称为"AAA"或"Triple A")测试模式是一种将测试组织为三个不同步骤的方法:准备测试、执行测试,最后得出结论。
  • Given, When, Then 模式:这种模式类似于 AAA 模式,但该模式源于行为驱动开发(BDD)。

一旦涵盖了测试本身的结构,未来的文章将详细介绍这些模式。如果想在此阶段深入了解,可以查阅这两篇文章:《Arrange-Act-Assert: A pattern for writing good tests》和《Given-When-Then》。

根据本文的所有知识点,下表总结了一个经典示例:

信息解释
前置条件(Prerequisites)编写测试前需要完成的所有准备工作
被测对象(Object under test)需要验证什么?
输入数据(Input data)变量及其值
执行步骤(Steps to be executed)让测试运行起来的所有操作:在测试中执行的所有动作或交互
预期结果(Expected result)应该发生什么,以及需要满足哪些期望
实际结果(Actual result)实际发生了什么

在自动化测试中,无需像测试人员那样记录所有这些测试用例,尽管这样做无疑很有帮助。只要留意,就可以在测试中找到所有这些信息。以下将这个传统的测试用例转换为自动化测试。

信息在测试自动化中的转化
前置条件需要的所有准备工作,安排测试,以及思考为了让测试场景发生需要给定什么条件(Arrange/Given)
被测对象这个“对象”可以是多种事物:被测的应用程序、流程、单元或组件
输入数据参数值
执行步骤在测试内部执行的所有动作和命令,执行的操作,以及当执行某些操作时观察发生的事情(Act/When)
预期结果用来验证应用程序的断言,在应用程序中进行断言的内容(Assert/Then)
实际结果自动化测试运行的结果

最佳实践​

通用指南与模式​

值得注意的是,无论是进行单元测试、集成测试还是端到端测试,特定的模式和要点都是至关重要的。这些原则可以且应该应用于这两种类型的测试,因此这些原则是一个很好的起点。

保持简单​

在编写测试时,最重要的事情之一就是保持简单。考虑大脑的处理能力很重要。主要的生产代码已经占据了大量空间,留给额外复杂性的空间很小。对于测试来说尤其如此。

如果大脑的可用空间变少,在测试工作中可能会变得更加放松。这就是为什么在测试中优先考虑简单性至关重要。事实上,Yoni Goldberg 的 JavaScript 测试最佳实践强调了黄金法则的重要性——测试应该感觉像一个助手,而不是一个复杂的数学公式。换句话说,应该能一眼看懂测试的意图。

无论测试的复杂程度如何,都应该力求简单。事实上,测试越复杂,简化测试就越关键。实现这一点的一种方法是通过扁平化测试设计(flat test design),即尽可能保持测试简单,并且只测试必要的内容。这意味着每个测试应该只包含一个测试用例,并且该测试用例应该专注于测试单一、特定的功能或特性。

从这个角度思考:在阅读失败的测试时,应该很容易找出问题所在。这就是保持测试简单易懂的原因。这样做可以在出现问题时快速识别并修复。

测试有价值的内容​

扁平化测试设计也有助于保持专注,并确保测试是有意义的。请记住,不应该仅仅为了覆盖率而创建测试——测试始终应该有明确的目的。

不要测试实现细节​

测试中一个常见的问题是,测试通常被设计为测试实现细节,例如在组件或端到端测试中使用选择器(selectors)。实现细节是指代码用户通常不会使用、看到甚至不知道的东西。这可能导致测试中出现两个主要问题:假阴性(false negatives)和假阳性(false positives)。

当测试的代码是正确的,但测试却失败了,就会发生假阴性。这通常发生在应用程序代码重构导致实现细节发生变化时。另一方面,当被测试的代码是不正确的,但测试却通过了,就会发生假阳性。

解决这个问题的一个方法是考虑拥有的不同类型的用户。终端用户和开发人员的方法可能不同,他们与代码的交互方式也可能不同。在规划测试时,必须考虑用户会看到或与什么进行交互,并使测试依赖于这些内容,而不是实现细节。

例如,选择不易发生变化的选择器可以使测试更可靠:使用 data-attributes 而不是 CSS 选择器。更多详情请参阅 Kent C. Dodds 关于此主题的文章

提示

注: 相反,如果把测试写得好,这也会对源代码结构产生积极影响,例如,考虑代码的可测试性时。

Mocking(模拟):不要失去控制​

Mocking 是单元测试(有时是集成测试)中使用的一个广泛概念。Mocking 涉及创建假数据或组件,以模拟对应用程序具有完全控制权的依赖项。这允许进行隔离测试。

在测试中使用 mock 可以提高可预测性、关注点分离和性能。而且,如果需要进行涉及人工参与的测试(例如护照验证),将不得不使用 mock 来隐藏人工验证环节。出于所有这些原因,mock 是一个值得考虑的强大工具。

同时,mock 可能会影响测试的准确性,因为 mock 是模拟的,而不是真实的用户体验。因此,在使用 mock 和 stub 时需要保持谨慎。

是否应该在端到端测试中使用 Mock?​

通常不建议。然而,mock 有时也能救命——所以不要完全排除 mock。

想象一下这个场景:正在为涉及第三方支付提供商服务的功能编写测试。处于他们提供的沙盒环境中,这意味着不会发生真实的交易。不幸的是,沙盒出现了故障,导致测试失败。修复需要由支付提供商来完成。能做的只有等待提供商解决这个问题。

在这种情况下,减少对无法控制的服务的依赖可能更有益。在集成或端到端测试中仍然建议谨慎使用 mock,因为 mock 会降低测试的可信度。

测试细节:该做与不该做的事​

优秀的单元测试应该包含什么?​

一个理想且有效的单元测试应该:

  • 专注于特定方面。
  • 独立运行。
  • 涵盖小规模场景。
  • 使用描述性的名称。
  • 如果适用,遵循 AAA(准备、执行、断言)模式。
  • 保证全面的测试覆盖。
该做 ✅不该做 ❌
尽可能保持测试小巧,每个测试用例只测一件事针对大型单元编写测试
始终保持测试隔离,并 mock 单元外部所需的内容包含其他组件或服务
保持测试相互独立依赖先前的测试或共享测试数据
覆盖不同的场景和路径仅局限于快乐路径或负面测试
使用描述性的测试标题,以便能立即了解测试的内容仅按函数名进行测试,描述性不够,例如:testBuildFoo() 或 testGetId()
特别是在此阶段,力求良好的代码覆盖率或更广泛的测试用例范围从每个类一直测试到数据库(I/O)级别

优秀的集成测试应该包含什么?​

理想的集成测试与单元测试有一些共同的标准。但是,还需要考虑一些额外的要点。一个出色的集成测试应该:

  • 模拟组件之间的交互。
  • 覆盖现实世界的场景,并使用 mock 或 stub。
  • 考虑性能。
该做 ✅不该做 ❌
测试集成点:验证每个单元在集成时能否优雅地协同工作孤立地测试每个单元——那是单元测试的工作
测试现实世界的场景:使用源自真实世界数据的测试数据使用重复的自动生成测试数据,或不反映现实世界用例的其他数据
为外部依赖项使用 mock 和 stub,以保持对整个测试的控制创建对第三方服务的依赖,例如对外部服务的网络请求
在每个测试前后使用清理程序忘记在测试内部使用清理措施,否则由于缺乏适当的测试隔离,这可能导致测试失败或假阳性

优秀的端到端测试应该包含什么?​

一个全面的端到端测试应该:

  • 复制用户交互。
  • 涵盖关键场景。
  • 跨越多个层级。
  • 管理异步操作。
  • 验证结果。
  • 考虑性能。
该做 ✅不该做 ❌
使用 API 驱动的快捷方式对每个步骤都使用 UI 交互,包括 beforeEach 钩子
在每个测试前使用清理程序,比单元和集成测试更加注重测试隔离,因为这里的副作用风险更高忘记在每个测试后进行清理,如果不清理残留的状态、数据或副作用,残留状态将影响后续执行的其他测试
将端到端测试视为系统测试,这意味着需要测试整个应用程序技术栈孤立地测试每个单元——那是单元测试的工作
在测试内部使用极少或不使用 mock,仔细考虑是否要 mock 外部依赖项严重依赖 mock
通过例如不在同一测试中过度测试大型场景来考虑性能和工作负载在不使用快捷方式的情况下覆盖大型工作流

代码覆盖率​

代码覆盖率是一个衡量指标,用于统计测试所执行的源代码的百分比。代码覆盖率有助于识别可能缺乏适当测试的代码区域。

通常,记录这些指标的表格看起来像这样:

文件% 语句 (Statements)% 分支 (Branch)% 函数 (Functions)% 行 (Lines)未覆盖的行
file.js90%100%90%80%89, 256
coffee.js55.55%80%50%62.5%10-11, 18

随着新功能和测试的添加,代码覆盖率百分比的提升会带来更强的信心,表明应用程序已经经过了彻底的测试。

代码覆盖率可以作为衡量测试有效性的有用指标。代码覆盖率可以通过确保代码中的关键逻辑得到充分测试,来帮助提高应用程序的质量。

然而,请记住代码覆盖率只是一个指标。务必同时考虑其他因素,例如测试的质量和应用程序的需求。

追求 100% 的代码覆盖率不是最终目标。相反,应当将代码覆盖率与完善的测试计划结合使用,该计划应包含各种测试方法,包括单元测试、集成测试、端到端测试和手动测试。

四种常见的代码覆盖率类型​

收集和计算代码覆盖率有四种常见的方法:函数覆盖率、行覆盖率、分支覆盖率和语句覆盖率。

函数覆盖率 (Function coverage)​

函数覆盖率是一个直观的指标。函数覆盖率统计的是测试调用了代码中百分之多少的函数。

行覆盖率 (Line coverage)​

行覆盖率衡量的是测试套件执行了百分之多少的可执行代码行。如果某行代码未被执行,则意味着代码的某些部分尚未被测试。

请注意,行覆盖率不考虑声明语句,因为声明语句不可执行。

分支覆盖率 (Branch coverage)​

分支覆盖率衡量的是代码中已执行的分支或决策点(例如 if 语句或循环)的百分比。分支覆盖率用于确定测试是否检查了条件语句的 true(真)和 false(假)两个分支。

语句覆盖率 (Statement coverage)​

语句覆盖率衡量的是测试执行了代码中百分之多少的语句。语句覆盖率与行覆盖率类似,但语句覆盖率会将包含多个语句的单行代码考虑在内。

如果始终保证每行只写一个语句,那么行覆盖率将与语句覆盖率非常相似。

应该选择哪种类型的代码覆盖率?​

大多数代码覆盖率工具都包含这四种常见的代码覆盖率类型。选择优先使用哪种代码覆盖率指标,取决于具体的项目需求、开发实践和测试目标。

通常情况下,语句覆盖率(Statement coverage)是一个很好的起点,因为语句覆盖率是一个简单且易于理解的指标。与语句覆盖率不同,分支覆盖率和函数覆盖率衡量的是测试是否调用了某个条件(分支)或函数。因此,分支覆盖率和函数覆盖率是语句覆盖率之后的自然进阶。

一旦实现了较高的语句覆盖率,就可以继续推进分支覆盖率和函数覆盖率。

测试覆盖率(Test coverage)等于代码覆盖率吗?​

不等于。测试覆盖率和代码覆盖率经常被混淆,但两者是不同的:

  • 测试覆盖率: 一个定性指标,衡量测试套件对软件功能的覆盖程度。测试覆盖率有助于确定所涉及的风险水平。
  • 代码覆盖率: 一个定量指标,衡量测试期间执行的代码比例。代码覆盖率关注的是测试覆盖了多少代码。

100% 的代码覆盖率并不意味着没有 Bug​

虽然在测试中实现高代码覆盖率当然是值得追求的,但 100% 的代码覆盖率并不能保证代码中没有 Bug 或缺陷。

糟糕的指标比没有指标更糟​

糟糕的指标会带来虚假的安全感,这比完全没有指标还要糟糕。例如,如果存在一个实现了 100% 代码覆盖率的测试套件,但测试全都没有意义,那么可能会产生一种代码已经过充分测试的错觉。如果不小心删除或破坏了应用程序代码的某一部分,测试仍然会通过,即使应用程序已经不再正常工作。

为了避免这种情况:

  • 测试审查:编写并审查测试,确保测试是有意义的,并在各种不同的场景中测试代码。
  • 将代码覆盖率作为指导原则,而不是衡量测试有效性或代码质量的唯一标准。

在不同类型的测试中使用代码覆盖率​

三种常见的测试类型中使用代码覆盖率:

  • 单元测试: 单元测试是收集代码覆盖率的最佳测试类型,因为单元测试旨在覆盖多个小规模的场景和测试路径。
  • 集成测试: 集成测试可以帮助收集集成测试的代码覆盖率,但需谨慎使用。在这种情况下,计算的是更大一部分源代码的覆盖率,因此很难确定哪些测试实际覆盖了代码的哪些部分。尽管如此,对于没有良好隔离单元的遗留系统来说,计算集成测试的代码覆盖率可能会有所帮助。
  • 端到端(E2E)测试: 由于这些测试性质复杂,衡量 E2E 测试的代码覆盖率非常困难且充满挑战。与其使用代码覆盖率,不如采用需求覆盖率(Requirement coverage),这可能是更好的选择。这是因为 E2E 测试的重点是覆盖测试需求,而不是关注源代码。