精通-Angular-测试驱动开发-全-

精通 Angular 测试驱动开发(全)

原文:zh.annas-archive.org/md5/1ba27f2d07743700638425110cd3d10b

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:前言

大家好! 测试驱动开发 (TDD**)是一种在 Angular 开发中广泛使用的软件开发流程,用于确保代码质量并减少调试时间。 TDD 是一种敏捷的软件开发方法,强调在编写实际代码之前迭代编写测试。 TDD 过程包括 三个步骤:

  • 红色:在这个初始阶段,开发者为他们打算实现的功能编写测试。 由于还没有相应的代码,这个测试最初会失败,因此用“红色”这个词来表示测试的失败状态。

  • 绿色:在红色阶段之后,开发者编写必要的最少代码以使测试通过。 这一阶段的目标是快速从失败的测试(红色)过渡到通过的测试(绿色),重点是满足测试条件,而不一定优化代码。

  • 重构:在测试成功通过后,代码随后得到改进和优化。 这一阶段涉及改进代码的设计、结构和效率,同时确保测试保持绿色状态,即继续通过。 重构对于在不改变代码外部行为的情况下提高代码质量、可维护性和性能至关重要,正如通过测试所证实的那样。

这些步骤会循环重复,用于每个新的功能,确保软件的每个部分都经过彻底测试并由测试支持,从而促进高代码质量,减少错误,并促进 自信的重构。

本书是一本全面的指南,为开发者提供了提高技能和交付高质量 Angular 应用程序的必要资源。 通过实践方法和真实世界的示例,它全面涵盖了 TDD 概念、技术和工具,不仅限于单元测试,还探索了 Angular 的管道、表单和响应式编程测试。 在这本书中,你将学习如何使用管道验证和操作数据,测试 Angular 表单的输入验证和用户交互,以及使用响应式编程处理异步操作。 此外,你将探索使用 Protractor、Cypress 和 Playwright 框架进行端到端测试,获得编写健壮的 Web 应用程序测试的宝贵见解,涵盖导航、元素交互和行为验证。 你还将探索 TDD 与 CI/CD 的集成,学习自动化测试、部署 Angular 应用程序和实现更快反馈循环的最佳实践。 借助具体的示例、最佳实践和清晰的解释,你将在本书结束时能够成功地在 Angular 项目中实施 TDD。

本书面向对象

精通 Angular 测试驱动开发 是一本全面的指南,为开发者提供了增强技能和交付高质量 Angular 应用程序的必要资源。 通过实践方法和真实世界的示例,它全面涵盖了 TDD 概念、技术和工具,不仅限于单元测试,还探索了测试 Angular 管道、表单和响应式编程。 在这本书中,你将学习如何使用管道验证和操作数据,测试 Angular 表单的输入验证和用户交互,以及使用响应式编程处理异步操作。 此外,你将探索使用 Protractor、Cypress 和 Playwright 框架进行端到端测试,获得编写健壮的 Web 应用程序测试的宝贵见解,涵盖导航、元素交互和行为验证。 你还将探索将 TDD 与 CI/CD 集成,学习自动化测试、部署 Angular 应用程序和实现更快反馈循环的最佳实践。 反馈循环。

借助实践示例、最佳实践和清晰的解释,你将在本书结束时成功地在 Angular 项目中实施 TDD。

本书涵盖内容

第一章, 使用 TDD 的第一步,旨在理解和设置测试驱动开发。 我们将通过它解决的问题、它带来的优势、其实施背后的逻辑以及它与 Angular 项目成功的相关性来分析这个概念。

第二章, 使用 Jasmine 和 Karma 测试 Angular 应用程序 测试 Angular 应用程序,目标是熟悉 Jasmine 和 Karma 并编写你的第一个单元测试。 你将了解如何在 Angular 项目中实施 Jasmine 和 Karma 的配置。

第三章, 为 Angular 组件、服务和指令编写有效的单元测试,探讨了在尊重 TDD 原则的情况下重构为 Angular 组件编写的测试,并编写用于我们项目业务逻辑的 Angular 服务和指令的测试。 测试将逐步重构,以突出 TDD 的渐进性特征。

第四章, 在 Angular 测试中模拟和存根依赖项,专注于创建模拟,这是测试 Angular 服务和指令时不可避免的一个方面。 同时,我们将利用这个机会完成和重构我们服务和指令的先前测试。

第五章, 测试 Angular 管道、表单和响应式编程,探讨了基于测试驱动开发原则如何测试 Angular 项目的管道、响应式表单和 RxJS 操作符。

第六章, 使用 Protractor、Cypress 和 Playwright 探索端到端测试,探讨了端到端测试——一种评估产品在端到端流程中功能的方法。 因此,当你想要确保所构建应用程序的质量时,查阅它是非常明智的。 目标是理解它,并了解在 Angular 项目中实施它的好处。

第七章, 理解 Cypress 及其 在 Web 应用程序端到端测试中的作用 *,探讨了 Cypress 是如何成为 Angular 端到端测试最佳工具的。 我们将一起发现如何安装它、配置它以及如何使用它来执行我们的各种面向组件的端到端测试。

第八章, 使用 Cypress 编写有效的端到端组件测试,更深入地讨论了使用 Cypress 编写端到端测试的问题——始终采用 TDD 方法,因为我们将在尊重 Cypress 最佳实践的同时改进和重构与组件相关的先前测试。

第九章, 理解持续集成和持续部署(CI/CD),讨论了持续集成和持续开发的主题。 目标是了解如何设置一个管道,该管道将负责在软件被编译并通过自动化作业通过所有测试之后,在将软件部署到我们的远程服务器之前使用 GitLab CI/CD 进行验证。

第十章, Angular TDD 的最佳实践和模式,专注于在 Angular 项目中实施测试驱动开发时的最佳实践以及存在的模式。 目标是探索向易于维护且更不易出现 bug 的清洁代码收敛的方法。

第十一章, 通过 TDD 重构和改进 Angular 代码,专注于利用测试驱动开发(TDD)技术重构和改进现有的 Angular 代码。 通过在做出更改之前编写测试,您可以确保代码质量和可维护性,并自信地增强功能。 TDD 通过基于测试反馈的编写测试、重构和改进代码的迭代周期,导致更健壮和高效的 Angular 应用程序。

为了充分利用本书

*亲爱的读者,在您开始阅读本书之前,您应该对如何使用 Angular 框架及其相关的 TypeScript 语言创建应用程序有一个基本的了解。 最后,您还需要了解如何使用 Git * *和 GitHub。 *。

本书涵盖的软件/硬件 操作系统要求
Angular 17 或 更新版 LTS
TypeScript 5.1
Node.js 18 或 更新版 LTS

如果您正在使用本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库获取代码(下一节中有一个链接)。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。 代码的复制和粘贴。

下载示例代码文件

您可以从 GitHub 在github.com/PacktPublishing/Mastering-Angular-Test-Driven-Development下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富图书和视频目录的代码包可供在github.com/PacktPublishing/获取。查看它们吧!

使用的约定

本书使用了多种文本约定。

<代码文本>: 表示文本中的代码单词、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL 和用户输入。

以下是一个示例:“关于percent.pipe.spec.ts的内容,我们有以下内容:”

代码块设置如下:

 import { PercentPipe } from './percent.pipe';
describe('PercentPipe', () => {
  it('create an instance', () => {
    const pipe = new PercentPipe();
    expect(pipe).toBeTruthy();
  });
});

任何命令行输入或输出都如下所示:

 $ ng g pipe percent –skip-import
$ ng test

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“现在我们可以通过点击提交 更改 按钮来保存文件。”

提示或重要注意事项

如此显示。

联系我们

读者反馈始终受欢迎。

一般反馈:如果您对本书的任何方面有疑问,请通过 customercare@packtpub.com 给我们发邮件,并在邮件主题中提及书名。

勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。 如果您在这本书中发现了错误,我们非常感谢您向我们报告。 请访问 www.packtpub.com/support/errata 并填写 表格。

盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。 请通过 copyright@packt.com 与我们联系,并提供 材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请 访问 authors.packtpub.com

分享您的想法

一旦您阅读了 精通 Angular 测试驱动开发,我们很乐意听听您的想法! 点击此处直接跳转到亚马逊评论页面 为此书分享 您的反馈。

您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供优质 的内容。

免费下载本书的 PDF 副本

感谢您购买 这本书!

您喜欢在路上阅读,但又无法携带您的印刷 书籍到处走吗?

您的电子书购买是否与您选择的设备不兼容?

不用担心,现在每购买一本 Packt 图书,您都可以免费获得该书的 DRM 免费 PDF 版本。 无需付费。

在任何地方、任何地点、任何设备上阅读。 从您最喜欢的技术书籍中搜索、复制和粘贴代码,直接将其放入 您的应用程序中。

优惠远不止于此,您还可以获得独家折扣、时事通讯和丰富的免费内容,这些内容将每日出现在您的 收件箱中

遵循以下简单步骤即可获得 以下好处:

  1. 扫描二维码或访问以下 链接

https://packt.link/free-ebook/9781805126089

  1. 提交您的购买证明

  2. 就是这样! 我们将直接将您的免费 PDF 和其他福利发送到您的 电子邮件

第一部分:在 Angular 中开始测试驱动开发

在本部分中,您将了解测试驱动开发TDD)的优势、Angular 项目中的测试环境、使用 Jasmine 编写单元测试以及 配置 Karma。

本部分包含以下章节:

  • 第一章, 使用 TDD 的第一步

  • 第二章, 使用 Jasmine 和 Karma 测试 Angular 应用程序

第二章:1

使用 TDD 的第一步

测试驱动开发 (TDD) 是一种在 Angular 开发中广泛使用的软件开发流程,用于确保代码质量并减少调试时间。通过在编写生产代码之前编写自动化测试,开发者可以确保他们的代码符合预期的规范,并且随着时间的推移可以轻松修改和维护。

在本章中,我们将探讨 Angular 中 TDD 的早期阶段。 我们将首先讨论 TDD 在 Angular 开发中的作用以及它如何有助于提高代码质量。 然后,我们将设置开发环境,这包括安装必要的工具 和依赖项。

接下来,我们将创建一个新的 Angular 项目,并探索与编写测试相关的各种文件,包括包含实际测试的 spec 文件,以及 <st c="842">karma.conf.js</st> 文件,该文件用于配置 测试框架。

在本章中,我将提供编写有效的 Angular 测试的示例和最佳实践,例如使用描述性的测试名称、测试所有代码路径以及使用模拟数据来模拟 不同的场景。

在本章结束时,你将对 Angular 中 TDD 的基础知识有一个扎实的理解,并了解如何开始使用它。 无论你是 TDD 的新手还是想提高你的技能,本章都将为你提供创建高质量 Angular 应用程序所需的知识和工具。

我们将涵盖以下主题: 以下主题:

  • 理解 TDD 及其在 Angular 中的作用

  • 设置 开发环境

  • 创建一个新的 Angular 项目

  • 探索与 编写测试 相关的不同文件

技术要求

为了跟随本章中的示例和练习,你需要对 Angular 和 TypeScript 有基本的了解,以及安装在你的 计算机上 的代码编辑器,例如 Visual Studio Code。

本章的所有代码文件都可以在 以下位置找到 https://github.com/PacktPublishing/Mastering-Angular-Test-Driven-Development/tree/main/Chapter%201

理解 TDD 及其在 Angular 中的作用

在本节中,我们将探讨 TDD 的基础及其在 Angular 开发中的作用。 我们将首先讨论使用 TDD 的一般好处,例如提高代码质量、加快开发周期和减少调试时间。 然后,我们将探讨 TDD 如何融入 Angular 开发的整体流程,以及如何用它来创建可扩展和可维护的应用程序。

什么是 Angular 和 TDD?

Angular 是一个流行的开源 JavaScript 框架,用于构建复杂网络应用程序。 它由 Google 开发,并被世界各地的开发者广泛使用。 Angular 提供了一套工具和功能,使得构建动态、响应和可扩展的网络应用程序变得容易。

Angular 是一个基于组件的框架,允许开发者构建可重用的 UI 组件。 这些组件可以组合起来创建复杂用户界面,使得维护和扩展应用程序变得容易。 Angular 还提供了内置的测试支持,使得编写和执行 Angular 应用程序的测试变得容易。

TDD 是一种软件开发动态方法,优先于在代码实现之前增量创建测试。 TDD 流程围绕一个称为 红-绿-重构 循环的结构化序列,它包括以下阶段:

  • 编写失败的测试:通过编写一个故意失败的测试来启动循环。 这个测试作为所需功能的规范。

  • 禁止过于复杂的测试:强调创建仅必要复杂的测试。 避免不必要的复杂性确保测试始终专注于特定的功能,增强清晰度和可维护性。

  • 最小化代码实现:编写通过失败测试所需的最少代码。 这种极简方法确保代码专注于满足指定的要求。

TDD 的迭代性质如下:编写失败的测试,实现代码以通过测试,并重构代码以增强代码设计和可维护性。 这个迭代循环持续到整个代码库 完成。

TDD 在代码执行前编写测试的独特方法具有双重作用。 首先,它通过符合预定义的测试要求来保证代码的正确性。 其次,它促进了干净、可维护和可适应的代码的创建。 鼓励开发者遵循最佳实践,从而产生易于理解、可修改和在整个软件开发生命周期中可扩展的代码。 生命周期。

现在我们知道了 Angular 是什么,并了解了使用 TDD 方法的好处,让我们来理解红绿重构周期。 红绿重构周期。

红绿重构周期

红绿重构周期是 TDD 的一个基本概念。 它在软件开发中充当一个强大且系统的方法论,为开发者提供了一个结构化的框架,以实现渐进式进步。 这种方法旨在将开发过程分解为离散、可管理的 步骤,保证代码正确性并符合预定义的测试要求。 现在,让我们深入了解这个迭代过程中的每个阶段 – 红色、绿色和重构 – 的技术细节:

  • 红色 – 编写失败的测试

    红绿重构周期的第一步是编写一个失败的测试。 测试应定义代码的预期行为,并且应该以它最初失败的方式来编写。 这被称为“红色”步骤 因为测试预期 将失败。

  • 绿色 – 编写通过测试的代码

    第二步是编写使测试通过的代码。 代码应该是最小的,并且只应该编写来使测试通过。 这被称为“绿色”步骤 因为测试预期 将通过。

  • 重构 – 不改变功能的情况下改进代码

    一旦测试通过,开发者可以通过消除重复、简化代码和改进其可读性来重构代码,从而增强其设计、可读性和可维护性。 关键是不要改变测试所覆盖的功能。

在下一节中,我们将探讨红绿重构周期的益处。

红绿重构周期的益处

红绿重构周期有几个益处,包括以下内容: 以下:

  • 增强代码质量:红-绿-重构循环确保代码正确、可靠,并满足测试中预定义的要求 的测试

  • 加速开发:红-绿-重构循环允许开发者在开发过程中早期捕捉错误,这节省了时间并减少了修复错误的成本 的修复错误

  • 更好的协作:红-绿-重构循环鼓励开发人员、测试人员和其他利益相关者之间的协作,这改善了沟通并有助于确保每个人都处于 同一页

  • 简化维护:红-绿-重构循环生成的代码更容易维护和扩展,这减少了未来开发的成本和努力 的未来开发

通过使用红-绿-重构 循环,开发人员可以构建可靠、可维护和可扩展的 软件应用

接下来,我们将学习 TDD 在应用时是如何成为一个重要资产的。 在应用时。

TDD 在 Angular 开发中的作用

TDD 在 Angular 开发中扮演着关键角色。 通过首先编写测试,开发人员可以确保代码正确并满足测试中定义的要求。 这确保了应用是可靠的 并减少了错误和缺陷的风险。 TDD 还鼓励开发人员编写干净、易于维护的代码,易于修改和扩展,这使得随着时间的推移更容易维护和更新应用

Angular 提供了内置的测试支持,使得编写和执行 Angular 应用的测试变得容易。 Angular 测试框架提供了一套工具和功能,使得编写单元测试、集成测试和端到端测试变得容易。 这些测试可以作为构建过程的一部分自动运行,确保应用始终得到测试并保持可靠。 和可靠。

在下一节中,我们将设置开发环境,这涉及到准备开发 项目所需的工具和资源

设置开发环境

在理论上理解 TDD 及其在 Angular 应用程序开发中的作用后,下一步是配置我们的开发环境,以便我们可以在 Angular 中应用 TDD 原则。 为此,我们需要安装创建 Angular 项目的所需工具。 通过遵循正确的步骤,我们可以创建一个促进使用 TDD 原则在 Angular 中进行有效和高效开发的开发环境

设置 Angular 的开发环境可能比设置通用开发环境要复杂一些。 然而,有了正确的指导,这可以是一个简单直接的过程。 在这里,我们将介绍你需要采取的步骤来设置你的 Angular 开发环境。 那么,让我们开始吧。

在 Windows 或 macOS 上安装 Node.js

按照以下步骤 在 Windows 或 macOS 上安装 Node.js:

  1. 访问官方 Node.js 网站(https://nodejs.org/en/),然后点击 LTS 版本的下载 按钮。 这将下载适用于 Windows 或 macOS 的最新版本 Node.js:

图 1.1 – Node.js 首页

图 1.1 – Node.js 首页

一旦安装程序下载完成,通过双击下载的文件来运行它。 你应该会看到 Node.js 设置向导:

图 1.2 – Node.js 安装 – 步骤 1

图 1.2 – Node.js 安装 – 步骤 1

  1. 阅读许可 协议,如果你同意条款,请点击 同意 按钮:

图 1.3 – Node.js 安装 – 步骤 2

图 1.3 – Node.js 安装 – 步骤 2

  1. 接下来,选择 你想要安装 Node.js 的位置。 默认位置 通常就很好,但如果你 更喜欢的话,可以选择不同的位置:

图 1.4 – Node.js 安装 – 步骤 3

图 1.4 – Node.js 安装 – 步骤 3

  1. 在下一屏幕上,您将被要求选择要安装的组件。

图 1.5 – Node.js 安装 – 步骤 4

图 1.5 – Node.js 安装 – 步骤 4

  1. 选择您想要创建 Node.js 开始菜单快捷方式的文件夹。

  2. 点击安装按钮开始安装过程。

图 1.6 – Node.js 安装 – 步骤 5

图 1.6 – Node.js 安装 – 步骤 5

  1. 安装完成后,您应该会看到一个消息,表明 Node.js 已成功安装。

图 1.7 – Node.js 安装 – 步骤 6

图 1.7 – Node.js 安装 – 步骤 6

  1. 为了验证 Node.js 是否已正确安装,打开命令提示符并运行以下命令:

    <st c="13704">$ node –v</st>
    

    这应该会显示您刚刚安装的 Node.js 的版本号:

图 1.8 – 检查 npm 版本

图 1.8 – 检查 npm 版本

在下一节中,我们将学习如何在 Linux 上安装 Node.js。

安装 Node.js 到 Linux

按照以下步骤在 Linux 上安装 Node.js:

  1. 打开您的终端并运行以下命令以更新包管理器:

    <st c="14050">$ sudo apt update</st>
    

    前面的命令将给出以下输出:

图 1.9 – 更新 Ubuntu 软件包

图 1.9 – 更新 Ubuntu 软件包

  1. 运行以下命令以安装 Node.js:

    <st c="16348">$ sudo apt install nodejs</st>
    

    前面的命令将给出以下输出:

图 1.10 – 在 Ubuntu 上安装 Node.js

图 1.10 – 在 Ubuntu 上安装 Node.js](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_01_10.jpg)

  1. 为了验证 Node.js 是否已正确安装,请运行以下命令:

    <st c="17816">$ node –v</st>
    

    这应该显示你刚刚安装的 Node.js 的版本号:

图 1.11 – 在 Ubuntu 上安装 Node.js 后的版本

图 1.11 – 在 Ubuntu 上安装 Node.js 后的版本

  1. npm 是 Node.js 的包管理器。 如果你还没有与 Node.js 一起安装它,请运行以下命令:

    <st c="18111">$ sudo apt install npm</st>
    

    这里是 输出结果:

图 1.12 – 在 Ubuntu 上安装 npm

图 1.12 – 在 Ubuntu 上安装 npm](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_01_12.jpg)

  1. 为了验证 npm 是否已正确安装,请运行以下命令:

    <st c="20695">$ npm –v</st>
    

    你应该看到以下输出:

图 1.13 – 安装 Node.js 后的 npm 版本

图 1.13 – 安装 Node.js 后的 npm 版本

在下一节中,我们将创建一个新的 Angular 项目。

创建一个新的 Angular 项目

随着我们的开发环境设置完毕并准备就绪,我们可以创建我们的 Angular 项目。 这涉及到使用 Angular CLI 生成我们项目的基结构,包括文件和文件夹。 一旦创建,我们就可以使用框架提供的强大工具和功能开始构建我们的 Angular 应用程序。

安装 Angular CLI 后,你可以在终端中运行以下命令来创建一个新的 Angular 项目:

 $ ng new getting-started-angular-tdd --routing

这将创建一个名为 <st c="21442">getting-started-angular-tdd</st> 的新 Angular 项目,位于当前目录中。

创建你的 Angular 项目后,你可以在终端中运行以下命令来提供服务:

 $ cd getting-started-angular-tdd
$ ng serve --open

这将启动开发服务器并在你的默认浏览器中打开你的应用程序。 在这里,你可以修改你的代码,并在你的浏览器中实时看到更改。

现在,是时候探索编写测试所涉及的不同文件了。

探索与编写测试相关的不同文件

<st c="21945">现在我们已经创建了 Angular 项目,我们将探索与在 Angular 中编写测试相关的不同文件。</st> 我将提供它们的作用以及使用它们的最佳实践。 <st c="22145">让我们开始吧。</st>

<st c="22163">*.spec.ts</st>

<st c="22179">*.spec.ts</st> 文件包含将要运行在你代码上的实际测试用例。 <st c="22208">这些文件是 Angular 测试的骨架,因为它们定义了确保你的代码按预期工作的单个测试用例。</st> 文件的名称应与被测试文件的名称匹配,并且它应位于与被测试文件相同的目录中。 <st c="22543">这些文件中的测试被组织成测试套件,这些套件是通过使用</st> describe()<st c="22639">函数定义的。</st>每个测试用例都是通过使用 <st c="22687">it()</st> 函数定义的。 <st c="22702">例如,如果你正在测试一个名为</st> MyComponent<st c="22764">》的组件,你将创建一个名为</st> my-component.spec.ts` 的文件,并在该文件中定义该组件的测试用例。

<st c="22880">The</st> <st c="22885">describe()</st> 函数用于将相关的测试用例组合在一起,它接受两个参数:一个描述测试套件的字符串和一个定义该套件内测试用例的函数。The <st c="23085">it()</st> 函数用于定义单个测试用例,它接受两个参数:一个描述测试用例的字符串和一个包含测试用例代码的函数。在测试用例函数内部,你可以使用 <st c="23311">expect()</st> 函数来定义你代码的预期行为。 <st c="23375">例如,你可能使用</st> expect(component.title).toEqual('My Title')<st c="23445">来测试组件的</st>title` 属性是否具有预期的值。

<st c="23516">*.spec.ts</st> 文件通常还会导入正在测试的组件或服务以及任何必要的依赖项。 <st c="23633">例如,如果你正在测试一个使用了</st> HttpClient<st c="23702">服务的组件,你需要从</st>HttpClientTestingModule<st c="23783">导入组件和</st>@angular/common/http/testing`

karma.conf.js 文件

The <st c="23846">karma.conf.js</st> 文件用于 配置 Karma 测试运行器,该运行器用于运行您的测试用例。 Karma 是 Angular 应用程序的一个流行的测试运行器,它提供了一种简单的方法在多种浏览器中运行您的测试。 karma.conf.js 文件指定了测试运行中应包含的文件,以及用于运行测试的浏览器。 测试。

The <st c="24227">karma.conf.js</st> 文件通常导出一个配置对象,该对象指定了这些设置。 配置对象包含几个属性,例如框架、文件、报告器和浏览器。 这些属性允许您配置测试运行的各个方面,例如使用哪个测试框架,包含哪些文件在测试运行中,使用哪些报告器来显示测试结果,以及使用哪些浏览器来运行 测试。

例如,一个典型的 <st c="24692">karma.conf.js</st> 文件可能看起来 像这样:

 module.exports = <st c="24750">function</st>(config) {
  config.set({
    frameworks: ['jasmine', '@angular/cli'],
    files: [
      { pattern: './src/test.ts', watched: false }
    ],
    reporters: ['progress', 'kjhtml'],
    browsers: ['Chrome']
  });
};

给定的配置概述了使用 <st c="25000">Jasmine</st> <st c="25011">@angular/cli</st> 框架,在测试运行期间包含 <st c="25058">./src/test.ts</st> 文件,实现 <st c="25124">progress</st> <st c="25137">kjhtml</st> 报告器来展示测试结果,以及在 Chrome 浏览器 中执行测试。

test.ts 文件

The <st c="25255">test.ts</st> 文件是您的测试用例的 入口点。 它设置测试环境并加载测试运行所需的全部必要文件。 此文件通常导入 <st c="25430">zone.js</st> 库,该库用于处理测试中的异步操作。 它还导入 Karma 测试运行器并开始 测试运行。

The <st c="25577">test.ts</st> 文件通常位于您的项目 <st c="25618">src</st> 目录中,并且通常在您创建一个新的 Angular 项目时自动生成。 此文件设置测试环境并加载测试运行所需的必要文件。 This file sets up the testing environment and loads the necessary files for the test run.

以下是一个 <st c="25848">test.ts</st> 文件可能 看起来的例子:

 // This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
// First, initialize the Angular testing environment
getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);
// Then, load all the .spec files
const context = require.context('./', true, /\.spec\.ts$/);
context.keys().map(context);

此文件使用 <st c="26491">getTestBed().initTestEnvironment()</st> 函数初始化 Angular 测试环境,该函数设置 <st c="26581">TestBed</st>。它还 加载所有 <st c="26615">*.spec.ts</st> 文件 使用 <st c="26637">require.context()</st>

tsconfig.spec.json 文件

<st c="26688">tsconfig.spec.json</st> 文件用于配置 TypeScript 编译器以处理您的测试用例。 它指定了在编译测试文件时应使用的编译器选项。 此文件通常扩展了项目的主 <st c="26901">tsconfig.json</st> 文件,但可能包含特定的测试设置。

以下是一个 <st c="27022">tsconfig.spec.json</st> 文件可能的样子:

 {
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/spec",
    "module": "commonjs",
    "target": "es5",
    "baseUrl": "",
    "types": [
      "jasmine",
      "node"
    ]
  },
  "files": [
    "src/test.ts",
    "src/polyfills.ts"
  ],
  "include": [
    "**/*.spec.ts",
    "**/*.d.ts"
  ]
}

此文件扩展了主 <st c="27347">tsconfig.json</st> 文件,并指定了应用于测试文件的编译器选项。 它还包括 <st c="27463">src/test.ts</st> <st c="27479">src/polyfills.ts</st> 文件在测试运行中。

src/test.ts 文件

<st c="27544">src/test.ts</st> 文件用于配置 Angular 测试环境。 它设置 <st c="27626">TestBed</st>,用于创建 组件和服务实例以进行测试。 它还导入任何必要的测试实用工具,例如 <st c="27771">TestBed</st> <st c="27783">async</st>

以下是一个 <st c="27818">src/test.ts</st> 文件可能的样子:

 import { TestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule,  platformBrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTestingModule()
);

此文件设置 <st c="28144">TestBed</st> 使用 <st c="28162">TestBed.initTestEnvironment()</st> 函数。 它指定了要使用的 测试模块和平台模块 ,分别是 <st c="28276">BrowserDynamicTestingModule</st> <st c="28308">platformBrowserDynamicTestingModule</st>

摘要

本章介绍了 TDD 的基础知识及其在 Angular 中的作用。 我们解释了 TDD 的好处以及它如何帮助开发者编写高质量的代码。 然后,我们讨论了如何设置 Angular 的开发环境,并使用 Angular 编程接口创建了一个新的 Angular 项目。 我们还探讨了与 Angular 中编写测试相关的各种文件,包括 <st c="28750">*.spec.ts</st>, <st c="28761">karma.conf.js</st>,<st c="28775">tsconfig.spec.json</st>,以及 <st c="28799">src/test.ts.</st> 我们为每个文件及其在测试 Angular 应用程序中的作用提供了详细的解释。 通过了解这些文件及其目的,开发者可以更有效地为他们的 Angular 应用程序编写和运行测试,并确保他们的代码表现 如预期。

在下一章中,我们将学习使用 Jasmine 编写和执行单元测试的过程,同时涵盖测试套件、测试规范和匹配器 等主题。

第三章:2

使用茉莉和卡玛测试 Angular 应用程序

茉莉 卡玛 是开发者可以用来测试他们的 Angular 应用程序的两个强大工具。 测试是开发过程中的一个重要部分,因为它有助于确保应用程序按预期工作,并避免任何潜在的错误 或问题。

茉莉是一个 行为驱动开发 (BDD) 框架 用于测试 JavaScript 代码。 它提供了一个简单易读的语法来编写测试,使得代码更容易理解和维护。 使用茉莉,开发者可以定义测试套件和测试用例,然后使用各种匹配器来检查代码的预期行为。

另一方面,卡玛是一个允许开发者在其多个浏览器和环境中执行测试的测试运行器。 它提供了与茉莉的无缝集成,使得开发者可以轻松地在不同的浏览器中运行茉莉测试,并获得测试结果的实时反馈。 卡玛还提供了一些额外的功能,例如代码覆盖率报告和持续 集成支持。

将茉莉和卡玛结合使用可以极大地增强 Angular 应用程序的测试过程。 开发者可以使用茉莉的表达式语法编写全面的测试套件,然后使用卡玛在不同的浏览器中运行这些测试,确保跨不同环境的兼容性。 这有助于在早期捕捉任何潜在的问题或错误,并促进更健壮和 可靠的应用程序。

在本章中,我们将探讨使用茉莉和卡玛测试 Angular 应用程序的基础知识。 我们将学习如何设置测试环境,使用茉莉编写单元测试,并配置卡玛在不同的浏览器中运行测试。

本章将涵盖以下主题:

  • 掌握茉莉的单元 测试技术

  • 编写你的 第一个与 Angular 相关的单元测试,关于 测试驱动开发 (TDD) (TDD)

  • 利用卡玛的代码覆盖率和测试结果分析

技术要求

为了跟随本章中的示例和练习,你需要对 Angular 和 TypeScript 有一个基本的了解,以及以下 技术要求:

  • 在您的计算机上安装了 Node.js LTS 和 npm LTS

  • 安装了 Angular 17 或更高版本的 CLI 全局

  • 您的计算机上安装了代码编辑器,例如 Visual Studio Code

本章的代码文件可以在 以下位置找到 https://github.com/PacktPublishing/Mastering-Angular-Test-Driven-Development/tree/main/Chapter%202

掌握 Jasmine 的单元测试技术

在本节中,我们将通过编写描述性测试套件、使用匹配器、使用间谍进行函数测试以及测试异步代码来探索 Jasmine 框架。 通过利用这些技术,您可以确保代码库的质量和可靠性。

什么是 Jasmine?

Jasmine 是一个广泛使用的 JavaScript 测试框架,通常用于编写 Web 应用程序和 Node.js 项目的测试。 凭借其简洁和表达性强的语法,Jasmine 允许开发者创建易于理解和维护的测试套件和案例。 它提供了断言、测试间谍和异步测试的内置功能。 Jasmine 与众多库和框架无缝集成,包括 AngularJS 和 React,使开发者能够编写全面可靠的 JavaScript 应用程序的测试。 其流行源于其简单性、灵活性和促进创建健壮可靠 JavaScript 代码测试的能力。

使用 Jasmine,开发者可以使用 BDD 风格来结构化他们的测试,这使得编写既描述性强又易于阅读的测试变得简单。它提供了一套内置的断言函数,允许开发者验证他们代码的预期行为。 这些断言涵盖了广泛的情况,使得编写验证被测试代码正确性的测试变得简单

Jasmine 还包括测试间谍等特性,这使开发者能够跟踪函数调用和参数,以及 模拟和存根函数行为。 这有助于测试与其它组件或 外部依赖项交互的代码。

此外,Jasmine 支持异步测试,这使得编写涉及异步操作(如 AJAX 请求或定时器)的代码的测试变得简单。 它提供了处理异步任务和确保测试在断言之前等待完成的机制。

Jasmine 具有高度的扩展性,可以与各种库和框架一起使用,例如 AngularJS、Angular 和 React。 它与这些生态系统无缝集成,允许开发者为其 JavaScript 应用程序编写全面且可靠的测试。

总的来说,Jasmine 的简洁性、灵活性以及 全面的功能集为其成为 JavaScript 测试框架的流行做出了贡献。 它使开发者能够编写健壮且可靠的测试,最终导致代码质量更高和部署更加 自信。

编写描述性的测试套件

有效单元测试的一个基本原则是编写描述性测试套件。 通过逻辑组织您的测试并使用描述性名称,您使自己和其他开发者更容易理解每个测试的目的和行为。 在本节中,我们将探讨创建有意义的测试套件名称和在清晰简洁的语言中描述预期行为的策略。 此外,我们还将讨论描述性测试套件如何作为未来参考的文档。

描述性测试套件是一组 相关的测试用例,专注于代码的特定功能或组件。 它作为文档工具,帮助开发者理解每个测试的目的和行为。 描述性测试套件对于维护代码质量、促进团队成员之间的协作以及确保测试随着时间的推移保持相关性和更新至关重要。 通过在创建描述性测试套件上投入时间,您可以提高测试代码的可维护性和可读性。

让我们考虑一个简单的场景,其中我们有一个名为 <st c="6183">calculateTotal</st> 的 JavaScript 函数,该函数计算购物车中商品的总价。 我们想要编写一个测试来确保当给定的商品及其 相应价格集合时,该函数返回正确的总价。

选择有意义的名称

创建描述性测试套件的第一步是为您的测试套件和测试用例选择有意义的名称。 使用清晰简洁的语言来描述正在测试的功能或行为。 避免使用模糊或通用的名称,这些名称不足以提供足够的上下文。 例如,不要将测试套件命名为“测试套件 1”,考虑将其命名为“用户身份验证测试”以传达测试的目的。 有意义的名称使开发者更容易定位特定的测试并理解其目的,即使在长时间后重新访问代码库时也是如此。

结构化测试套件

在 Jasmine 中创建描述性测试套件时,以逻辑和层次结构组织您的测试套件至关重要。 一个结构良好的测试套件反映了您的代码库的结构,这使得定位和理解特定的测试变得更容易。 将相关的测试组合在一起以提高可读性和可维护性。 例如,如果您正在测试用户身份验证模块,为登录功能创建一个特定的测试套件,并为注册创建另一个。 这种分离有助于您隔离并专注于特定的功能,使您更容易识别和解决问题。 此外,考虑使用嵌套的 describe 块来进一步组织您的测试层次结构。 以下是一个示例: 示例:

 describe("User Authentication", () => {
  describe("Login", () => {
    // Login-related test cases
  });
  describe("Registration", () => {
    // Registration-related test cases
  });
});

编写清晰简洁的测试描述

在每个测试用例中,编写清晰简洁的描述,准确描述预期的行为。 尽可能使用易于理解的语言,并避免使用技术术语。 良好的测试描述应提供足够的信息,让您和其他人无需深入了解实现细节就能理解测试的目的。 考虑使用“应该”格式来描述预期的行为——例如,“应该正确计算包含多个项目的购物车的总额。”通过使用描述性语言,未来的开发者可以快速理解测试的意图并识别任何与预期行为的偏差。

除了测试描述外,在测试代码中包含注释也有助于提供进一步的澄清或上下文,当需要时。 这些注释可以解释某些断言背后的推理或提供有关测试场景的额外信息。 然而,重要的是要找到平衡,避免过度注释,这可能会使测试代码变得杂乱。

维护和更新描述性测试套件

描述性的测试套件不是一次性的努力,但随着代码库的演变,需要持续维护和更新。定期审查和更新测试套件对于确保它们保持相关性和准确性至关重要。当对代码进行更改时,开发者还应更新相应的测试以反映更新的行为。此外,如果测试用例变得过时或冗余,应将其删除或重构。

当更新测试套件时,保持其描述性至关重要。如果测试用例需要重大更改,可能最好创建一个新的测试用例,并附上适当的描述,而不是修改现有的一个。这有助于保持测试套件的清晰性和透明度。

让我们考虑一个简单的场景,其中我们有一个名为 <st c="9887">calculateTotal</st> 的 JavaScript 函数,该函数计算购物车中商品的总价。我们想要编写一个测试来确保当给定一组具有相应价格的物品时,该函数返回正确的总额:

 // Function under test
function calculateTotal(items) {
  let total = 0;
  items.forEach(item => {
    total += item.price;
  });
  return total;
}
// Test suite
describe("calculateTotal function", () => {
  // Test case 1: Calculate total for an empty cart
  it("should return 0 for an empty cart", () => {
    const cart = [];
    const result = calculateTotal(cart);
    expect(result).toBe(0);
  });
  // Test case 2: Calculate total for a cart with multiple items
  it("should correctly calculate the total for a cart with multiple items", () => {
    const cart = [
      { name: "Item 1", price: 10 },
      { name: "Item 2", price: 15 },
      { name: "Item 3", price: 20 }
    ];
    const result = calculateTotal(cart);
    expect(result).toBe(45);
  });
});

在前面的示例中,我们为 <st c="10859">calculateTotal</st> 函数 创建了一个测试套件。在测试套件中,我们有两个测试用例,测试用例的描述清楚地说明了正在测试的行为:

  • 第一个测试用例,“对于空购物车应返回 0”, 验证了函数正确处理空购物车并返回总额为 0

  • 第二个测试用例,“应正确计算包含多个商品的购物车的总额”, 测试了包含多个商品的购物车,并检查计算出的总额是否符合预期

通过提供描述性的测试用例描述,其他开发者可以轻松理解每个测试的意图和行为。这些描述充当文档,使得在代码库演变时更容易维护和更新测试。

在下一节中,我们将探讨如何使用 TDD 原则编写我们的第一个单元测试。

在 Angular 项目中编写第一个单元测试

单元测试 是 Angular 开发的关键方面,它确保了代码质量、可靠性和可维护性。 TDD(测试驱动开发) 是一种软件开发方法,强调在实际代码实现之前编写测试。 在本节中,您将学习如何在遵循 TDD 原则的同时,在 Angular 项目中编写您的第一个单元测试。 通过利用 Jasmine 测试框架和 Angular 的测试工具,开发者可以创建有效且健壮的单元测试,以验证其代码的正确性。

我们将使用 我们在 第一章 创建的项目 进行练习。 按照以下 步骤编写您的第一个 单元测试:

  1. 通过运行以下命令创建一个名为 `CalculatorComponent 的新组件:

    <st c="12597">calculator.component.spec.ts</st> file will be created in the <st c="12654">src/ app/calculator</st> folder. When you open the file, you’ll see the following code by default:
    
    

    import { ComponentFixture, TestBed } from '@angular/core/testing';

    import { CalculatorComponent } from './calculator.component';

    describe('CalculatorComponent', () => {

    let calculator: CalculatorComponent;

    let fixture: ComponentFixture;

    beforeEach(async () => {

    await TestBed.configureTestingModule({

    declarations: [ CalculatorComponent ]

    })

    .compileComponents();

    fixture = TestBed.createComponent(CalculatorComponent);

    calculator = fixture.componentInstance;

    fixture.detectChanges();

    });

    it('should create', () => {

    expect(calculator).toBeTruthy();

    });

    });

    
    <st c="13326">In the</st> <st c="13333">preceding generated code, we have a</st> <st c="13369">test suite where we have used the</st> `<st c="13404">describe</st>` <st c="13412">function, providing a descriptive name for the component under test.</st> <st c="13482">Within the test suite, we have a</st> `<st c="13515">beforeEach</st>` <st c="13525">block to set up the test environment.</st> <st c="13564">The</st> `<st c="13568">TestBed.configureTestingModule</st>` <st c="13598">method is used to configure the test module and provide the necessary dependencies.</st> <st c="13683">The</st> `<st c="13687">calculator</st>` <st c="13697">variable is then assigned to an instance of</st> `<st c="13742">CalculatorComponent</st>` <st c="13761">using the</st> `<st c="13772">TestBed.inject</st>` <st c="13786">method.</st><st c="13794">Our</st> `<st c="13799">CalculatorComponent</st>` <st c="13818">component will enable us to perform basic arithmetic operations.</st> <st c="13884">To write a unit test using TDD, we’ll start by creating a test case that verifies the component’s</st> <st c="13982">expected behavior.</st>
    
  2. 现在,我们将 使用 <st c="14049">it</st> <st c="14051">函数</st> 编写实际的测试用例。 在这种情况下,我们将通过传递两个数字来测试 <st c="14091">add</st> <st c="14094">方法</st> <st c="14091">的</st> CalculatorComponent<st c="14124">,并期望结果为</st>5 <st c="14189">expect</st> <st c="14195">函数用于定义期望的行为并检查实际结果。</st> <st c="14274">以下代码必须添加到测试套件中——即,在</st> describe 函数` 内部:

     it('should add two numbers correctly', () => {
        const result = calculator.add(2, 3);
        expect(result).toBe(5);
     });
    });
    
  3. 您将在代码编辑器中遇到一个错误,提示您 `add 函数 不存在:

图 2.1 – 代码错误

图 2.1 – 代码错误

这是正常的,因为它还没有被 创建。

当我们返回到 Karma 服务器时,我们会看到 我们的测试用例没有显示在 <st c="14838">CalculatorComponent</st> 中,并且在终端中,我们有一个与函数不存在相关的错误,以及一条消息表明没有测试 成功。

别慌张 – 这是 TDD 的红色阶段! 做得好!

  1. 接下来,我们将实现 <st c="15073">add</st> 函数,在 <st c="15089">calculator.component.ts</st>中。定义了我们的第一个测试用例后,我们可以继续实现 <st c="15178">calculator.component.ts</st> 以使测试通过。 遵循 TDD 方法,编写通过测试所需的最少代码:

     add(a: number, b: number): number {
        return a + b;
      }
    

    你将在你的 Karma 服务器上看到以下结果:

图 2.2 – 测试成功

图 2.2 – 测试成功

在你的终端中,你会收到以下消息:

图 2.3 – 测试执行成功

图 2.3 – 测试执行成功

有了这些,我们就 进入了 TDD 的绿色阶段,并编写了通过测试所需的最少代码。 做得好!

一旦测试通过,你可以重构代码以改进其设计、可读性和可维护性。 重构是 TDD 过程中的关键步骤,因为它有助于消除重复并改进代码结构和整体质量。 确保重构后测试仍然运行是至关重要的。 随着代码库的发展,定期审查和更新测试将有助于维护单元测试的完整性和可靠性。

在我们的例子中,我们不需要重构测试。 别担心 – 我们将在 第三章有机会这样做。

在下一节中,我们将探讨如何使用 Karma 的代码覆盖率和测试结果分析

利用 Karma 的代码覆盖率和测试结果分析

代码覆盖率 和测试结果分析 是软件开发过程中的关键方面。 通过测量代码覆盖率,开发者可以评估单元测试的有效性,并识别需要额外测试的区域。 Karma 是 JavaScript 生态系统中的一个流行测试框架,它提供了内置的代码覆盖率和测试结果分析支持。 在本节中,我们将学习如何利用 Karma 来测量代码覆盖率,生成详细报告,并分析测试结果。 通过利用这些功能,开发者可以确保全面测试,并提高代码的整体质量和可靠性。

在进一步操作之前,值得注意的一点是,我们将要查看的所有配置已经存在于我们的 Angular 项目中。 当我们创建项目时,Angular 会负责所有 配置。

以下是 Angular 为我们执行的 不同步骤:

  • 步骤 1 – 使用 代码覆盖率设置 Karma:

    为了 使用 Karma 进行代码覆盖率分析,首先 安装 必要的依赖项:

    <st c="17946">karma.conf.js</st>) with the following changes:
    
    

    module.exports = function(config) {

    config.set({

    // ... reporters: ['progress', 'coverage'],
    
    coverageReporter: {
    
    dir: 'coverage/',
    
    reporters: [
    
        { type: 'html', subdir: 'report-html' },
    
        { type: 'lcov', subdir: 'report-lcov' }
    
    ]
    
    },
    
    // ... });
    

    };

    
    <st c="18234">This configuration specifies the reporters to be used (</st>`<st c="18290">progress</st>` <st c="18299">for test progress and</st> `<st c="18322">coverage</st>` <st c="18330">for code coverage).</st> <st c="18351">The</st> `<st c="18355">coverageReporter</st>` <st c="18371">section defines the output directory and the types of reports to generate (HTML</st> <st c="18452">and LCOV).</st>
    
  • <st c="18860">代码覆盖率</st> 目录用于查看生成的报告。 在网页浏览器中打开 HTML 报告(<st c="18932">coverage/report-html/index.html</st>)以可视化代码覆盖率细节。 报告突出显示已覆盖行、未覆盖行和整体覆盖率百分比。 此外,LCOV 报告(<st c="19143">coverage/report-lcov/lcov-report/index.html</st>)提供了代码覆盖率的更详细分解。

  • <st c="19682">mocha-reporter</st> 显示 关于测试失败的详细信息,包括堆栈跟踪和错误消息,而 <st c="19799">junit-reporter</st> 生成 JUnit 风格的 XML 报告,这些报告可以被 CI 工具用于 进一步分析。

    要将 Karma 与 CI 工具集成,请在您的 Karma 配置文件中配置相应的插件或报告器。 例如,要为 Jenkins 生成 JUnit 报告,添加 <st c="20075">karma-junit-reporter</st> 插件并相应配置。

  • 步骤 4 – 利用阈值和 质量门

    Karma 允许开发者定义代码覆盖率和测试结果 的阈值和质量门。 通过设置这些阈值,开发者 可以建立代码覆盖率和测试成功率的最小要求。 这确保了代码库保持一定的质量水平,并减少了发布未测试或测试覆盖率低代码的风险。

    为了设置代码覆盖率阈值,请更新您的 Karma 配置文件 如下:

     module.exports = function(config) {
      config.set({
        // ... coverageReporter: {
          // ... check: {
            global: {
              statements: 80,
              branches: 80,
              functions: 80,
              lines: 80
            }
          }
        },
        // ... });
    };
    

    在这个 示例中,阈值已设置为 80%,用于语句、分支、函数和 行。 如果这些阈值中的任何一个未 达到,Karma 将报告失败的 测试结果。

代码覆盖率可视化

在我们的项目中,我们首先在 CalculatorComponent上编写了测试。现在,我们可以使用 Karma 查看代码覆盖率。 在我们的 项目终端中运行以下命令:

<st c="21183">$ ng test –code-coverage</st>

执行前面的命令后,我们将观察到以下 三个要点:

  • 如果一切顺利,在终端中我们将看到以下内容:

图 2.4 – 终端中的测试覆盖率

图 2.4 – 终端中的测试覆盖率

  • Karma 启动我们的浏览器,显示我们执行的各种测试:

图 2.5 – 成功的测试

图 2.5 – 成功的测试

  • 在我们的 项目结构 中创建了一个 覆盖率 文件夹:

图 2.6 – 测试覆盖率文件夹

图 2.6 – 测试覆盖率文件夹

内部是一个 <st c="22695">index.html</st> 文件。 当我们用浏览器打开这个文件时,我们会看到一个表格,总结所有测试过的文件,并且在每个文件中,我们会被告知给定代码片段的测试覆盖率。 有多少已经被测试。

以下 截图显示了 测试覆盖率:

图 2.7 – 网页上的测试覆盖率可视化 – 第一部分

图 2.7 – 网页上的测试覆盖率可视化 – 第一部分

图 2.8 – 网页上的测试覆盖率可视化 – 第二部分

图 2.8 – 网页上的测试覆盖率可视化 – 第二部分

图 2.9 – 网页上的测试覆盖率可视化 – 第三部分

图 2.9 – 网页上的测试覆盖率可视化 – 第三部分

图 2.10 – 网页上的测试覆盖率可视化 – 第四部分

图 2.10 – 网页上的测试覆盖率可视化 – 第四部分

图 2.11 – 网页上的测试覆盖率可视化 – 第五部分

图 2.11 – 网页上的测试覆盖率可视化 – 第五部分

通过使用代码覆盖率以及 Karma 的测试结果分析,开发者可以提升他们的测试实践并确保全面的代码覆盖率。Karma 内置的代码覆盖率支持使得开发者能够衡量测试的有效性并识别需要额外关注的区域。 此外,Karma 的测试报告和集成能力允许对测试结果进行更深入的分析,使开发者能够跟踪测试套件的健康状况并识别测试失败的模式。 通过设置阈值和质量门,开发者可以建立代码覆盖率和测试成功率的最小要求,确保更高的代码质量和可靠性。 和可靠性。

总结

本章介绍了如何设置测试环境,使用 Jasmine 编写单元测试,并配置 Karma 在不同的浏览器中运行测试。 Jasmine 和 Karma 是测试 Angular 应用程序的强大工具。 Jasmine 是一个 BDD 框架,它为编写测试用例提供了直观的语法。 另一方面,Karma 是一个测试运行器,允许你在各种环境中执行测试,并提供代码覆盖率和测试结果分析等功能。 结果分析。

要使用 Jasmine 和 Karma 测试 Angular 应用程序,你需要通过安装必要的依赖项和配置 Karma 来设置测试环境。 Jasmine 提供了一套丰富的匹配器和断言,用于验证 Angular 组件、服务和指令的行为。 你可以创建测试套件和测试用例来覆盖不同的场景 和期望。

Karma 允许你在真实浏览器或无头环境中运行测试,这使得模拟用户交互并测试应用程序在不同平台上的行为变得容易。 它还提供了代码覆盖率支持,生成报告以帮助识别代码库中需要 额外测试的区域。

通过结合使用 Jasmine 和 Karma,你可以为你的 Angular 应用程序编写完整的单元测试,并实践 TDD 的原则。

在下一章中,我们将学习如何为 Angular 组件、服务和指令编写有效的单元测试。

第二部分:编写有效的单元测试

在本部分中,你将使用管道、表单和响应式编程编写组件、服务和指令的单元测试,同时遵循 TDD 原则。

本部分包含以下章节: 以下章节:

  • 第三章, 为 Angular 组件、服务和指令编写有效的单元测试

  • 第四章, 在 Angular 测试中模拟和存根依赖

  • 第五章, 测试 Angular 管道、表单和响应式编程

第四章:3

为 Angular 组件、服务和指令编写有效的单元测试

在本章中,我们将深入探讨如何为 Angular 组件、服务和指令编写有效的单元测试。 我们将继续上一章的内容,深入编写 Angular 组件的单元测试。 组件是 Angular 应用程序的基本构建块,彻底测试它们至关重要。 在本章中,我们将学习如何设置测试环境,创建组件实例,并测试组件属性、方法和事件处理。 我们还将探讨测试组件模型的技术,包括 DOM 操作和 事件模拟。

我们还将关注 Angular 服务的单元测试。 服务在 Angular 应用程序中扮演着至关重要的角色,提供可重用的逻辑和数据操作。 我们将学习如何创建服务实例,模拟测试服务方法,并进行 数据操作。

最后,我们将探讨如何对 Angular 指令进行单元测试。 指令是操作 DOM 和改进我们应用程序行为的强大工具。 我们将学习如何为指令设置测试环境,创建指令实例,并测试其行为以及与 DOM 的交互。

在本章中,我将提供实际示例和真实场景,以说明有效的单元测试概念和技术。 我们还将讨论常见的单元测试陷阱和挑战,并提供克服它们的策略。

总结来说,以下是一些主要话题,我们将 进行探讨:

  • Angular 单元测试的高级技术:生命周期钩子和 依赖关系

  • Angular 单元测试的高级技术: Angular 服务

  • 使用严格的指令测试来确保适当的渲染 和功能

在本章结束时,你将牢固地理解如何为 Angular 组件、服务和指令编写有效的单元测试。 你将具备确保 Angular 代码质量和可靠性的知识和工具。 因此,让我们深入探讨,掌握编写有效单元测试的技巧,以 Angular 应用程序。

技术要求

为了跟随本章中的示例和练习,你需要对 Angular 和 TypeScript 有基本的了解,以及 以下内容:

  • 在您的计算机上安装了 Node.js 和 npm

  • 全局安装 Angular CLI

  • 在您的计算机上安装了代码编辑器,例如 Visual Studio Code

该章节的代码文件可以在以下位置找到

高级 Angular 单元测试技术 – 生命周期钩子

在本节中,我们将了解如何利用生命周期钩子来管理 Angular 组件的单元测试中的依赖关系。您将获得编写健壮和高效单元测试所需的知识和技能,以确保您的 Angular 应用程序的质量和稳定性。 让我们深入探讨 Angular 组件的高级单元测试技术。

发现生命周期钩子

Angular 提供了几个生命周期钩子,允许我们在组件生命周期的特定阶段执行操作。测试这些钩子确保我们的组件按预期行为。 但在我们深入探讨测试生命周期的主题之前,让我们 看看一些 Angular 的 生命周期方法:

  • <st c="3412">ngOnInit()</st>: The <st c="3430">ngOnInit()</st> hook is called after the component has been initialized. In our <st c="3505">Calculator</st> component, we can use this hook to set the initial values and perform any necessary setup. To test <st c="3615">ngOnInit()</st>, we can verify whether the initial values are correctly set and whether any necessary setup is performed.

  • <st c="3731">ngOnChanges()</st>: The <st c="3752">ngOnChanges()</st> hook is called whenever there are changes to the component’s input properties. In our <st c="3852">Calculator</st> component, we can use this hook to update the component state based on the changes. To test <st c="3955">ngOnChanges()</st>, we can simulate changes to the input properties and verify whether the component state is updated accordingly.

  • <st c="4080">ngOnDestroy()</st>: The <st c="4101">ngOnDestroy()</st> hook is called just before the component is destroyed. In our <st c="4177">Calculator</st> component, we can use this hook to clean up any resources or subscriptions. To test <st c="4272">ngOnDestroy()</st>, we can simulate the component destruction and verify whether the necessary cleanup actions are performed.

在下一节中,我们将学习如何测试 Angular 组件中存在的依赖关系。

实际应用

We will continue our project from the previous chapter, namely the Calculator application. We’ll start testing the expected behavior when we initialize our calculator. When our calculator is launched, the result displayed should be <st c="4744">0</st>, since no operation has been performed. To do this, we’ll test the <st c="4813">ngOnInit()</st> method of the Angular lifecycle, which allows us to initialize our component.

<st c="4905">calculator.component.spec.ts</st>中,我们将添加以下 单元测试:

 it('should initialize result to 0', () => {
    calculator.ngOnInit();
    expect(calculator.result).toEqual(0);
  });

编写此代码片段后,你会注意到代码中有错误,因为 <st c="5171">result</st> 不是 <st c="5205">Calculator</st> 类的属性:

图 3.1 – ngOnInit 方法测试用例出错

图 3.1 – ngOnInit 方法测试用例出错

Don’t forget the principles of TDD. It’s normal for the test to fail in the first instance and then for you to write the minimum amount of code necessary for the test to succeed. In the meantime, we need to rectify the problem with our <st c="5617">result</st> class attribute by declaring it in our component.

我们的组件 现在应该看起来像这样:

图 3.2 – 在 calculator.component.ts 中添加 add 方法

图 3.2 – 在 calculator.component.ts 中添加 add 方法

现在声明问题已经解决,让我们专注于我们的红色测试。 我们得到一个错误,告诉我们结果应该初始化为 <st c="6264">0</st>:

图 3.3 – calculator.component.ts 测试失败

图 3.3 – calculator.component.ts 测试失败

为了解决这个问题,我们 只需要在我们的 <st c="7046">result</st> 值中初始化 <st c="7062">0</st> 在我们的 <st c="7071">ngOnInit()</st> 方法中:

 ngOnInit(): void {
    this.result = 0;
  }

结果,我们的测试变成了绿色。 做得好!

图 3.4 – calculator.component.ts 测试成功

图 3.4 – calculator.component.ts 测试成功

我们现在已经测试了 <st c="7526">ngOnDestroy()</st> 方法,确保我们的组件以预期的值初始化,同时尊重 TDD 的原则。

这是其他生命周期方法应采用的相同哲学。 为了能够测试我们的 <st c="7780">ngOnDestroy()</st> 方法,我们在业务逻辑中添加了一个小层。

让我们假设以下场景。

我们有一个处理计算器各种算术运算并返回结果给我们的服务。 这个服务被注入到我们的 <st c="8030">CalculatorComponent</st> 中,以调用各种方法,这些方法会在算术运算后返回结果。

正如我们现在所习惯的,我们首先在我们的 <st c="8198">CalculatorService</st> 测试文件中初始化 <st c="8236">我们的</st> CalculatorComponent`

图 3.5 – 声明 CalculatorService 的一个实例

图 3.5 – 声明 CalculatorService 的一个实例

然后,由于它是一个服务,我们需要在t <st c="8583">providers</st> 数组中声明它:

图 3.6 – 在提供者数组中添加 CalculatorService

图 3.6 – 在提供者数组中添加 CalculatorService

最后,我们将注入它,以便在描述性 测试套件中使用:

图 3.7 – 在测试上下文中注入 CalculatorService

图 3.7 – 在测试上下文中注入 CalculatorService

现在我们可以创建我们的服务,使其被测试文件识别。 在我们的项目的 <st c="9370">src</st> 文件夹中,创建一个包含一个 <st c="9391">core</st> 文件夹的 <st c="9416">services</st> 文件夹。 基本上,你将 拥有 <st c="9456">src/core/services</st>

在终端中打开 <st c="9484">services</st> 文件夹,并运行以下命令:

 ng g s calculator

一旦服务被创建,将其导入测试文件,错误将从你的 代码编辑器中消失:

 import { CalculatorService } from 'src/core/services/calculator.service';

根据我们的 场景,服务现在负责执行算术运算。 因此,我们将 我们 <st c="9883">add()</st> 方法中移动逻辑到 服务中。

这是在 测试端的样子:

图 3.8 – Add 方法测试用例

图 3.8 – Add 方法测试用例

我们已经重构了我们的 <st c="10245">应该正确添加两个数字</st> 测试套件。 之前,计算是在组件中直接执行的。 现在,它被转移到负责这项工作的服务中。 计算后,服务将结果返回给 组件。

服务的 add() 方法必须执行求和并返回 结果。

组件的 <st c="10571">add()</st> 方法调用服务的 <st c="10607">add()</st> 方法以检索 结果。

现在我们只需要 声明 <st c="10678">add()</st> 方法在我们的服务中,这样我们的编辑器代码就不再 包含错误:

图 3.9 – 在 calculator.service.ts 中添加 add 方法声明

图 3.9 – 在 calculator.service.ts 中添加 add 方法声明](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_03_9.jpg)

我们的测试现在编译并显示 一个错误:

图 3.10 – add 方法测试用例失败

图 3.10 – add 方法测试用例失败

我们现在可以使用最少的代码将我们的服务变为绿色。 为此,我们的 服务的 <st c="12443">add()</st> 方法必须返回一个数字(我们将限制自己为整数)。 然后它接受两个参数,即数字 <st c="12565">a</st> <st c="12570">b</st>

 add(a: number, b: number): number {}

最后,它必须返回 <st c="12645">a</st> <st c="12650">b</st>的和:

 add(a: number, b: number): number {
    return a + b;
  }

现在工作已经在服务端完成,我们需要更新我们的组件代码。 首先,我们将服务注入到我们的 组件构造函数中:

 constructor(private calculatorService: CalculatorService) {}

然后,我们的 组件的 <st c="12938">add() 方法 变成这样:

 add(a: number, b: number): void {
    this.result = this.calculatorService.add(a, b);
  }

如果您注意到,我们已经从返回一个 <st c="13108">数字</st> 值的函数转变为返回一个 <st c="13126">void</st>。我们的服务直接返回 <st c="13136">result</st> 值。 如果一切顺利,<st c="13208">ng test</st> 命令将在屏幕上返回以下内容:

图 3.11 – calculator.component.ts 测试用例成功

图 3.11 – calculator.component.ts 测试用例成功

我们刚刚使用我们的服务执行了一个小的依赖测试。 在下一节中,我们将进一步探讨与我们的服务相关的测试。

Angular 单元测试的先进技术 – Angular 服务

在本节中,我们将探讨用于单元测试 Angular 服务的先进技术。 我们将深入研究测试服务的各个方面,包括测试方法、HTTP 请求、可观察对象和错误处理。 通过掌握这些技术,您将能够为您的 Angular 服务编写全面且健壮的单元测试,确保它们按预期执行并能优雅地处理各种 场景。

测试服务方法

服务通常包含 执行特定操作或逻辑的方法。 这些方法可以单独测试以确保它们产生预期的结果。 通过模拟任何依赖并提供适当的输入,您可以测试服务方法的行为了解 它们的输出。

我们在上一节中已经开始了对服务add() 方法进行测试。 重复是教育的,我们将实现其他方法,即减法、乘法和除法。

让我们打开我们的 <st c="14833">calculator.component.spec.ts</st> 文件,并继续编写与我们的业务逻辑相关的测试,遵循 TDD 原则。

我们将编写我们的红色测试,用于减去两个数字。 由于我们已经有了对 加法功能 的经验,我们可以从中汲取灵感:

图 3.12 – 没有某些错误的减法方法测试代码

图 3.12 – 没有某些错误的减法方法测试代码

明显,我们还没有实现减法方法,因为 <st c="15451">subtract</st> 方法还不存在。我们的测试甚至无法运行。让我们看看我们的 <st c="15537">calculator.service.ts</st> 服务并添加它。记住,我们需要尽可能少地编写代码:

图 3.13 – calculator.service.ts 中的减法方法声明

图 3.13 – calculator.service.ts 中的减法方法声明

《st c="15766">另一方面,在我们的 <st c="15794">calculator.component.spec.ts</st> 中,注意红色变少了,但仍然有一些,如图所示:</st c="15892">

图 3.14 – 在 calculator.component.ts 中实现减法方法之前的减法方法测试

图 3.14 – 在 calculator.component.ts 中实现减法方法之前的减法方法测试

《st c="16236">我们的 <st c="16241">calculator.component.ts</st> 组件缺少 <st c="16290">subtract()</st> 方法。</st c="16301">就像我们处理 <st c="16329">add()</st> 一样,我们将从中汲取灵感:</st c="16369">

图 3.15 – 在 calculator.component.ts 中添加减法方法

图 3.15 – 在 calculator.component.ts 中添加减法方法

《st c="16542">在我们的 <st c="16561">calculator.component.spec.ts</st> 测试文件中的结果与预期相符:</st c="16589">

图 3.16 – 没有错误的减法方法测试用例

图 3.16 – 没有错误的减法方法测试用例

当我们在终端中浏览时,我们得到以下预览:

图 3.17 – 我们应用的测试覆盖率

图 3.17 – 我们应用的测试覆盖率

我们将对乘法和除法进行相同的练习。在我们的 <st c="17317">calculator.component.spec.ts</st> 中,我们会得到以下结果:

图 3.18 – 添加乘法和除法方法测试用例

《st c="17826">图 3.18 – 添加乘法和除法方法测试用例</st c="17826">

然后在我们的 <st c="17898">calculator.service.ts</st> 服务中,我们有以下内容:

图 3.19 – 在 calculator.service.ts 中添加乘法和除法方法

《st c="18094">图 3.19 – 在 calculator.service.ts 中添加乘法和除法方法</st c="18094">

最后,在我们的 <st c="18188">calculator.component.ts</st> 中,我们有以下内容:

图 3.20 – 在 calculator.component.ts 中添加乘法和除法方法

图 3.20 – 在 calculator.component.ts 中添加乘法和除法方法

在我们的 终端中,我们可以看到 以下内容:

图 3.21 – 我们应用的测试覆盖率

图 3.21 – 我们应用的测试覆盖率

在我们的浏览器中,我们可以看到 以下内容:

图 3.22 – calculator.component.ts 测试用例成功

图 3.22 – calculator.component.ts 测试用例成功

在下一节中,我们将使事情变得更有趣。 我们将把我们的 <st c="20101">result</st> 变量转换为一个可观察对象。 这样,我们就不必每次在组件的计算方法中调用它。 这将使我们能够了解如何测试 一个可观察对象。

使用严格的指令测试以确保适当的渲染和功能

Angular 指令在构建和增强 Web 应用程序的功能方面发挥着至关重要的作用。 它们允许开发者操作 DOM,创建可重用组件,并提供动态行为。 指令测试是验证指令是否正确渲染并按预期工作的过程。 通过彻底测试指令,开发者可以在它们影响应用程序的整体性能和 用户体验之前识别并修复问题。

在我们的当前计算器应用程序开发项目中,我们将使用一个指令来将颜色应用到屏幕上显示的计算结果。

实现颜色更改指令

为了处理我们的 Angular 计算器应用程序中的颜色 更改,我们将创建一个自定义指令。 指令允许我们扩展 HTML 元素的功能并封装 特定的行为。

在这种情况下,我们将创建一个名为 <st c="21274">colorChange</st> 的指令,该指令将负责处理颜色过渡。 该指令将接受一个输入参数,指定要更改的颜色。 然后它将应用所需的 CSS 样式以实现 期望的效果。

要创建 指令,请按照 以下步骤操作:

  1. 在项目 <st c="21577">core</st> 文件夹中创建一个 <st c="21552">directives</st> 文件夹。 因此,我们将基本上有 <st c="21630">src/core/directives</st>,并且我们将在 <st c="21722">directives</st> 文件夹中执行以下命令:

    <st c="21796">calculator.module.ts</st> file, we’ll import our directive into the declarations table:
    

图 3.23 – 在 CalculatorModule 的声明数组中添加 ColorChangeDirective

图 3.23 – 在 CalculatorModule 的声明数组中添加 ColorChangeDirective

  1. 然后,在我们的 <st c="22301">color-change.directive.ts</st> 文件中,在 <st c="22340">selector</st> 属性中,我们将用简单的 <st c="22374">appColorChange</st> 替换为 <st c="22396">colorChange</st>:

图 3.24 – 修改 ColorChangeDirective 的选择器名称

图 3.24 – 修改 ColorChangeDirective 的选择器名称

现在我们已经创建了 colorChange 指令,让我们继续到下一部分,在那里我们将为这个新创建的指令编写测试。 我们将编写测试来检查这个新创建的指令。 的测试。

为 colorChange 指令编写测试

由于我们遵循 TDD 方法,我们的测试应该检查我们指令的使用。 由于我们遵循 TDD 方法,我们的测试应该检查我们指令的使用。

当我们通过指令将颜色应用到我们的 HTML 内容中时,该颜色应该改变。 我们的测试将自然失败,因为我们还没有为我们的 <st c="23038">colorChange</st> 指令 编写适当的代码。

随后,我们将编写测试通过所需的最小代码量,并在必要时进行重构。 通过测试,并在必要时进行重构。 重构。

在我们的 <st c="23184">color-chnage.directive.spec.ts</st> 文件中,我们有 以下内容:

 import { ColorChangeDirective } from './color-change.directive';
describe('ColorChangeDirective', () => {
  it('should create an instance', () => {
    const directive = new ColorChangeDirective();
    expect(directive).toBeTruthy();
  });
});

我们将修改前面的代码,并按照预期的逻辑完成测试套件。 的预期逻辑。

在前面的代码中,当我们的指令被创建时,我们注意到编写的测试通过创建一个对象来检查实例是否存在。 在我们的场景中,我们不会这样做。 我们将在 <st c="23802">configureTestingModule</st> 方法中直接声明我们的指令,这保证了它的存在以及无需通过 构造函数 访问它的可能性。 这将给我们以下结果:

 import { TestBed } from '@angular/core/testing';
import { ColorChangeDirective } from './color-change.directive';
describe('ColorChangeDirective', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ColorChangeDirective],
    }).compileComponents();
  });
});

<st c="24270">前面的代码将是我们的起点。</st> <st c="24318">现在快速提醒一下。</st> <st c="24344">当我们想在 HTML 标签上使用一个接受属性作为参数的指令时,它看起来是这样的:</st> <st c="24444">:</st>

 <p [colorChange]="color"> </p>

根据前面的代码,<st c="24520">colorChange</st> <st c="24531">是我们的指令。</st> <st c="24550">它接受</st> <st c="24559">color</st> <st c="24564">作为参数。</st> <st c="24581">这意味着颜色是我们组件的一个属性。</st> <st c="24639">因此,我们将调用我们的</st> <st c="24667">CalculatorComponent</st> <st c="24686">以测试套件,与指令链接,以便我们可以与之交互。</st> <st c="24765">下面是它的样子:</st> <st c="24780">:</st>

 import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ColorChangeDirective } from './color-change.directive';
import { CalculatorComponent } from 'src/app/calculator/calculator.component';
describe('ColorChangeDirective', () => {
  let fixture: ComponentFixture<CalculatorComponent>;
  let calculator: CalculatorComponent;
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ColorChangeDirective, CalculatorComponent],
    }).compileComponents();
    fixture = TestBed.createComponent(CalculatorComponent);
    calculator = fixture.componentInstance;
    fixture.detectChanges();
  });
});

<st c="25409">我们知道</st> <st c="25423">我们需要选择我们的段落</st> <st c="25456">p</st> <st c="25457">,在</st> <st c="25476">CalculatorComponent</st> <st c="25495">组件中,以改变段落的颜色</st> <st c="25534">p</st> <st c="25535">,如我们所愿。</st> <st c="25552">由于组件中只有一个段落,以下是我们的操作步骤:</st> <st c="25614">:</st>

 import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ColorChangeDirective } from './color-change.directive';
import { CalculatorComponent } from 'src/app/calculator/calculator.component';
describe('ColorChangeDirective', () => {
  let fixture: ComponentFixture<CalculatorComponent>;
  let calculator: CalculatorComponent;
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ColorChangeDirective, CalculatorComponent],
    }).compileComponents();
    fixture = TestBed.createComponent(CalculatorComponent);
    calculator = fixture.componentInstance;
    fixture.detectChanges();
  });
  it('should apply the specified color', () => {
    const element: HTMLElement = fixture.debugElement.query(By.css('p')).nativeElement;
    const color: string = 'red';
    calculator.color = color;
    fixture.detectChanges();
    expect(element.style.color).toBe(color);
  });
});

<st c="26547">在前面代码中,我们已经使用</st> <st c="26615">By</st> <st c="26617">. 因此,由于</st> <st c="26633">color</st> <st c="26638">属性将用于定义段落的颜色,我们将在测试套件中使用它。</st> <st c="26734">代码编辑器将</st> <st c="26761">color</st> <st c="26766">用红色突出显示,因为我们还没有在我们的组件中声明它。</st> <st c="26827">我们将</st> <st c="26833">对我们的</st> <st c="26867">组件进行必要的更改。</st>

<st c="26882">在</st> <st c="26890">CalculatorComponent</st> <st c="26909">类中,我们将声明</st> <st c="26935">color</st> <st c="26940">属性:</st>

图 3.25 – 在 calculator.component.ts 中添加颜色属性

<st c="26984">图 3.25 – 在 calculator.component.ts 中添加颜色属性</st>

<st c="27051">在 HTML 文件中,我们有</st> <st c="27078">以下内容:</st>

 <p [colorChange]="color"> {{ result }} </p>

在前面的代码中,请注意,<st c="27172">[colorChange]</st><st c="27185">="color"</st> <st c="27194">在我们的</st> <st c="27225">HTML 模板中</st>被视为一个错误:

图 3.26 – 添加带有错误的 colorChange 指令

<st c="27548">图 3.26 – 添加带有错误的 colorChange 指令</st>

<st c="27606">这是正常的,因为我们的指令缺少一个声明。</st> <st c="27666">由于指令接受一个属性作为参数,我们需要</st> <st c="27732">声明它。</st>

<st c="27743">在我们的</st> <st c="27777">color-change.directive.ts</st> <st c="27802">指令中,我们需要做的是:</st>

 import { Directive, Input } from '@angular/core';
@Directive({
  selector: '[colorChange]',
})
export class ColorChangeDirective {
  @Input() colorChange!: string;
  constructor() {}
}

在我们的 HTML 模板 计算器 组件中,没有 更多错误:

图 3.27 – 无错误地添加 colorChange 指令

图 3.27 – 无错误地添加 colorChange 指令

尽管如此,我们仍然会有 <st c="28206">calculator.component.spec.ts</st> 测试用例失败,并且当我们在运行 测试时会在屏幕上显示这些失败:

图 3.28 – 由于 colorChange 导致 calculator.component.ts 测试失败

图 3.28 – 由于 colorChange 导致 calculator.component.ts 测试失败

为了解决这个问题,我们需要在我们的 CalculatorComponent的测试文件 中声明我们的指令,即 <st c="32213">calculator.component.spec.ts</st>

图 3.29 – 更新 calculator.component.spec.ts 中的 beforeEach 方法

图 3.29 – 更新 calculator.component.spec.ts 中的 beforeEach 方法

然后我们只会有一处错误:

图 3.30 – 由于 colorChange 导致 calculator.component.ts 测试失败

图 3.30 – 由于 colorChange 导致 calculator.component.ts 测试失败

This 错误是由于我们还没有为我们的指令编写逻辑。 我们需要编写最少的代码以使测试通过:

 import { Directive, ElementRef, Input, OnInit, Renderer2 } from '@angular/core';
@Directive({
  selector: '[colorChange]',
})
export class ColorChangeDirective implements OnInit {
  @Input() colorChange!: string;
  constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
  ngOnInit() {
    this.renderer.setStyle(this.elementRef.nativeElement, 'color', this.colorChange);
  }
}

在前面的 代码中,我们实现了 <st c="33715">ngOnInit()</st> 生命周期,以确保指令被加载到 DOM 中。 然后我们在构造函数中注入了依赖项,即 <st c="33848">ElementRef</st> <st c="33863">Renderer2</st>,以操作 HTML 元素并应用样式。 结果可以在以下屏幕截图中看到:

图 3.31 – ColorChangeDirective 测试成功

图 3.31 – ColorChangeDirective 测试成功

通过实现自定义指令并遵循 TDD 原则编写严格的测试,我们可以确保我们的应用程序按预期运行,为用户提供视觉上吸引人和 交互式的体验。

总结

本章涵盖了组件测试的各个方面,包括初始化组件、渲染模板、处理事件以及操作 DOM。 它解释了如何使用 Angular 的测试工具,例如 <st c="35003">TestBed</st> <st c="35015">ComponentFixture</st>,在测试期间设置和交互组件。 它提供了关于测试 Angular 服务的见解,包括测试服务方法和处理依赖关系。 它探讨了 Angular 指令的测试,重点关注测试指令行为和与 DOM 的交互。 它解释了如何有效地测试指令属性、输入和 输出。

在下一章中,我们将探讨如何在 Angular 中模拟和存根依赖

第五章:4

在 Angular 测试中模拟和存根依赖

为了为 Angular 应用编写有效和可靠的测试,理解如何处理依赖是至关重要的。依赖通常会增加复杂性并使测试变得困难。然而,通过利用模拟和存根等技术,我们可以更好地控制我们的测试并确保应用程序的准确性和稳定性。

在本章中,我们将探讨间谍和方法替代的概念。 间谍允许我们在测试期间监控和验证依赖的行为。我们将学习如何使用 Jasmine 测试框架创建间谍,并使用它们来找出某些方法是否被调用,它们被调用了多少次,以及调用时使用了什么参数。此外,我们还将发现方法替代的力量,它允许我们用自己的自定义逻辑替换方法的实现。

接下来,我们将看看 TestBed 提供者和它们如何允许我们将模拟依赖注入到测试中。 <st c="933">TestBed</st> 提供者允许我们创建一个测试模块,并使用必要的依赖来配置它。TestBed 是一个强大的 Angular 测试实用工具,它允许我们创建一个测试模块,并使用必要的依赖来配置它。我们将学习如何创建和配置 TestBed 提供者,用模拟版本替换真实依赖。 这种技术允许我们将测试的组件或服务隔离出来,并控制其依赖的行为。

最后,我们将探讨在设置依赖时如何处理异步操作和复杂场景。 我们将发现 Angular 测试框架提供的 <st c="1528">async</st> <st c="1538">fakeAsync</st> 实用工具,并了解如何使用它们来管理测试中的异步代码。此外,我们还将讨论处理复杂场景的策略,例如具有多个方法的依赖或需要特定初始化步骤的依赖。

总结来说,本章将涵盖以下主要主题:

  • 使用方法存根和存根来监控和控制依赖调用

  • 使用 TestBed 提供者注入模拟依赖

  • 处理异步操作和复杂场景

技术要求

为了跟随本章中的示例和练习,你需要对 Angular 和 TypeScript 有基本的了解。你还需要以下内容:

  • 在您的计算机上安装了 Node.js 和 npm

  • 全局安装了 Angular CLI

  • 在您的计算机上安装了代码编辑器,例如 Visual Studio Code

本章的代码文件可以在github.com/PacktPublishing/Mastering-Angular-Test-Driven-Development/tree/main/Chapter%204找到。

使用方法占位符和间谍监控和控制依赖调用

在 Angular 应用程序的测试中,一个关键方面是能够监控和控制依赖调用。依赖项是代码正确运行所依赖的外部资源或服务。监控和控制这些依赖调用允许开发者确保他们的代码与外部系统正确交互,并优雅地处理不同的场景。

间谍和方法占位符是 Angular 测试框架中的两种强大技术,使开发者能够实现这种程度的控制。间谍允许开发者监控函数调用,记录有关这些调用的信息,并断言对其使用的期望。另一方面,方法占位符提供了一种用简化版本替换真实依赖的方法,允许开发者在测试期间控制这些依赖的行为。

通过使用间谍,开发者可以验证是否以正确的参数调用了正确的函数,并且它们被调用的次数符合预期。这在测试与外部 API 或数据库交互的代码时特别有用。另一方面,方法占位符使开发者能够模拟不同的场景,并为方法调用提供预定义的响应。这允许对边缘情况进行彻底的测试,并确保代码的健壮性。

在本节中,我们将探讨 Angular 测试框架中间谍和方法的占位符概念。我们将深入探讨它们的应用,并展示它们在监控和控制依赖调用中的实用性。仍然基于我们与计算器应用相关的项目,我们将演示如何使用间谍和方法替代品来创建可靠和完整的测试,重点在于测试驱动开发(TDD)的原则。

方法占位符和间谍

<st c="4432">方法存根,也</st> <st c="4451">被称为模拟或哑对象,在测试期间用来用简化版本替换真实依赖。</st> <st c="4563">通过提供对方法调用的预定义响应,方法替代表示开发者可以隔离和控制被测试代码的行为</st> <st c="4699">。</st>

<st c="4710">在计算器应用程序中,让我们考虑一个用户执行除法操作,除数等于零的场景。</st> <st c="4846">我们想要确保应用程序能够正确处理这个场景。</st> <st c="4921">通过为除法函数创建一个方法插件,我们可以模拟除以零的场景,并检查应用程序是否显示适当的</st> <st c="5071">错误消息。</st>

<st c="5085">目前,我们的计算器的除法操作没有处理除以零相关的异常。</st>

在我们的<st c="5192">calculator.component.spec.ts</st> <st c="5200">测试文件</st> <st c="5228">中,我们将添加一个测试,使我们能够引发这个异常。</st> <st c="5308">由于我们遵循 TDD 原则,测试应该</st> <st c="5362">自然失败。</st>

<st c="5377">运行我们的测试后,我们注意到测试确实失败了,如下面的截图所示:</st> <st c="5461">:</st>

图 4.1 – 除以零测试用例

<st c="5745">图 4.1 – 除以零测试用例</st>

图 4.2 – 除以零测试用例失败

<st c="6446">图 4.2 – 除以零测试用例失败</st>

<st c="6492">为了纠正这个问题,我们需要更新我们的</st> <st c="6532">CalculatorService</st> <st c="6549">。在我们的当前场景中,有一个巧妙的方法可以避免直接与我们的核心服务交互,并确保</st> <st c="6669">在采取任何此类行动之前一切正常运作。</st> <st c="6727">这种方法涉及使用一个方法</st> <st c="6778">存根概念。</st>

<st c="6791">基本上,我们将我们的</st> <st c="6819">CalculatorService</st> <st c="6836">服务</st> <st c="6840">指向一个模拟服务,这将使我们能够在修改服务本身之前检查我们想要实现的逻辑的正确性。</st> <st c="6980">实际上,这个模拟服务将仅仅是一个存根方法,用来替换我们基本</st> <st c="7084">CalculatorService</st> <st c="7101">服务的经典除法。</st> <st c="7111">首先,你需要注释掉所有与我们的</st> <st c="7144">calculator.component.spec.ts</st> <st c="7229">文件中其他运算符相关的测试,如果你已经有了的话。</st> <st c="7262">接下来,我们将声明这个</st> <st c="7287">存根方法:</st>

 const calculatorServiceStub = {
  divide: (a: number, b: number) => {
        if (b === 0) {
      throw new Error('Cannot divide by zero');
    }
    return a / b;
  },
};

然后,在 <st c="7460">描述</st> 方法中,在 <st c="7488">configureTestingModule</st> 方法内,我们将替换我们的 <st c="7539">CalculatorService</st> 提供者,如下所示:

 providers: [
        { provide: CalculatorService, useValue: calculatorServiceStub },
      ],

最后,这是 我们伪造的 <st c="7711">calculatorServiceStub</st> 服务的测试用例,其中包含我们的 <st c="7766">divide</st> 方法:

 it('should raise an exception when dividing by zero', () => {
    spyOn(calculatorService, 'divide').and.callThrough();
    expect(() => calculator.divide(10, 0)).toThrowError(
      'Cannot divide by zero'
    );
    expect(calculatorService.divide).toHaveBeenCalledWith(10, 0);
  });

以下是实现我们的 伪造服务的完整代码:

 import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CalculatorComponent } from './calculator.component';
import { CalculatorService } from 'src/core/services/calculator.service';
import { ColorChangeDirective } from 'src/core/directives/color-change.directive';
const calculatorServiceStub = {
  divide: (a: number, b: number) => {
    if (b === 0) {
      throw new Error('Cannot divide by zero');
    }
    return a / b;
  }, };
describe('CalculatorComponent', () => {
  let calculator: CalculatorComponent;
  let fixture: ComponentFixture<CalculatorComponent>;
  let calculatorService: CalculatorService;
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [CalculatorComponent, ColorChangeDirective],
      providers: [      { provide: CalculatorService, useValue: calculatorServiceStub },
      ],
    }).compileComponents();
    fixture = TestBed.createComponent(CalculatorComponent);
    calculator = fixture.componentInstance;
    calculatorService = TestBed.inject(CalculatorService);
    fixture.detectChanges();
  });
  it('should create', () => {
    expect(calculator).toBeTruthy();
  }); it('should initialize result to 0', () => {
    calculator.ngOnInit();
    expect(calculator.result).toEqual(0);
  });
  it('should divide two numbers correctly', () => {
    spyOn(calculatorService, 'divide').and.callThrough();
    calculator.divide(4, 2);
    expect(calculatorService.divide).toHaveBeenCalledWith(4, 2);
    expect(calculator.result).toBe(2);
  });
  it('should raise an exception when dividing by zero', () => {
    spyOn(calculatorService, 'divide').and.callThrough();
    expect(() => calculator.divide(10, 0)).toThrowError(
      'Cannot divide by zero'
    );
    expect(calculatorService.divide).toHaveBeenCalledWith(10, 0);
  });
});

让我们看一下 <st c="9810">代码。</st>

<st c="9824">calculatorServiceStub</st> 对象被创建来模拟 <st c="9876">divide</st> 方法 <st c="9897">CalculatorService</st> 服务。 <st c="9924">divide</st> 方法接受两个参数, <st c="9964">a</st> <st c="9970">b</st>,并执行除法操作。 <st c="10010">在这种情况下,存根检查</st> b<st c="10049">是否等于零。</st>如果是,则抛出一个 <st c="10081">错误</st> 来模拟除以零的场景。

最后的 <st c="10149">expect</st> 语句检查组件的 <st c="10185">result</st> 属性是否等于 <st c="10230">'Division by zero'</st>。这验证了当发生除以零时,错误信息被正确显示。

请注意,我们的测试执行已失败。 <st c="10383">这是一个打字错误,因为</st> result<st c="10418">是</st>number<st c="10440">类型,而不是</st>string`

因此,我们将编写我们 <st c="10524">CalculatorComponent</st> 中需要的最小代码量来解决 问题:

图 4.3 – 更新结果属性的声明

图 4.3 – 更新结果属性的声明

运行测试后,请注意,所有测试都已变为绿色,如图 我们的截图所示:

图 4.4 – 除以零测试用例成功

图 4.4 – 除以零测试用例成功

现在我们将按照 TDD 原则的要求进行重构。 我们将在项目的 <st c="11301">core</st> 文件夹中创建一个名为 <st c="11326">stubs</st> 的文件夹,并在其中创建一个 <st c="11352">calculator.service.stub.ts</st> 文件。

图 4.5 – calculator.service.stub.ts 文件结构

图 4.5 – calculator.service.stub.ts 文件结构

一旦文件创建完成,我们将把基于模拟的服务的源代码从 <st c="11598">calculator.component.spec.ts</st> 移动到我们的 <st c="11636">calculator.service.stub.ts</st> 文件中 如下:

图 4.6 – calculator.service.stub.ts 代码源

图 4.6 – calculator.service.stub.ts 代码源

由于它是一个服务,我们将使用依赖注入技术,正如我们将在下一节中更详细地看到的那样。 在这个阶段,我们将分几个步骤来做: 几个步骤:

  1. <st c="12141">calculator.service.stub.ts</st> 中创建一个名为 <st c="12175">CalculatorServiceStub</st>的类。

  2. 实现我们 计算器应用程序的所有运算符方法。

    以下是源代码的 样子:

     export class CalculatorServiceStub {
      add(a: number, b: number): number {
        return a + b;
      }
      substract(a: number, b: number): number {
        return a - b;
      } multiply(a: number, b: number): number {
        return a * b;
      } divide(a: number, b: number): number | Error {
        if (b === 0) {
          throw new Error('Cannot divide by zero');
        }
        return a / b;
      }
    }
    
  3. 在更新我们的模拟服务后,我们将进入我们的 <st c="12683">calculator.component.spec.ts</st> 测试文件,以替换提供者 如下:

     import { CalculatorServiceStub } from 'src/core/stubs/calculator.service.stub';
    ... providers: [
            { provide: CalculatorService, useClass: CalculatorServiceStub },
          ],
    ...
    
  4. 现在我们可以取消注释我们 <st c="12970">calculator.component.spec.ts</st> 文件中的所有方法,除了 测试用例 <st c="13016">it('should display error message for division by zero')</st> 注意,所有 我们的测试都是绿色的,如图所示:

图 4.7 – 使用 CalculatorServiceStub 测试用例成功测试 CalculatorComponent

图 4.7 – 使用 CalculatorServiceStub 测试用例成功测试 CalculatorComponent

间谍和方法存根是 Angular 测试框架中的强大工具,使开发者能够监控和控制其应用程序中依赖项的行为。 通过将这些技术融入 TDD 过程,开发者可以编写更全面、更可靠的测试,确保其 Angular 应用程序的功能性和稳定性。 计算器应用程序示例说明了间谍和方法存根如何应用于模拟不同场景并验证测试代码的行为。 在下一节中,我们将更详细地探讨如何使用 TestBed 提供者注入模拟依赖项 TestBed 提供者。

使用 TestBed 提供者注入模拟依赖项

模拟依赖项 是简化版的外部服务或资源,它们模仿实际依赖项的行为。 通过使用 TestBed 提供者注入这些模拟依赖项,开发者可以在测试期间控制它们的行为,确保可靠和彻底的测试,而不依赖于 外部系统。

在本节中,我们将探讨 Angular 测试框架中 TestBed 提供者的概念。 我们将深入研究其应用,并展示它们如何使开发者能够将模拟依赖项注入到其代码中。 通过这样做,我们可以创建隔离且可控的测试环境,遵循 TDD 的原则。

通过使用 TestBed 提供者注入模拟依赖项,开发者可以专注于测试特定的代码单元,而无需担心实际依赖项的复杂性。 这种方法使得调试更加容易,提高了测试覆盖率,并提高了整体 代码质量。

在本节中,我们将强调 TDD 的原则,并通过使用我们的计算器应用程序的实际示例来展示 TestBed 提供者的使用。 通过理解和有效利用 TestBed 提供者,开发者可以编写可靠、可维护且经过彻底测试的 Angular 应用程序。

让我们考虑向我们的计算器应用程序添加计算数字平方根的功能,同时仍然依赖于我们的 <st c="15794">CalculatorService</st> 服务。 在这种情况下,我们将专注于计算器应用程序的平方根功能。

首先,我们需要创建一个模拟服务来模拟实际平方根服务的行为。 我们可以创建一个简单的类,为不同的输入返回预定义的平方根值。 这个类,命名为 <st c="16123">MockSquareRootService</st>,将被创建在 <st c="16169">mocks</st> 文件夹中,我们将在这个文件夹与 <st c="16240">stubs</st> 文件夹相同的层次结构级别创建它,如以下截图所示:

图 4.8 – mock-square-root.service.mock.ts 文件层次结构

图 4.8 – mock-square-root.service.mock.ts 文件层次结构

这里是一个模拟平方根服务的示例 根服务:

 export class MockSquareRootService {
  calculateSquareRoot(value: number): number {
    // Perform a predefined square root calculation based on the input value
    return Math.sqrt(value);
  }
}

接下来,我们 使用 TestBed 提供者配置测试模块,用模拟版本替换 <st c="16804">calculator.component.spec.ts</st>中的实际平方根服务。 以下是如何配置测试模块的示例:

 import { MockSquareRootService } from './mock-square-root.service';
... beforeEach(async () => {
    await TestBed.configureTestingModule({
     ... providers: [{ provide: CalculatorService, useClass: MockSquareRootService }]
    }).compileComponents();

在前面的代码中,我们提供了 <st c="17169">CalculatorService</st> 服务令牌,并使用 <st c="17217">useClass</st> 属性指定了 <st c="17240">MockSquareRootService</st> 服务。 此配置告诉 TestBed,当测试代码请求实际平方根服务的实例时,使用模拟服务。

现在,当我们为计算器应用程序运行测试时,任何依赖于平方根服务的代码都将接收到模拟服务的实例。 我们可以在测试期间控制服务的行为,确保计算器应用程序在不同场景下正确执行平方根计算。 以下是一个 <st c="17757">MockSquareRootService</st> 服务的示例代码,该服务通过为不同的输入返回预定义的平方根值来模拟 <st c="17819">CalculatorService</st> 服务的行为 ,如下所示:

 export class MockSquareRootService {
  calculateSquareRoot(value: number): number {
    // Perform a predefined square root calculation based on the input value
    if (value === 4) {
      return 2;
    } else if (value === 9) {
      return 3;
    } else if (value === 16) {
      return 4;
    } else {
      throw new Error('Invalid input');
    }
  }
}

让我们通过将此序列添加到我们的测试 文件中, <st c="18319">calculator.component</st><st c="18339">.spec.ts</st>,来完成对数字平方根的测试编写:

 it('should calculate the square root correctly', () => {
   spyOn(calculatorService, 'squareRoot').and.callThrough();
   calculator.squareRoot(16);
   expect(calculatorService.squareRoot).toHaveBeenCalledWith(16);
   expect(calculator.result).toBe(4);
  });

为了使其功能正常,您需要将 <st c="18638">squareRoot()</st> 方法添加到 <st c="18661">calculator.component.ts</st> <st c="18689">calculator.service.ts</st>中。我不会在当前项目中这样做,因为目的是简要展示如何设置模拟。

Angular 中的 TestBed 提供者允许您将模拟依赖项注入到测试中。 这是一个 强大的 功能,它使您能够控制外部依赖项的行为并隔离待测试的代码。

当使用 <st c="19058">TestBed.configureTestingModule</st>配置测试模块时,您可以提供一个提供者列表,指定您想要模拟的依赖项的令牌。 然后,您可以使用 <st c="19217">useClass</st> <st c="19229">useValue</st> 属性为每个依赖项提供一个模拟或存根实现。

通过提供模拟实现,您可以在测试期间定义依赖项的行为。 这允许您模拟不同的场景并控制依赖项的返回值或错误条件。 您还可以通过使用间谍或其他 测试技术来验证待测试代码与依赖项之间的交互。

使用 TestBed 提供者注入模拟依赖项有助于提高测试的可靠性和稳定性。 它允许您专注于测试代码的特定功能,而不依赖于外部依赖项的实际实现。 这使得您的测试更加确定,并且不太可能因依赖项行为的变化而失败。

总的来说,TestBed 提供者提供了一种方便的方法将模拟依赖项注入到您的 Angular 测试中。 它们允许您控制外部依赖项的行为并隔离待测试的代码,从而实现更可靠和专注的测试。 在接下来的章节中,我们将采取动手实践的方法。 本节的目标是理解异步任务的相关性以及为什么在实现关于它们的测试时需要小心。 在下一节中,我们将管理异步操作和 复杂场景。

处理异步操作和复杂场景

测试异步操作和复杂场景是确保现代软件应用程序可靠性和功能性的关键部分。 在当今的软件开发领域,应用程序通常依赖于异步操作(如承诺和可观察者)来处理数据获取、处理和用户交互。 此外,涉及复杂工作流程、条件逻辑和多个依赖关系的复杂场景需要彻底测试,以确保应用程序在各种场景下都能按预期行为。 此外,涉及复杂工作流程、条件逻辑和多个依赖关系的复杂场景需要彻底测试,以确保应用程序在各种场景下都能按预期行为。 此外,涉及复杂工作流程、条件逻辑和多个依赖关系的复杂场景需要彻底测试,以确保应用程序在各种场景下都能按预期行为。

测试这些异步操作和复杂场景需要使用专门的技巧和工具来处理它们所提出的独特挑战。 在 Angular(一个流行的 JavaScript 框架)的上下文中,开发者可以访问一个全面的测试框架,该框架提供了强大的实用工具来测试 此类场景。

在本节中,我们将探讨在 Angular 应用程序中测试异步操作(如承诺和可观察者)以及复杂场景的重要性。我们将深入研究各种技术和最佳实践,以确保有效地测试这些场景,从而实现可靠和全面的测试覆盖率。

理解异步操作

异步操作是可以在主程序流程之外独立执行的任务。它们通常用于处理耗时操作,如网络请求、文件 I/O 或数据库查询。 而不是等待这些操作完成,程序可以继续执行其他任务,从而提高整体性能 和响应性。

处理异步操作的一种常见方法是通过回调。 回调是一个函数,它作为参数传递给另一个函数,并在异步操作完成后执行。 这允许我们定义操作完成后应该发生什么。 然而,管理回调可能导致回调地狱,使代码难以阅读和维护。 为了解决这个问题, <st c="22617">承诺</st> 应运而生。

承诺提供了一种更结构化的方式来处理异步操作。 承诺表示异步操作最终完成或失败,并允许我们附加回调来处理这些结果。 通过链式方法(如 .then() .catch(),承诺提供了一种更易于阅读和维护的方式来处理异步代码。

然而,JavaScript 新版本中引入了一种新的实现 Promise 的方法。 <st c="23085">async</st>/<st c="23092">await</st> 是一种简洁的语法,用于处理异步操作。 它允许我们编写看起来像同步代码的异步代码,使其更容易推理和维护。 使用 <st c="23301">async</st> 关键字,我们可以定义使用 <st c="23386">await</st> 关键字定义的函数,该关键字等待 Promise 解决 或拒绝。

此外,另一种实现异步操作的方法是传递可观察对象。 可观察对象是管理响应式编程中数据流和异步操作的有力工具。 它们代表随时间观察到的值序列。 可观察对象可以异步输出多个值,并提供一系列用于转换、过滤和组合数据流的操作符。 它们在 Angular 等框架中常用,用于处理事件、HTTP 请求和其他使用 RxJS 的异步操作

异步操作和可观察对象也使我们的代码能够实现并发和并行。 并发指的是同时执行多个任务的能力,而并行 则是指在多个处理器或线程上同时执行任务。 异步编程和可观察对象使我们能够并发处理多个操作,从而提高需要大量计算 或 I/O 的应用程序的性能。

处理异步操作

让我们考虑一下,我们的 计算器应用程序,它执行加法、减法、乘法和除法操作,现在有一个执行这些操作并将结果作为 <st c="24654">可观察对象返回的服务。</st>

首先,假设我们还有一个名为 <st c="24731">CalculatorAsyncService</st>的计算器服务,我们将在 <st c="24781">services</st> 文件夹中创建它。 我们需要在 <st c="24863">services</st> 文件夹中运行以下命令行一次:

<st c="24969">services</st> folder:
			![Figure 4.9 – Creation of the CalculatorAsyncService](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_04_9.jpg)

			<st c="25066">Figure 4.9 – Creation of the CalculatorAsyncService</st>
			<st c="25117">As opposed to the previous service we had to create, here we’ll essentially be doing asynchronous operations, in keeping with the subject we’re exploring.</st> <st c="25273">Our service’s methods will be based on the same principle, that is, receiving the two operands related to our calculation as parameters, then performing them as observables (which emphasizes the asynchronous aspect) and returning the result at the end.</st> <st c="25526">Based on the principles of TDD, we’ll look at what to expect using the</st> `<st c="25597">add()</st>` <st c="25602">method as an example.</st> <st c="25625">In our</st> `<st c="25632">calculator-async.service.spec.ts</st>` <st c="25664">test file, we’ll add</st> <st c="25685">this</st> <st c="25691">test case:</st>

it('should add two numbers', fakeAsync(() => {

let result = 0;

service.add(1, 2).subscribe((val) => {

result = val;

});

expect(result).toBe(3);

}));


			<st c="25850">The preceding code snippet is a unit test for a service method that adds two numbers using Angular’s</st> `<st c="25952">fakeAsync</st>` <st c="25961">utility to handle asynchronous operations synchronously.</st> <st c="26019">Here is the</st> <st c="26031">code breakdown:</st>

				*   `<st c="26046">fakeAsync</st>`<st c="26056">: This is an Angular utility function that lets you write tests that rely on asynchronous operations synchronously.</st> <st c="26173">It is useful for testing code that uses observables, promises, or other</st> <st c="26245">asynchronous operations</st><st c="26268">.</st>
				*   `<st c="26269">service.add(1, 2).</st><st c="26288">subscribe((val) =></st> <st c="26307">{ result = val ; });</st>`<st c="26328">: This line calls a service’s</st> `<st c="26359">add</st>` <st c="26362">method, passing two numbers (</st>`<st c="26392">1</st>` <st c="26394">and</st> `<st c="26399">2</st>`<st c="26400">).</st> <st c="26403">The</st> `<st c="26407">add()</st>` <st c="26412">method is supposed to return an observable that outputs the result of the addition.</st> <st c="26497">The</st> `<st c="26501">subscribe</st>` <st c="26510">method is used to subscribe to this observable and manage the emitted value.</st> <st c="26588">In this case, the output value is assigned to the</st> <st c="26638">result variable.</st>

			<st c="26654">Now we can run</st> <st c="26669">our favorite</st> `<st c="26683">ng test</st>` <st c="26690">command from our</st> <st c="26708">project terminal:</st>
			![Figure 4.10 – Error in our test case related to the add method on the terminal](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_04_10.jpg)

			<st c="27076">Figure 4.10 – Error in our test case related to the add method on the terminal</st>
			<st c="27154">As we can see, our test failed.</st> <st c="27187">This is normal, as this is the red phase.</st> <st c="27229">We have</st> <st c="27237">two errors:</st>

				*   <st c="27248">The no</st><st c="27255">n-existence of the</st> `<st c="27275">add()</st>` <st c="27280">method in</st> <st c="27291">our</st> `<st c="27295">CalculatorAsyncService</st>`
				*   <st c="27317">The absence of the type of our</st> `<st c="27349">val</st>` <st c="27352">variable</st>

			<st c="27361">To fix this, here’s what we’ll do.</st> <st c="27397">First, our</st> `<st c="27408">val</st>` <st c="27411">variable is the result of our calculation.</st> <st c="27455">It is therefore of the type number.</st> <st c="27491">So, we’ll d</st><st c="27502">o</st> <st c="27505">the following:</st>

it('should add two numbers', fakeAsync(() => {

let result = 0;

service.add(1, 2).subscribe((val: number) => {

result = val;

});

expect(result).toBe(3);

}));


			<st c="27676">Here’s how it looks on</st> <st c="27700">the terminal:</st>
			![Figure 4.11 – Error in our test case related to the add method on the terminal](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_04_11.jpg)

			<st c="27910">Figure 4.11 – Error in our test case related to the add method on the terminal</st>
			<st c="27988">The error related to</st> <st c="28009">the</st> `<st c="28014">val</st>` <st c="28017">variable has now disappeared, but the error relate</st><st c="28068">d to our service’s</st> `<st c="28088">add()</st>` <st c="28093">method remains.</st> <st c="28110">This is normal because our</st> `<st c="28137">CalculatorAsyncService</st>` <st c="28159">d</st><st c="28161">oesn’t yet have an</st> `<st c="28180">add()</st>` <st c="28185">method.</st> <st c="28194">Now we’re going to write the minimum code required for our test to pass.</st> <st c="28267">As a reminder, our</st> `<st c="28286">add()</st>` <st c="28291">method must return an observable.</st> <st c="28326">Here’</st><st c="28331">s the code for the</st> `<st c="28351">add()</st>` <st c="28356">method to be added to</st> <st c="28379">our</st> `<st c="28383">Calcula</st><st c="28390">torAsyncService</st>`<st c="28406">:</st>

add(a: number, b: number): Observable {

return of(a + b);

}


			<st c="28477">Here is a breakdown of t</st><st c="28502">he</st> <st c="28506">p</st><st c="28507">receding cod</st><st c="28519">e:</st>

				*   `<st c="28522">add(a :</st> <st c="28530">number, b : number) : Observable<number></st>`<st c="28571">: This is the method signature.</st> <st c="28604">Th</st><st c="28606">e method is called</st> `<st c="28626">add()</st>` <st c="28631">and takes two parameters,</st> `<st c="28658">a</st>` <st c="28659">and</st> `<st c="28664">b</st>`<st c="28665">, both of which are numbers.</st> <st c="28694">The method returns an observable, which</st> <st c="28733">outputs a number.</st> <st c="28752">This indicates that the method is asynchronous and will produce a value at some point in</st> <st c="28841">th</st><st c="28843">e future</st><st c="28852">.</st>
				*   `<st c="28853">return of(a + b);</st>`<st c="28871">: This line uses RxJS’s</st> `<st c="28896">of</st>` <st c="28898">function to create an observable that outputs a single value and terminates.</st> <st c="28976">The value emitted is the result</st> <st c="29008">of adding</st> `<st c="29018">a</st>` <st c="29019">and</st> `<st c="29024">b</st>`<st c="29025">. The</st> `<st c="29031">of</st>` <st c="29033">function is a utility function that converts the given arguments into an observable sequence.</st> <st c="29128">In this case, it is used to create an observable that outputs the sum of</st> `<st c="29201">a</st>` <st c="29202">and</st> `<st c="29207">b</st>`<st c="29208">. Don’t forget to</st> <st c="29226">import it.</st>

			<st c="29236">Afte</st><st c="29241">r implementing the</st> `<st c="29261">add()</st>` <st c="29266">method, here’s the result in</st> <st c="29296">our terminal:</st>
			![Figure 4.12 – add asynchronous method test case succeeded on the terminal](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_04_12.jpg)

			<st c="29360">Figure 4.12 – add asynchronous method test case succeeded on the terminal</st>
			<st c="29433">And in our b</st><st c="29446">rowser running on Karma, we</st> <st c="29475">have this:</st>
			![Figure 4.13 – add asynchronous method test case succeeded in the browser](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_04_13.jpg)

			<st c="29555">Figure 4.13 – add asynchronous method test case succeede</st><st c="29611">d in the browser</st>
			<st c="29628">Well done!</st> <st c="29640">We’ve just</st> <st c="29651">implemented our calculator’s first asynchronous method using TDD principles.</st> <st c="29728">We’ll now do the same with subtraction, multiplication,</st> <st c="29784">and division.</st>
			<st c="29797">In our</st> `<st c="29805">calculator-async.service.spec.ts</st>` <st c="29837">test file, we’re going to write the expected tests related to our subtraction, multiplication, and division operators.</st> <st c="29957">Usually, we’d do these separately, but as we’ve alre</st><st c="30009">ady seen with the</st> `<st c="30028">add()</st>` <st c="30033">method, we’ll do them all at once.</st> <st c="30069">Here’</st><st c="30074">s what</st> <st c="30082">we’ll get:</st>

it('应该减去两个数字', fakeAsync(() => {

let result = 0;

service.subtract(5, 3).subscribe((val: number) => {

result = val;

}));

expect(result).toBe(2);

}));

it('应该乘以两个数字', fakeAsync(() => {

let result = 0;

service.multiply(3, 4).subscribe((val: number) => {

result = val;

});

expect(result).toBe(12);

}));

it('应该除以两个数字', fakeAsync(() => {

let result = 0;

service.divide(10, 2).subscribe((val: number) => {

result = val;

});

expect(result).toBe(5);

}));


			<st c="30596">As you may have</st> <st c="30613">noticed in your terminal, we have</st> <st c="30647">these errors:</st>
			![Figure 4.14 – Error in our test case related to the subtract, multiply, and divide methods on the terminal](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_04_14.jpg)

			<st c="31246">Figure 4.14 – Error in our test case related to the subtract, multiply, and divide methods on the terminal</st>
			<st c="31352">These errors are due to the absence of the</st> `<st c="31396">subtract</st>`<st c="31404">,</st> `<st c="31406">multiply</st>`<st c="31414">, and</st> `<st c="31420">divide</st>` <st c="31426">methods in our</st> `<st c="31442">CalculatorAsyncService</st>`<st c="31464">. As we had to do for the</st> `<st c="31490">add</st>` <st c="31493">method, we’ll add the minimum amount of code needed to make our tests go green.</st> <st c="31574">In our</st> `<st c="31581">CalculatorAsyncService</st>`<st c="31603">, we’ll add</st> <st c="31614">these methods:</st>

subtract(a: number, b: number): Observable {

return of(a - b);

}

multiply(a: number, b: number): Observable {

return of(a * b);

}

divide(a: number, b: number): Observable {

return of(a / b);

}


			<st c="31851">On our terminal, we</st> <st c="31871">have</st> <st c="31877">the following:</st>
			![Figure 4.15 – subtract, multiply, and divide asynchronous methods test cases succeeded on the terminal](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_04_15.jpg)

			<st c="32320">Figure 4.15 – subtract, multiply, and divide asynchronous methods test cases succeeded on the terminal</st>
			<st c="32422">And in our browser, we have</st> <st c="32451">the following:</st>
			![Figure 4.16 – subtract, multiply, and divide asynchronous methods test cases succeeded on the browser](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_04_16.jpg)

			<st c="32615">Figure 4.16 – subtract, multiply, and divide asynchronous methods test cases succeeded on the browser</st>
			<st c="32716">All our tests</st> <st c="32731">are green!</st>
			<st c="32741">However, there’s one case we haven’t yet tested at the divi</st><st c="32801">sion level.</st> <st c="32814">It’s</st> `<st c="32819">division by zero</st>`<st c="32835">. As with the</st> `<st c="32849">CalculatorService</st>` <st c="32866">service previously, we also need to handle division by 0 by raising an exception or returning an error message.</st> <st c="32979">So, at the</st> `<st c="32990">CalculatorAsyncService</st>` <st c="33012">level, we’re going to add a second test case related to division, which handles the case where we don’t try to divide</st> <st c="33131">a number</st> <st c="33140">by 0:</st>

it('当除以零时应该抛出错误', fakeAsync(() => { let error = { message: '' }; ;

service.divide(10, 0).subscribe({

error: (err) => (error = err),

});

expect(error).toBeTruthy();

expect(error.message).toBe('Cannot divide by zero');

}));


			<st c="33401">Here’s a</st> <st c="33411">breakdown of</st> <st c="33424">the code:</st>

				*   `<st c="33433">se</st><st c="33436">rvice.divid</st><st c="33448">e(10, 0).subscribe({ error : (err</st><st c="33482">) =</st><st c="33486">> (error = err), });</st>`<st c="33507">: This line calls a service’s</st> `<st c="33538">divide</st>` <st c="33544">method with arguments</st> `<st c="33567">10</st>` <st c="33569">and</st> `<st c="33574">0</st>`<st c="33575">. The</st> `<st c="33581">divide</st>` <st c="33587">method is expected to return an observable.</st> <st c="33632">The</st> `<st c="33636">subscribe</st>` <st c="33645">method is used to subscribe to the observable.</st> <st c="33693">The object passed to</st> `<st c="33714">subscribe</st>` <st c="33723">specifies how to handle the case of an error.</st> <st c="33770">If an error occurs during division (which will happen, since division by zero is not defined), the error-handling function</st> `<st c="33893">(err) => (error = err)</st>` <st c="33915">is executed, assigning the error message to the</st> `<st c="33964">error</st>` <st c="33969">variable.</st>
				*   `<st c="33979">expect(</st><st c="33987">error).toBeTruthy();</st>`<st c="34008">: This line asserts that the</st> `<st c="34038">error</st>` <st c="34043">variable is true, that is, that it has a value.</st> <st c="34092">This is a basic check to ensure that an error has</st> <st c="34142">been triggered.</st>
				*   `<st c="34157">expect(error.message).toBe('Cann</st><st c="34190">ot divide by zero');</st>`<st c="34211">: This line asserts that the</st> `<st c="34241">message</st>` <st c="34248">property of the</st> `<st c="34265">error</st>` <st c="34270">object is equal to the</st> `<st c="34294">'Cannot divide by zero'</st>` `<st c="34317">string</st>`<st c="34324">. This is the specific error message expected when attempting to divide</st> <st c="34396">by zero.</st>

			<st c="34404">After adding our test case, let’s see what happens on</st> <st c="34459">our terminal:</st>
			![Figure 4.17 – Asynchronous division-by-0 test case failed on the terminal](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_04_17.jpg)

			<st c="35418">Figure 4.17 – Asynchronous division-by-0 test case failed on the terminal</st>
			<st c="35491">And in our</st> <st c="35503">browser, we have</st> <st c="35520">the following:</st>
			![](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_04_18.jpg)

			<st c="36779">Figure 4.18 – Asynchronous division-by-0 test case failed in the browser</st>
			<st c="36851">This error is to be expected because we haven’t yet handled it in our</st> `<st c="36922">CalculatorAsyncService</st>` <st c="36944">service.</st> <st c="36954">We’re now going to write the minimal code needed to resolve this error.</st> <st c="37026">In our</st> `<st c="37033">CalculatorAsyncService</st>` <st c="37055">service, we’re going to modify ou</st><st c="37089">r</st> `<st c="37092">divide</st>` <st c="37098">method:</st>

divide(a: number, b: number): Observable {

if (b === 0) {

return throwError(() => new Error('Cannot divide by zero'));

} 返回(a / b).pipe(

catchError((error) => {

    return throwError(() => error);

})

);

}


			<st c="37323">Here’s a breakdown of</st> <st c="37346">the code:</st>

				*   `<st c="37426">b</st>` <st c="37427">is equal to 0 using</st> `<st c="37448">if (b === 0)</st>`<st c="37460">. If</st> `<st c="37465">b</st>` <st c="37466">is equal to 0, it returns an observable that immediately throws an error with the message</st> `<st c="37557">Cannot divide by zero</st>`<st c="37578">. To do this, we use RxJS’s</st> `<st c="37606">throwError</st>` <st c="37616">function, which creates an observable that emits no elements and immediately issues an</st> <st c="37704">error notification.</st>
				*   `<st c="37746">b</st>` <st c="37747">is not 0, the method performs the</st> `<st c="37782">a / b</st>` <st c="37787">division operation and wraps</st> <st c="37817">the result in an observable using the RxJS</st> `<st c="37860">of</st>` <st c="37862">function.</st> <st c="37873">This function creates an observable that outputs the specified value,</st> <st c="37943">then terminates.</st>
				*   `<st c="37988">divide</st>` <st c="37994">operation is then routed through the</st> `<st c="38032">catchError</st>` <st c="38042">operator.</st> <st c="38053">This operator catches any errors that occur during the execution of the observable chain and allows you to handle them.</st> <st c="38173">In this case, the</st> `<st c="38191">catchError</st>` <st c="38201">operator is used to catch any errors that may occur during the</st> `<st c="38265">divide</st>` <st c="38271">operation and throw them back using the</st> `<st c="38312">throwError</st>` <st c="38322">function.</st> <st c="38333">This ensures that if an error occurs (other than division by zero, which is explicitly handled), the observable will issue an</st> <st c="38459">error notification.</st>
				*   `<st c="38557">of</st>` <st c="38559">function, which outputs the result of the division operation, or the observable created by the</st> `<st c="38655">throwError</st>` <st c="38665">function in the event of</st> <st c="38691">an error.</st>

			<st c="38700">After adding our test case, let’s see what happens on</st> <st c="38755">our terminal:</st>
			![Figure 4.19 – Asynchronous division-by-0 test case succeeded on the terminal](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_04_19.jpg)

			<st c="38924">Figure 4.19 – Asynchronous division-by-0 test case succeeded on the terminal</st>
			<st c="39000">And in our browser, we have</st> <st c="39029">the following:</st>
			![Figure 4.20 – Asynchronous division-by-0 test case succeeded on the browser](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_04_20.jpg)

			<st c="39234">Figure 4.20 – Asynchronous division-by-0 test case succeeded on the browser</st>
			<st c="39309">Error handling is an essential part of working with asynchronous operations.</st> <st c="39387">In our calculator application, if an error occurs while retrieving data or performing calculations, we can display an</st> <st c="39504">error message to the user and provide options for retrying or handling the error in an elegant way.</st> <st c="39605">The RxJS module provides error-handling mechanisms, such as the use of the</st> `<st c="39680">catchError</st>` <st c="39690">operator.</st>
			<st c="39700">Emphasizing the importance of testing async operations</st>
			<st c="39755">In a calculator app, async</st> <st c="39783">operations can include fetching data from an API, performing calculations asynchronously, or handling user input events.</st> <st c="39904">Properly testing these async operations is essential to ensure that the app functions correctly, provides accurate results, and handles</st> <st c="40040">errors gracefully.</st>
			<st c="40058">Unit testing is a fundam</st><st c="40083">ental approach to testing individual components or functions in isolation.</st> <st c="40159">In the context of async operations in Angular, unit tests play a crucial role in verifying the behavior of code that handles async tasks.</st> <st c="40297">For example, you can write unit tests to verify that an API service correctly fetches exchange rates or that a calculation service accurately performs</st> <st c="40448">calculations asynchronously.</st>
			<st c="40476">To effectively test async operations, it is essential to mock dependencies, such as API services or calculation functions.</st> <st c="40600">By mocking these dependencies, you can control the behavior of external services or functions during testing, allowing you to focus on the specific code that handles async operations.</st> <st c="40784">Angular provides tools such as TestBed and Jasmine spies to mock</st> <st c="40849">dependencies effectively.</st>
			<st c="40874">Testing async operations often involves dealing with timing issues.</st> <st c="40943">For example, when testing a function that performs an async calculation, you need to ensure that the test waits for the calculation to be completed before making assertions.</st> <st c="41117">Angular provides utilities, such as</st> `<st c="41153">fakeAsync</st>`<st c="41162">, that allow you to control the timing of async operations in your tests, making it easier to write accurate and</st> <st c="41275">deterministic tests.</st>
			<st c="41295">While unit testing is essential, it is equally important to perform integration testing to validate the interaction between different components in your calculator app.</st> <st c="41465">Integration tests can verify that async operations, such as fetching data and performing calculations, are correctly integrated into the overall functionality of the app.</st> <st c="41636">For example, you can write integration tests to ensure that the UI is updated correctly when async operations</st> <st c="41746">are complete.</st>
			<st c="41759">Testing error handling is crucial in async operations.</st> <st c="41815">For example, when fetching data from an API, you need to test scenarios where the API returns an error response.</st> <st c="41928">By simulating error conditions in your tests, you can verify that the app handles errors gracefully, displays appropriate error messages, and provides fallback mechanisms.</st> <st c="42100">Angular’s</st> `<st c="42110">HttpClient</st>` <st c="42120">module provides mechanisms for mocking API responses and testing different</st> <st c="42196">error scenarios.</st>
			**<st c="42212">End-to-end</st>** <st c="42223">(</st>**<st c="42225">E2E</st>**<st c="42228">) testing is</st> <st c="42241">essential to validate the entire system’s behavior, including the async operations in your calculator app.</st> <st c="42349">E2E tests simulate real-world user interactions and validate the app’s functionality from a user’s perspective.</st> <st c="42461">By writing E2E tests that</st> <st c="42487">cover scenarios involving async operations, you can ensure that the app functions correctly and provides a seamless</st> <st c="42603">user experience.</st>
			<st c="42619">Summary</st>
			<st c="42627">In this chapter, we covered three important topics related to testing in Angular: method stubs and spies, TestBed providers, and handling async operations and</st> <st c="42787">complex scenarios.</st>
			<st c="42805">Firstly, we explored the concept of method stubs and spies, which allowed us to monitor and control the calls to dependencies in our tests.</st> <st c="42946">We learned how to create method stubs using Jasmine’s</st> `<st c="43000">spyOn</st>` <st c="43005">function, which enabled us to replace a method’s implementation with our own custom behavior.</st> <st c="43100">This allowed us to test our code in isolation and ensure that it behaved</st> <st c="43173">as expected.</st>
			<st c="43185">Next, we delved into TestBed providers, which are used to inject mocked dependencies into our tests.</st> <st c="43287">We learned how to use the</st> `<st c="43313">TestBed.configureTestingModule</st>` <st c="43343">method to configure our test module and provide mocked instances of dependencies.</st> <st c="43426">This technique allowed us to control the behavior of dependencies and focus on testing specific scenarios without relying on</st> <st c="43551">real implementations.</st>
			<st c="43572">Lastly, we tackled the challenges of handling async operations and complex scenarios in our tests.</st> <st c="43672">We explored techniques such as using the fakeAsync function to handle asynchronous code.</st> <st c="43761">These techniques enabled us to write reliable tests for scenarios involving asynchronous operations and</st> <st c="43865">complex dependencies.</st>
			<st c="43886">In the next chapter, we’ll learn how to test Angular’s pipes, forms, and</st> <st c="43960">reactive programming.</st>

第六章:5

测试 Angular 管道、表单和响应式编程

Angular 框架的主要特点之一是它能够通过使用管道、表单和响应式编程轻松处理数据操作和表单输入

Angular 中的管道允许在向用户显示之前转换数据 管道可用于格式化日期和货币,甚至可以应用自定义逻辑以按任何所需方式操作数据 测试这些管道确保它们按预期工作并产生预期的输出

在 Angular 中,表单是收集和验证用户输入的必要组件,测试 Angular 表单涉及验证表单字段是否正确绑定到模型,验证输入,并处理表单提交 通过测试表单,开发人员可以确保表单按预期运行并提供无缝的用户体验

响应式编程是一种处理异步数据流和事件的范例,在 Angular 中,响应式编程通常与 ReactiveForms 模块一起使用,该模块提供了一种以响应式方式管理表单状态和数据流的方法

本章将探讨测试 Angular 管道、表单和响应式编程的不同方法和最佳实践,我们将介绍编写有效测试可用的工具和技术,以及常见的故障排除技巧和提示 在本章结束时,您将深入了解如何测试 Angular 应用程序的这些关键方面,并确保它们的可靠性和准确性

概括来说,以下是本章将涵盖的主要主题

  • 在我们的项目中使用的 Angular 管道的测试 我们的项目

  • 将测试驱动开发应用于我们的响应式表单 响应式表单

技术要求

为了跟随本章中的示例和练习,您需要对 Angular 和 TypeScript 有一个基本的了解,以及访问以下内容 以下内容:

  • 在您的计算机上安装的 Node.js 和 npm 您的计算机

  • 安装全局的 Angular CLI 全局安装

  • 在您的计算机上安装的代码编辑器,例如 Visual Studio Code 您的计算机

本章的代码文件可以在以下位置找到https://github.com/PacktPublishing/Mastering-Angular-Test-Driven-Development/tree/main/Chapter%205

测试我们在项目中使用的 Angular 管道

我们的计算器应用程序是一个 简单的工具,执行基本的算术运算,包括加法、减法、乘法和除法。 假设我们有一个功能,允许我们以百分比的形式表示一个数字。 为此,我们将使用一个管道。 我们将称我们的 管道 <st c="2573">percent</st>

在我们的 <st c="2589">core</st> 文件夹中,我们将创建一个 <st c="2617">pipes</st> 文件夹。 我们将通过我们的终端访问此文件夹,然后运行以下命令来创建我们的 <st c="2722">percent</st> 管道:

<st c="2735">$ ng g pipe percent –skip-import</st>

管道生成后,你应该看到以下树结构:

图 5.1 – 管道文件夹

图 5.1 – 管道文件夹

<st c="2946">percent.pipe.spec.ts</st>的内容相关,这是我们所拥有的:

 import { PercentPipe } from './percent.pipe'; describe('PercentPipe', () => { it('create an instance', () => {
    const pipe = new PercentPipe();
    expect(pipe).toBeTruthy();
  });
});

遵循 测试驱动开发 (TDD),我们将创建一个包含以下 测试 的测试套件:

  • 一个将正数格式化为 百分比字符串 的测试

  • 一个将负数格式化为 百分比字符串 的测试

  • 一个将小数格式化为 百分比字符串 的测试

  • 一个将非数字格式化为 百分比字符串 的测试

让我们 开始吧。

正数转换为百分比字符串格式化测试

我们的目标非常简单。 如果我们 有一个正数,例如 <st c="3669">123</st>,它的百分比格式必须是 <st c="3704">12300%</st>。在我们的 <st c="3719">percent.pipe.spec.ts</st> 文件中,我们将添加以下 测试套件e:

 it('should format a positive number to a percentage string', () => {
  const input = 123;
  const output = new PercentPipe().transform(input);
  expect(output).toBe('12300%');
});

在我们的终端中,运行 <st c="3995">ng test</st> 命令后,我们将 得到以下结果:

图 5.2 – 正数转换为百分比字符串格式化测试在终端中失败

图 5.2 – 正数转换为百分比字符串格式化测试在终端中失败

在我们的浏览器中,我们将得到以下结果:

图 5.3 – 正数转换为百分比字符串格式化测试在浏览器中失败

图 5.3 – 正数转换为百分比字符串格式化测试在浏览器中失败

我们的测试失败是很正常的。 记住,我们正在遵循 TDD 的原则。 因此,我们已经考虑了当我们的数字被格式化为百分比时预期的结果,并编写了相应的测试序列。 现在我们将编写测试通过所需的最少代码。

现在让我们打开我们的 <st c="5633">percent.pipe.ts</st> 文件,看看它包含什么。 我们将看到以下内容:

图 5.4 – percent.pipe.ts 文件中的代码

图 5.4 – percent.pipe.ts 文件中的代码

因此,让我们添加我们的算法。 这将使我们能够 以最少的代码将之前的测试带到绿色阶段:

 transform (value: number): string {
    const formattedValue = value * 100;
    return formattedValue + '%';
  }

我们在这里所做的一切只是将需要格式化为百分比的数字乘以 100,并将其与 <st c="6317">%</st> 符号连接起来,以获得所需的渲染。 现在,当我们进入我们的测试终端时,我们的测试将 变为绿色:

图 5.5 – 正数转换为百分比字符串格式化测试在终端中成功

图 5.5 – 正数转换为百分比字符串格式化测试在终端中成功

在我们的浏览器中,我们将得到以下 结果:

图 5.6 – 正数转换为百分比字符串格式化测试在浏览器中成功

图 5.6 – 正数转换为百分比字符串格式化测试在浏览器中成功

在下一节中,我们将查看将负数格式化为 百分比的测试。

负数转换为百分比字符串格式化测试

正如我们所见,我们的目标也非常 简单。 如果我们有一个负数,例如 <st c="8188">-123</st>,它的百分比格式必须是 <st c="8224">-12300%</st>。因此,在我们的 <st c="8244">percent.pipe.spec.ts</st> 文件中,我们将 添加以下 测试套件e:

 it('should format a negative number to a percentage string', () => {
  const input = -123;
  const output = new PercentPipe().transform(input);
  expect(output).toBe('-12300%');
});

在我们的终端中,运行 <st c="8520">ng test</st> 命令后,我们得到以下 结果:

图 5.7 – 负数转换为百分比字符串格式化测试在终端中成功

图 5.7 – 负数转换为百分比字符串格式化测试在终端中成功

在我们的浏览器中,我们有以下 结果:

图 5.8 – 负数转换为百分比字符串格式化测试在浏览器中成功

图 5.8 – 负数转换为百分比字符串格式化测试在浏览器中成功

我们可以得出结论,我们 percent.pipe.ts 文件中编写的最小代码量,用于将正数格式化为百分比,也适用于负数,因为我们不需要添加任何代码来通过测试

在下一节中,我们将查看将小数格式化为 百分比的测试。

小数转换为百分比字符串格式化测试

这次我们将用小数进行相同的练习。 如果我们有一个小数,例如 <st c="10527">123.45</st>,它的百分比 格式必须是 <st c="10565">12345%</st>。在我们的 <st c="10580">percent.pipe.spec.ts</st> 文件中,我们将添加以下 以下 测试序列:

 it('should format a decimal number to a percentage string', () => {
  const input = 123.45;
  const output = new PercentPipe().transform(input);
  expect(output).toBe('12345%');
});

在我们的终端中,运行 <st c="10859">ng test</st> 命令后,我们 得到以下结果:

图 5.9 – 十进制数字转换为百分比字符串格式化测试在终端中成功

图 5.9 – 十进制数字转换为百分比字符串格式化测试在终端中成功

在我们的浏览器中,我们有 以下结果:

图 5.10 – 十进制数字转换为百分比字符串格式化测试在浏览器中成功

图 5.10 – 十进制数字转换为百分比字符串格式化测试在浏览器中成功

我们可以得出结论, 我们 percent.pipe.ts 文件中编写的最小代码量,用于将正数和负数格式化为百分比,也适用于小数,因为我们不需要添加任何代码来通过测试

在下一节中,我们将查看将非数字表达式,例如 不是一个数字 (NaN**),格式化为 百分比的测试。

非数字转换为百分比字符串格式化测试

在这种情况下,目标是处理一个异常。 让我们 假设要转换成百分比的数字实际上不是一个数字。 通常情况下,我们会 收到一个错误信息,例如 <st c="13344">percent.pipe.spec.ts</st> 文件,我们将添加以下 测试 套件:

 it('should return an Error when the value is not a number NaN', () => {
    const input = NaN;
    const output = new PercentPipe().transform(input);
    expect(output).toBe('Error');
  });

在我们的终端中,在运行了<st c="13621">ng test</st>命令之后,我们得到如下结果:

图 5.11 – 在终端中非数字转换为百分比字符串格式化测试失败

图 5.11 – 在终端中非数字转换为百分比字符串格式化测试失败

在我们的浏览器中,我们有如下结果:

图 5.12 – 在浏览器中非数字转换为百分比字符串格式化测试成功

图 5.12 – 在浏览器中非数字转换为百分比字符串格式化测试成功

如您可能已经注意到的,我们有一些错误,并且我们的测试失败了。这是正常的,因为我们还没有使用我们的<st c="15075">PercentPipe</st>来解决这个问题的。

为了修复我们的测试,我们将前往<st c="15117">percent.pipe.ts</st>并添加处理这个异常所需的最少代码:

 if (isNaN(value)) {
      return 'Error';
    }

这段代码需要添加到<st c="15271">transform()</st>函数中。以下是对<st c="15336">transform()</st>函数的完整代码:

 transform(value: number): string {
    if (isNaN(value)) {
      return 'Error';
    }
    const formattedValue = value * 100;
    return formattedValue + '%';
  }

在我们的终端中,在运行了<st c="15533">ng test</st>命令之后,我们将得到如下结果:

图 5.13 – 在终端中非数字转换为百分比字符串格式化测试成功

图 5.13 – 在终端中非数字转换为百分比字符串格式化测试成功

在我们的浏览器中,我们将得到如下结果:

图 5.14 – 在浏览器中 PercentPipe 的测试用例成功

图 5.14 – 在浏览器中 PercentPipe 的测试用例成功

最后一步是前往<st c="16250">CalculatorComponent</st>来测试我们在<st c="16301">calculator.component.html</st>模板中的<st c="16282">PercentPipe</st>。为了做到这一点,我们将在<st c="16386">calculator.module.ts</st>模块中声明我们的<st c="16367">PercentPipe</st>,这样我们就可以在模板中使用它了。这将是我们得到的结果:

图 5.15 – 我们组件中的 PercentPipe 实现

图 5.15 – 我们组件中的 PercentPipe 实现

所有的东西都工作得非常完美!

在下一节中,我们将设置 计算器的用户界面。 到目前为止,还没有用户交互。 我们只是操作代码来玩转变量状态。 为了设置用户界面,我们将使用 响应式表单,并看看我们如何应用 TDD 的原则。

在我们的计算器应用中实现响应式表单的 TDD测试

响应式表单是一种使用响应式编程原则来根据用户输入更新表单状态的表单类型。 响应式表单常用于 Web 应用程序中,因为它们可以用来创建动态 和响应式 的用户界面。

在本节中,我们将讨论如何为我们的计算器应用实现响应式表单用户界面。 我们将使用 TDD 来确保我们的表单是有效的,并且计算器组件按预期工作。 编写计算器表单的测试

使用响应式表单用户界面为计算器带来的好处如下: 一个计算器:

  • 动态和响应式:响应式表单可以用来创建动态和响应式的用户界面。 例如,你可以使用响应式表单创建一个计算器,当用户在输入字段中输入值时,它会更新结果。

  • 有效性:响应式表单提供验证功能,可以用来确保用户输入是有效的。 例如,你可以使用响应式表单创建一个计算器,验证操作数是否为数字,以及运算符是否为有效的 数学运算符。

  • 可测试性:响应式表单易于使用 TDD 进行测试。 这确保了你的表单是有效的,并且计算器组件按预期工作。

要实现一个用于使用计算器的 TDD 响应式表单用户界面,你需要遵循 以下步骤。

编写计算器表单的测试

第一步是为 calculator 表单编写测试。 这个测试将确保当所有字段都正确填写时,表单是有效的。 在我们的 <st c="19086">calculator.component.spec.ts</st>中,我们将添加以下 测试套件:

 it('should be valid when all of the fields are filled in correctly', () => {
  const form = new FormGroup({
    operand1: new FormControl(123),
    operand2: new FormControl(456),
    operator: new FormControl('+'),
  });
  expect(form.valid).toBe(true);
});

不要忘记导入 <st c="19418">ReactiveFormModule</st> ,如以下代码所示:

 await TestBed.configureTestingModule({
    imports: [ReactiveFormsModule],
  }).compileComponents();

正如你所见,我们现在有一个测试套件与我们的响应式表单管理相关联,以防它是有效的。 当我们 运行 <st c="19698">ng test</st> 命令时,这将 是结果:

图 5.16 – 计算器表单测试在终端中成功

图 5.16 – 计算器表单测试在终端中成功

在我们的浏览器中,我们将得到 以下结果:

图 5.17 – 计算器表单测试在浏览器中成功

图 5.17 – 计算器表单测试在浏览器中成功

我们的测试套件启动了! 这并不令人惊讶! 实际上,我们在测试中直接创建了我们表单的实例。 由于我们尚未与计算器组件的表单交互,我们现在将通过修改我们的 <st c="21118">test suite</st> 来完成这一点:

 it('should be valid when all of the fields are filled in correctly', () => {
    calculator.calculatorForm.get('operand1')?.setValue(123);    calculator.calculatorForm.get('operand2')?.setValue(456);
  calculator.calculatorForm.get('operator')?.setValue('+');
expect(calculator.calculatorForm.valid).toBe(true);
});

在我们的终端中,运行 <st c="21485">ng test</st> 命令后,我们将 得到以下结果:

图 5.18 – 计算器表单构建在终端中失败

图 5.18 – 计算器表单构建在终端中失败

我们得到的结果是正常的,因为 <st c="22322">calculatorForm</st> 不在我们的 <st c="22358">CalculatorComponent</st>中。我们需要将其作为属性添加到我们的类中 如下所示:

图 5.19 – 在我们的组件中将 calculatorForm 声明为 FormGroup 模块

图 5.19 – 在我们的组件中将 calculatorForm 声明为 FormGroup 模块

现在我们已经添加了 <st c="22671">calculatorForm</st>,我们还将通过以下方式初始化我们的 <st c="22745">calculatorForm</st> 来添加三个输入字段:

 this.calculatorForm = new FormGroup({
   operand1: new FormControl(null, [Validators.required]),
   operand2: new FormControl(null, [Validators.required]),
   operator: new FormControl(null, [Validators.required]),
});

图 5.20 – 初始化我们的 FormGroup 模块 calculatorForm

图 5.20 – 初始化我们的 FormGroup 模块 calculatorForm

在我们的终端中,运行 <st c="23332">ng test</st> 命令后,我们将 得到以下结果:

图 5.21 – 计算器表单测试在终端中成功

图 5.21 – 计算器表单测试在终端中成功

在我们的浏览器中,我们将得到 以下结果:

图 5.22 – 计算器表单测试在浏览器中成功

5.22 – 计算器表单测试在浏览器中成功

我们将进行另一个测试以确保 相反的情况是正确的,即当表单 无效时。

由于所有字段都是必填的,如果某个字段未填写,则表单必须无效。 在我们的 <st c="24892">calculator.component.spec.ts</st> 文件中,我们将添加以下 测试 序列:

 it('should be invalid when one of the field is not filled in correctly', () => {
    calculator.calculatorForm.get('operand1')?.setValue(123);
    calculator.calculatorForm.get('operator')?.setValue('+');
    expect(calculator.calculatorForm.valid).toBe(false);
  });

在我们的终端中,运行 <st c="25257">ng test</st> 命令后,我们将看到以下内容:

图 5.23 – 在终端中计算器表单测试成功

图 5.23 – 在终端中计算器表单测试成功

在我们的浏览器中,我们将看到以下结果:

图 5.24 – 在浏览器中计算器表单测试成功

图 5.24 – 在浏览器中计算器表单测试成功

在下一节中,我们将实现 我们的计算器应用程序的用户 界面。

实现用户界面

让我们在我们的 calculator.component.html 文件中使用以下内容来 实现我们的计算器用户 界面:

 <form [formGroup]="calculatorForm">
  <input type="number" formControlName="operand1" />
  <input type="number" formControlName="operand2" />
  <select formControlName="operator">
    <option value="+">+</option>
    <option value="-">-</option>
    <option value="*">*</option>
    <option value="/">/</option>
  </select>
  <button (click)="calculate()" [disabled]="calculatorForm.invalid">
    Calculate
  </button>
  <p [colorChange]="color">{{ result | percent }}</p>
</form>

别忘了将 <st c="27234">ReactiveFormsModu</st><st c="27251">le</st> 添加到 <st c="27262">导入</st> <st c="27269">数组</st> <st c="27274">和</st> calculate()` 方法 的正确位置。 如果您有任何错误,可以检查源代码。 这可以在 https://github.com/PacktPublishing/Mastering-Angular-Test-Driven-Development/tree/main/Chapter%205/getting-started-angular-tdd/src/app/calculator找到。

在我们的终端中,运行 <st c="27581">ng serve -o</st> 命令后,我们将在 我们的浏览器中看到以下内容:

图 5.25 – 在浏览器中计算器用户界面

图 5.25 – 在浏览器中计算器用户界面

在下一节中,我们将实现我们 calculate() 按钮的 业务 逻辑。

为计算器组件编写测试

基于 TDD 的原则,我们将实现我们 calculate() 按钮的 业务 逻辑。

我们将从添加与用户输入的两个数字相加的功能开始。 在我们的 <st c="28036">calculator.component.spec.ts</st> 文件中,我们将添加以下 测试套件:

 it('should be added when the + operator is selected and the calculate button is clicked', () => {
    calculator.calculatorForm.get('operand1')?.setValue(2);
    calculator.calculatorForm.get('operand1')?.setValue(3);
    calculator.calculatorForm.get('operator')?.setValue('+');
    calculator.calculate();
    expect(calculator.result).toBe(5);
  });

在我们的终端中,运行 命令后,我们将 得到以下结果:

图 5.26 – 计算器运算符(+)选择测试在终端中失败

图 5.26 – 计算器运算符(+)选择测试在终端中失败

在我们的浏览器中,我们将得到 以下结果:

图 5.27 – 计算器运算符(+)选择测试在浏览器中失败

图 5.27 – 计算器运算符(+)选择测试在浏览器中失败

正如我们所见,我们的测试失败了,这是完全正常的,因为我们的 <st c="29965">calculate()</st> 方法目前在我们的 <st c="30010">CalculatorComponent</st>中是空的。我们现在需要添加使它功能化的最小代码。 我们需要考虑之前在前面章节中必须添加到类中的函数。 以下是当我们 更新 <st c="30264">calculate()</st> 方法在 <st c="30290">calculator.component.ts</st> 文件中的样子:

图 5.28 – 计算器运算符(+)选择

图 5.28 – 计算器运算符(+)选择

在我们的终端中,运行 <st c="30617">ng test</st> 命令后,我们将 得到以下结果:

图 5.29 – 计算器运算符(+)选择测试在终端中成功

图 5.29 – 计算器运算符(+)选择测试在终端中成功

在我们的浏览器中,我们将得到 以下结果:

图 5.30 – 计算器运算符选择(+)测试在浏览器中成功

图 5.30 – 计算器运算符选择(+)测试在浏览器中成功

现在我们将对 减法功能 进行相同的操作。 在我们的 <st c="32026">calculator.component.spec.ts</st> 文件中,我们将添加以下 测试套件:

 it('should subtract when the - operator is selected and the calculate button is clicked', () => {
    calculator.calculatorForm.get('operand1')?.setValue(2);
    calculator.calculatorForm.get('operand2')?.setValue(3);
    calculator.calculatorForm.get('operator')?.setValue('-');
 calculator.calculate();
 expect(calculator.result).toBe(-1);
});

在我们的终端中,运行 <st c="32467">ng test</st> 命令后,我们将 得到以下结果:

图 5.31 – 计算器运算符选择(-)测试在终端中失败

图 5.31 – 计算器运算符选择(-)测试在终端中失败

在我们的浏览器中,我们将得到 这个结果:

图 5.32 – 计算器运算符选择(-)测试在终端中失败

图 5.32 – 计算器运算符选择(-)测试在终端中失败

我们的测试失败了,这是完全正常的,因为我们的 <st c="34029">calculate()</st> 方法目前还没有处理我们 <st c="34092">CalculatorComponent</st>中的减法。我们现在需要添加最少的代码 来使其功能化。 我们需要考虑之前在前面章节中添加到类中的函数。 以下是我们在 <st c="34340">calculate()</st> 方法中更新 <st c="34366">calculator.component.ts</st> 文件时的样子:

图 5.33 – 计算器运算符选择(-)

图 5.33 – 计算器运算符选择(-)

在我们的终端中,在运行 <st c="34684">ng test</st> 命令后,我们将得到 这个:

图 5.34 – 计算器运算符选择(-)测试在终端中成功

图 5.34 – 计算器运算符选择(-)测试在终端中成功

在我们的浏览器中,我们将 得到 这个结果:

图 5.35 – 计算器运算符选择(-)测试在浏览器中成功

图 5.35 – 计算器运算符选择(-)测试在浏览器中成功

接下来是乘法功能。 在我们的 <st c="36139">calculator.component.spec.ts</st> 文件中,我们将添加这个 测试套件:

 it('should multiply when the * operator is selected and the calculate button is clicked', () => {
    calculator.calculatorForm.get('operand1')?.setValue(2);
    calculator.calculatorForm.get('operand2')?.setValue(3);
    calculator.calculatorForm.get('operator')?.setValue('*');
calculator.calculate();
expect(calculator.result).toBe(6);
});

在我们的终端中,在运行 <st c="36567">ng test</st> 命令后,我们将得到 这个:

图 5.36 – 计算器运算符选择(*)测试在终端中失败

图 5.36 – 计算器运算符选择(*)测试在终端中失败

在我们的浏览器中,我们将得到 这个结果:

图 5.37 – 计算器运算符选择(*)测试在浏览器中失败

图 5.37 – 计算器运算符选择(*)测试在浏览器中失败

我们的测试失败了,这是完全正常的,因为我们的 <st c="37995">calculate()</st> 方法目前还没有处理 <st c="38061">CalculatorComponent</st> 中的乘法。我们现在需要添加最少的代码来使其功能正常。我们需要考虑之前在前面章节中添加到类中的函数。 以下是我们在 <st c="38309">calculate()</st> 方法中更新 <st c="38335">calculator.component.ts</st> 文件时的样子:

图 5.38 – 计算器运算符选择 (*)

图 5.38 – 计算器运算符选择 (*)

在我们的终端中,运行 <st c="38597">ng test</st> 命令后,我们将得到以下结果:

图 5.39 – 终端中计算器运算符选择 (*) 测试成功

图 5.39 – 终端中计算器运算符选择 (*) 测试成功

在我们的浏览器中,我们将得到以下结果:

图 5.40 – 计算器运算符选择 (*) 测试成功在浏览器中

图 5.40 – 计算器运算符选择 (*) 测试成功在浏览器中

最后,我们将设置最后一个操作,即 <st c="40123">divide</st>。在我们的 <st c="40138">calculator.component.spec.ts</st> 文件中,我们将添加这个 <st c="40188">测试套件</st>

 it('should divide when the / operator is selected and the calculation button is clicked.', () => {
    calculator.calculatorForm.get('operand1')?.setValue(3);
    calculator.calculatorForm.get('operand2')?.setValue(2);
    calculator.calculatorForm.get('operator')?.setValue('/');
    calculator.calculate();
    expect(calculator.result).toBe(1.5);
  });

在我们的终端中,运行 <st c="40569">ng test</st> 命令后,我们将得到以下结果:

图 5.41 – 计算器运算符选择 (/) 测试失败在终端中

图 5.41 – 计算器运算符选择 (/) 测试失败在终端中

在我们的浏览器中,我们将得到以下结果:

图 5.42 – 浏览器中计算器运算符选择 (/) 测试失败

图 5.42 – 计算器运算符选择 (/) 测试失败在浏览器中

我们的测试失败了,这是完全正常的,因为我们的 <st c="42079">calculate()</st> 方法目前还没有处理 <st c="42139">CalculatorComponent</st> 中的除法。我们现在需要添加最少的代码来使其功能正常。我们需要考虑之前在前面章节中添加到类中的函数。 以下是我们在 <st c="42387">calculate()</st> 方法中更新 <st c="42413">calculator.component.ts</st> 文件时的样子:

图 5.43 – 计算器运算符选择 (/)

图 5.43 – 计算器运算符选择 (/)

在我们的终端中,运行 <st c="42662">ng test</st> 命令后,我们将 得到以下结果:

图 5.44 – 计算器运算符选择 (/) 测试在终端中成功

图 5.44 – 计算器运算符选择 (/) 测试在终端中成功

在我们的浏览器中,我们将 得到 以下结果:

图 5.45 – 计算器运算符选择 (/) 测试在浏览器中成功

图 5.45 – 计算器运算符选择 (/) 测试在浏览器中成功

现在我们可以运行 <st c="44292">ng serve -o</st> 命令来查看我们的 应用程序的行为:

图 5.46 – 计算器用户界面测试

图 5.46 – 计算器用户界面测试

摘要

在本章中,我们学习了如何测试 Angular 管道并将 TDD 应用于反应式编程的 Angular。 reactive programming。

我们探讨了 Angular 管道的测试,这些管道是用于在视图中显示输入数据的必要组件。 这个过程涉及创建覆盖不同场景和边缘情况的测试用例,以确保管道按预期行为。

我们深入研究了 TDD 在 Angular 反应式表单中的应用。 TDD 是一种软件开发方法,涉及在编写代码之前编写测试,这些测试驱动代码的开发。 这种方法确保代码是健壮的、可靠的,并且 经过良好测试的。

在下一章中,我们将探讨使用 Protractor、Cypress 和 Playwright 进行端到端测试。

第三部分:端到端测试

在本部分,您将了解使用 Protractor、Cypress 和 Playwright 进行端到端测试的概述。 然后,您将深入了解如何使用 Cypress 编写 端到端测试。

本部分包含以下章节:

  • 第六章, 使用 Protractor、Cypress 和 Playwright 探索端到端测试

  • 第七章, 理解 Cypress 及其在 Web 应用程序端到端测试中的作用

  • 第八章, 使用 Cypress 编写有效的端到端组件测试

第七章:6

使用 Protractor、Cypress 和 Playwright 探索端到端测试

在软件开发中,端到端 (E2E) 测试在确保应用程序从开始到结束的集成和功能方面发挥着至关重要的作用。 端到端测试涵盖了整个应用程序流程,模拟与真实用户的交互,以验证应用程序在各种 使用场景中是否按预期工作。

端到端测试提供了许多好处,有助于提高软件应用程序的整体质量和可靠性。 端到端测试可以发现并消除可能阻碍用户满意度和采用率的可用性问题。

通过彻底测试应用程序的集成组件,端到端测试减少了在实际使用过程中出现意外错误或中断的可能性。 端到端测试能够早期发现缺陷,最大限度地减少在开发周期后期解决问题所需的时间和成本。 端到端测试的主动故障检测可以显著降低长期 维护成本。

有几种强大的工具可用于简化端到端测试,每个工具都有其独特的优势和功能。 我们将探讨三个主要选项:Protractor、Cypress、Playwright。

在本章中,我们将探讨端到端测试的理论,经过长期和深入的分析后端到端测试的优势,以及一些端到端工具(如 Protractor、Cypress、Playwright)背后的哲学。

总结,以下是本章将涵盖的主要主题:

  • 发现 端到端测试

  • 分析在 项目中 E2E 测试的优势

  • 探索可用于端到端测试的不同工具,如 Protractor、Cypress、Playwright

技术要求

为了跟随本章中的示例和练习,你需要对 Angular 和 TypeScript 有基本的了解,以及以下 技术要求:

  • 在你的计算机上安装 Node.js 和 npm

  • 全局安装 Angular CLI

  • 在你的计算机上安装代码编辑器,例如 Visual Studio Code

理解端到端测试

端到端测试是一种从用户角度评估整个应用程序流程的软件测试方法,确保所有组件按预期工作,并且软件在现实场景中能够正确运行。 它包括对应用程序的用户界面、后端系统、外部 API 和数据交互的测试,以确保无缝和 一致的用户体验。 与关注单个组件的单元测试不同,端到端测试将整个应用程序作为一个统一的实体来检查,确保所有组件协同工作以提供 所需的功能。

发现端到端测试的好处

端到端测试提供了众多好处,使其成为软件开发团队的无价工具:

  • 早期发现缺陷:端到端测试能够在开发周期的早期发现缺陷,减少后期纠正所需的成本和努力。

  • 增强用户体验:端到端测试是一种确保软件应用从用户角度出发按预期工作的方法。 这种方法提高了整体用户体验并最小化了不满意的风险

  • 提高软件质量信心:端到端测试增强了软件质量的信心,降低了生产中意外问题的风险,并保护了软件开发团队的声誉

  • 简化开发流程:端到端测试可以自动化,简化开发流程,促进 持续集成和持续交付 (CI/CD)实践,并 使软件能够更快、更高效地发布。

在下一节中,我们将学习如何实现 端到端测试。

探索不同的端到端测试方法

端到端测试包括两种主要策略:基于脚本的测试和探索性测试。 基于脚本的测试依赖于自动化脚本来执行测试用例,确保测试过程的连贯性、可重复性和 效率。 另一方面,探索性测试是一种实际的方法,涉及在探索不同场景的同时手动测试应用程序。 让我们深入探讨这两种方法中的每一种: 探索不同的端到端测试方法

  • 基于脚本的测试:基于脚本的测试是一种软件测试方法,其中使用自动化脚本来执行测试用例。 这种方法保证了测试流程的一致性、可重复性和效率,使其成为确保软件应用程序平稳运行的可信工具。

    有几个流行的基于脚本的测试框架,包括 Protractor、Cypress 和 Playwright。 这些框架各有其特点和优势:

    • Protractor 是一个用于 Angular 和 AngularJS 应用程序的端到端测试框架。 它的目标是简化测试设置过程,使测试更易于阅读,并产生更容易理解的结果。

    • Cypress 是一款为现代网络设计的下一代前端测试工具。 它提供了一套完整的测试解决方案,包括直接在 JavaScript 中编写测试的能力,无需额外的预处理器或编译器。 Cypress 以其易用性和快速安装而闻名。

    • Playwright,由微软开发和维护,是一个开源的基于 Node.js 的端到端测试自动化框架。 它被创建出来是为了满足跨多个浏览器进行自动化端到端测试的需求。 Playwright 的主要目标是运行在主要的浏览器引擎上——Chromium、WebKit 和 Firefox。 它提供了广泛的本地移动测试功能,支持在 Android 和 iOS 平台上的移动自动化测试。

  • 探索性测试:探索性测试是一种动态灵活的软件测试方法,涉及测试人员积极探索应用程序并尝试不同的场景、输入和交互来识别错误和问题。 与基于脚本的测试不同,后者遵循预定义的测试用例和步骤,探索性测试基于测试人员的好奇心、创造力和直觉。

    这种方法在关于软件确切应该做什么或在实际情况下应该如何表现存在许多未知因素时特别有用。 在现实生活中的情况下。

    探索性测试通常在应用程序复杂或不断演变时使用。 当需要快速反馈、需求不明确或时间紧迫时,它也非常有用。 的精髓。

    探索性测试有不同的类型——自由式测试、基于场景的测试和 基于策略的测试:

    • 自由式测试:这在您需要快速熟悉 应用程序 时很有用

    • 基于场景的测试:这侧重于 现实世界的 使用场景

    • 基于策略的测试:这结合了 探索性测试 已知的 测试方法

    探索性测试有时在识别正式测试可能遗漏的更细微的缺陷方面更有用。 然而,它需要高度的专业技能和对 应用程序 的理解。

基于脚本的测试和探索性测试的比较

基于脚本的测试和探索性测试的比较 提供了我们几个 重要的见解:

  • 一致性和效率:基于脚本的测试保证了测试过程的 一致性和可重复性 ,这在需要大量测试的大规模项目中至关重要。 它也可以更高效,因为它消除了手动 测试执行 的需要。

  • 灵活性和发现:另一方面,探索性测试提供了更大的灵活性,因为它允许测试人员自由探索应用程序。 这可能导致发现脚本测试可能遗漏的潜在问题,尤其是在复杂应用程序或应用程序频繁更改的情况下。 frequent changes.

  • 文档:两种方法之间的一个显著区别是文档的级别。 基于脚本的测试通常涉及详细的测试用例和步骤的文档,而探索性测试可能涉及较少的文档,这可能导致关键错误 被忽略。

基于脚本的测试和探索性测试的限制

基于脚本的测试和探索性测试方法都有其局限性。 例如,探索性测试 可能受到测试人员偏见的影响,如果测试人员缺乏足够的知识或未能彻底探索,则可能 未能捕获所有潜在问题。 同样,如果测试用例没有涵盖所有 可能场景,脚本测试可能会错过明显的缺陷。

许多组织更喜欢混合方法,结合基于脚本和探索性测试的元素,以利用每种方法的优势并减轻 它们的弱点。

了解项目的 上下文,例如,它是一个小型项目 还是更大项目的一部分,可以指导选择基于脚本和 探索性测试之间的选择。

我们已经查看了一些之前提到的端到端测试工具。 现在我们将更详细地探讨它们。

利用端到端测试工具的力量

有多种工具可供使用,以促进端到端测试,使软件开发团队能够有效地实施这一 测试策略。

让我们更深入地了解一下 这些工具。

Selenium – 经证实的 Web 应用程序测试强大工具

Selenium 是一个著名的开源框架,用于自动化 Web 应用程序测试。 其全面的功能集满足广泛的测试需求,包括 功能、集成和跨浏览器测试。 Selenium 支持多种编程语言,包括 Java、Python 和 C#,使测试人员能够选择与他们专业知识和偏好相匹配的语言。 此外,Selenium 与最流行的浏览器具有广泛的兼容性,如 Chrome、Firefox、Safari 和 Edge。 这种广泛的兼容性确保了测试可以在不同的浏览器上运行,消除了浏览器特定的差异,并保证了无缝 的用户体验。

Cypress – 简洁和快速进行 Web 测试

Cypress 已经确立了自己 作为基于 JavaScript 的 Web 应用程序测试框架的地位,以其简洁、快速和易用而闻名。 与需要安装二进制驱动程序和配置的 Selenium 不同,Cypress 提供了一个独立的包,简化了安装过程。 基于 Node.js 的 Cypress 架构提供了卓越的性能,使测试能够快速运行,减少测试时间,并提高整体效率。 其直观的 API 和简单的语法 使其对所有级别的测试人员都易于访问,促进了快速测试开发和维护。

Appium – 在多个平台上进行移动应用程序测试

Appium 是一个多功能的框架,用于在不同平台上自动化移动应用程序测试,包括 iOS 和 Android。 它使用 Appium 驱动程序与原生移动应用程序通信 ,允许测试人员模拟用户交互,如点击、滑动和输入。 Appium 的跨平台兼容性意味着测试可以在真实设备或模拟器上运行,确保应用程序在不同环境中的一致性。

Protractor – Angular 应用程序的无缝自动化

Protractor,专为 Angular 应用程序设计,利用 Selenium 提供完整的测试框架。 它与 Angular 框架组件的集成简化了测试 开发和维护,允许测试人员与元素交互并使用 Angular 特定的功能。 Protractor 对本地和远程测试环境的支持满足广泛的 测试需求。

Playwright – 具有性能的跨浏览器测试

Playwright,由微软开发,已成为跨浏览器测试的强大工具。 它支持 Chromium、WebKit 和 Firefox 浏览器,使得 可以在不同的渲染引擎上执行全面测试。 基于 Node.js 的 Playwright 架构提供了卓越的性能,使得测试可以快速高效地运行。 它的 API 简单直观,使得所有级别的测试人员都能轻松使用。

端到端测试工具的选择取决于软件开发团队的具体需求和偏好。

以下是端到端 测试工具的优势:

  • Selenium 的灵活性和广泛的浏览器兼容性使其成为全面 Web 应用程序测试的可靠选择

  • Cypress 的简洁和速度吸引了寻求简化 Web 测试方法的团队

  • Appium 满足移动测试需求,支持在 iOS 和 Android 平台上自动化

  • 量角器简化了 Angular 应用程序的 自动化

  • Playwright 提供高性能的 跨浏览器测试

最终,最 有效的工具是那个与团队的专业知识、项目需求和 测试目标相匹配的工具。

在下一节中,我们将分析项目中进行端到端测试的好处。

分析项目中进行端到端测试的好处

采用端到端测试作为软件开发项目的一部分揭示了众多好处,这些好处提高了软件产品的质量和用户体验

尽管端到端测试提供了许多优点,但它们的实施并非没有问题。理解这些障碍对于制定克服它们的有效策略至关重要:

  • 复杂性:端到端测试通常具有错综复杂的结构,这使得它们难以创建和维护。这种复杂性源于需要测试多个应用程序组件和准确模拟真实用户的行为。

  • 扩展性:端到端测试容易不稳定,这种特性表现为间歇性故障,原因难以理解。这种间歇性可能会损害端到端测试结果的可信度。

  • 缓慢:由于端到端测试具有全局性质,需要与整个应用程序交互,并可能因等待外部系统响应而出现延迟,因此端到端测试通常运行速度较慢。

尽管存在固有的挑战,但端到端测试仍然是提高软件质量的无价工具。通过采用经过验证的策略,开发人员和测试人员可以有效地应对这些挑战,并收获端到端测试的许多好处:

  • 尽早参与:在开发周期早期启动端到端测试至关重要。这种积极主动的方法有助于及早发现缺陷,使缺陷的处理更加容易和成本效益更高。

  • 利用自动化:自动化通过简化执行过程和最大限度地减少时间和精力的消耗来加强端到端测试。自动化还便于将端到端测试无缝集成到CI/CD管道中。

  • 细致的测试设计:对端到端测试的设计进行细致的关注对于确保其有效性和效率至关重要。这意味着专注于测试最关键的用户流程和识别最可能发生故障的点。

  • 利用专用工具:有大量工具可供支持端到端测试。 选择一个符合项目需求并提供 用户友好功能的工具至关重要。

在下一节中,我们将探讨可用于端到端测试的不同工具,例如 Protractor、Cypress 和 Playwright。

探索 Protractor、Cypress 和 Playwright 进行端到端测试

有众多不同的端到端测试工具可用,选择正确的工具可能很棘手。 本综述将探讨三种流行的工具:Protractor、Cypress 和 Playwright。 我们将发现它们的优点、缺点和理想的使用案例,使您能够为您的 端到端测试选择理想的工具。

量角器

由谷歌的开发者团队制作,Protractor 最初是为 Angular 应用程序设计的,后来作为开源解决方案提供。 今天,它扩展了 其功能,不仅限于 Angular,还适应了非 Angular 应用程序。 它是 WebDriver.js 的增强版本,集成了所有 Selenium WebDriver 功能,并增加了专门用于 Angular 开发的函数。

什么是 Protractor?

Protractor 是一个开源的 测试框架,主要用于 Angular 和 AngularJS 应用程序的端到端测试。 虽然最初是为 Angular 设计的,但它已经发展到支持测试 Angular 和非 Angular 网络应用程序。 它以真实用户的方式工作,在真实网络浏览器中运行测试,从而产生真实的测试场景。 Protractor 是一个 Node.js 应用程序,它使用 WebDriverJS 作为 Selenium WebDriver API 的 JavaScript 绑定,充当 Selenium WebDriver 的包装器。 它可以创建与真实浏览器交互的测试,从而实现 端到端测试。

它集成了其他技术,如 Node.js、Jasmine、Selenium、Mocha 等。 由于其起源于 Angular 团队,并使用 Angular 特定的定位器来识别 DOM 元素,因此它特别适合 Angular 应用程序。 然而,截至 2022 年 11 月,Protractor 被认为已过时,其开发结束时间定于 2022 年底。 Protractor 背后的团队宣布这一决定是因为网络开发和 JavaScript 语言的变化,这使得 Protractor 与 当代应用程序的兼容性降低。

为什么选择 Protractor?

Protractor 提供了一套 Angular 特定的 API,这使得与 Angular 元素交互以及执行等待 Angular 进程完成、处理异步操作和管理与 Angular事件循环的同步等操作变得更加容易。

您可以模拟真实用户交互,例如点击按钮、填写表单、在页面间导航以及验证 Angular 元素的行为。这使得 Protractor 成为 Angular 应用程序端到端测试的强大工具。

它会在执行下一步之前自动等待 Angular 进程完成,从而消除了显式等待的需要,并减少了测试中的不可靠性。这使得它成为测试 Angular 应用程序的可靠选择。

它支持为 Angular 应用程序专门设计的各种定位器,包括模型、绑定、重复器和 CSS 选择器。这些定位器使得识别和交互 Angular 元素更加容易,这对于测试 Angular 应用程序尤其有用。

它允许您在包括 Chrome、Firefox 和 Safari 在内的多个浏览器中运行测试,从而实现 Angular 应用程序的跨浏览器测试。这一特性确保了您的 Angular 应用程序在不同浏览器中都能正确运行,这对于良好的用户体验至关重要。

将其与 CI 系统集成后,您可以在每次代码提交时自动执行测试,确保您的 Angular 应用程序保持稳定和功能正常。这一特性尤其有利于大型团队和项目,其中多个开发者正在同一 代码库上工作。

Protractor 测试功能

通过提供对异步操作的内置支持,包括回调、承诺和 async/await,Protractor 使开发者能够做到以下事项:以下是一些 Protractor 测试功能:

  • 提升测试执行速度:实现更快的端到端测试,特别是对于具有动态元素的 Angular Web 应用程序 dynamic elements

  • 增强测试可读性:使用现代异步编程方法编写更干净、更易于维护的测试 programming methods

  • 提高测试可靠性:简化处理动态行为并提高测试稳定性e test stability

Protractor 的一个关键特性是其自动等待功能。 以下是它是如何工作的:

  • <st c="19558">wait</st> 语句在您的测试中的每个动作之间。

  • Angular 同步:Protractor 与 Angular 框架集成,以了解应用程序何时完成处理并准备好交互。 这确保了您的测试不会在元素完全加载和可用之前尝试与之交互

虽然 Protractor 可以自动处理大多数等待场景,但仍然存在一些情况,你可能仍然需要 显式 等待:

  • <st c="20047">ExpectedConditions</st> API 用于定义等待的特定条件,例如等待元素可点击或特定文本 出现。

  • 长时间等待:默认的隐式等待时间可能对于某些场景来说太短。 您可以为特定的测试用例配置自定义的等待时间。

    Protractor 支持 Angular 和非 Angular 应用程序。 它为使用 AngularJS 构建的 Web 应用程序提供综合的端到端测试。 这使得它成为一个多功能的工具,可以用于测试广泛的 Web 应用程序。

Protractor 支持页面对象模式,这是一种设计模式,可以增强测试维护并减少代码重复。 使用此模式,您可以创建可重用的页面对象,这些对象可以在 不同的测试中 使用。

Cypress

专为 Web 开发和测试自动化设计,Cypress 是一个基于 JavaScript 的端到端测试框架,它 简化了 Web 和 API 测试。

什么是 Cypress?

Cypress 是一个开源的、基于 JavaScript 的端到端测试框架,专门为现代 Web 应用程序设计。 它提供了一种开发者友好的方法来自动化 Web UI 测试,简化了前端开发人员和 QA 工程师 的流程。

为什么选择 Cypress?

以下是使用 Cypress 进行您的 Web 应用程序测试的一些关键优势:

  • 简化设置:与 Selenium 等其他工具相比,Cypress 需要的最小配置。 通常只需安装包并使用 JavaScript 编写测试

  • 快速高效:Cypress 直接在浏览器中运行测试,消除了单独设置 WebDriver 和启动浏览器的需求。这使得测试执行速度显著提高,改善了开发和 测试工作流程。

  • 自动等待:Cypress 自动处理等待元素加载和变为交互式的过程。你不需要在测试中编写显式的等待或暂停,这减少了代码复杂性,并使测试更加健壮。

  • 易于调试:Cypress 提供了一个可视化调试器,允许你逐步执行测试,检查元素属性,并快速识别问题。这简化了调试过程。

  • 时间旅行调试:使用 Cypress 的时间旅行调试功能,你可以回放或快进测试执行,以确定失败发生的确切时刻,使故障排除更加高效。

  • 跨浏览器兼容性:Cypress 支持开箱即用的跨浏览器测试(Chrome、Firefox、Edge 等)。你可以配置测试在多种浏览器上运行,或使用 CI 平台来自动化浏览器测试。

  • 与开发工具集成:Cypress 与流行的开发者工具(如 DevTools)无缝集成,允许你在测试环境中利用现有的调试技能。

  • 活跃的社区和支持:Cypress 拥有一个庞大且活跃的社区,提供广泛的文档、教程和支持资源。这使得学习、使用和需要时获得帮助变得更加容易。

Cypress 测试功能

Cypress 拥有丰富的功能,有助于其在 Web 应用程序测试中的有效性:

  • 命令链 API:提供了一种使用 JavaScript 语法编写测试的自然方式,使测试易于阅读和维护。

  • 自动断言:简化了对预期元素属性和行为的检查,确保你的测试验证了正确的功能。

  • 截图和视频录制:允许在测试期间捕获截图或录制视频,这有助于可视化错误和调试问题。

  • 网络模拟:允许 模拟服务器响应,便于测试 API 交互、边缘情况和各种 网络场景

  • 自定义命令:授予创建可重用代码块的能力,用于频繁使用的测试操作,促进代码重用和模块化 和模块化

  • 集成测试:Cypress 可用于 Web 应用程序的单元和集成测试,提供全面的 测试覆盖率

通过利用 Cypress 的优势,您可以构建一个强大且高效的测试套件,有助于确保您 Web 应用程序的质量和可靠性。

Playwright

Playwright 是由 Microsoft 创建的一个现代、多功能且强大的浏览器自动化框架。 它使 开发者能够使用单个 API 编写自动化测试、抓取 Web 数据,并通过多个浏览器(包括 Chrome、Firefox 和 Microsoft Edge)与 Web 应用程序进行交互。

什么是 Playwright?

Playwright 是一个强大的开源 Node.js 库,用于在 Chrome、Firefox、Safari 和 Edge 等各种浏览器上自动化和测试 Web 应用程序。 它还可以自动化浏览器操作,如点击、表单填写和页面导航。 Playwright 是一个直观的平台,支持 JavaScript、TypeScript、Python 和 Java 等多种编程语言。 并行化的定义特征是,您可以在不编写 单独测试的情况下同时测试多个浏览器。

为什么选择 Playwright?

Playwright 支持跨多个浏览器进行测试,包括 Chromium、Firefox 和 WebKit。 这确保了跨浏览器兼容性和一致的 用户体验。

Playwright 以其 速度和效率 而闻名。其现代架构和高效的浏览器自动化使得与其他框架相比,测试执行速度更快,从而减少了整体测试时间

Playwright 的自动等待 API 和高级选择器引擎显著提高了测试稳定性,减少了测试不稳定性的可能性,并最大限度地减少了手动干预的需求。

Playwright 是为了处理现代 网络技术而构建的,例如 单页应用程序 (SPAs**),比其他框架做得更好。 它提供了与动态内容交互的原生支持,使得为复杂 应用程序编写测试变得更加容易。

与其他仅支持特定浏览器的框架不同,Playwright 提供了一个跨多个浏览器的统一 API,确保更广泛的兼容性和更全面的 测试套件。

Playwright 具有内置的模拟功能,允许你在组件级别编写精简的端到端测试。 这对于测试复杂组件而不必与整个应用程序交互特别有用。

Playwright 支持强大的伪 CSS 选择器,可以替代 XPath 的唯一用例。 这可以使你的测试更易于阅读 和维护。

由于 Playwright 能够在 100 毫秒内创建测试上下文,因此在运行相同测试时,它比 Selenium 快约 30%。

与 Selenium 相比,Playwright 需要更少的依赖,并且安装过程更加简单。 这意味着你可以更快地开始编写测试,并且 更少地遇到麻烦。

Playwright 测试的特点

让我们看看 Playwright 测试的 关键特性

  • Playwright 在不同浏览器之间提供一致和可靠的自动化,确保你的网络应用程序在所有 主要平台上都能良好运行。

  • Playwright 以其速度和效率而闻名,使其成为测试和自动化 网络交互的绝佳选择。

  • 你可以以无头模式(没有可见的浏览器 UI)运行 Playwright 脚本以实现更快的执行,或者以有头模式进行调试 和交互。

  • 剧本编写者支持流行的编程语言,如 TypeScript、Python 和 Java。 这意味着你可以用你最 舒适的语言编写自动化脚本。

  • Playwright 使用原生 浏览器自动化 API 准确地模拟用户交互,从而产生更 可靠的测试。

你可以利用 Playwright 的全部功能来利用浏览器 DevTools,这使得检查、调试和诊断你的 网络应用程序中的问题变得更加容易。

总结

在本章中,我们学习了端到端测试,这是一种确保应用程序从开始到结束完美运行的必要软件开发实践。 端到端测试涉及所有集成系统,无论是内部还是外部,以识别依赖关系并验证信息流的顺畅。

我们还了解了最常用的端到端测试工具,Protractor、Cypress 和 Playwright。 我们探讨了每个工具的优势:

  • Protractor: 专为 Angular 应用程序设计,提供用于与元素交互和处理异步操作的 Angular 特定 API

  • Cypress: 一款用户友好的工具,支持异步/await 语法和并发测试执行(尽管免费版本存在一些限制)

  • Playwright: 提供了一种现代的异步/await 测试脚本方法,鼓励同时执行测试以加快执行速度,并拥有一个简单的 API 以实现高效的测试开发

了解这些工具的优势和局限性,使我们作为开发者能够做出明智的决定,并选择最适合项目特定需求、考虑技术栈和团队熟悉度的工具。

以下章节将更深入地探讨 Cypress 及其在 Web 应用程序端到端测试中的作用。 这个专注的总结强化了章节的关键学习成果,突出了其价值(了解端到端测试和工具),并概述了下一章的主题。

第八章:理解 Cypress 及其在 Web 应用端到端测试中的作用

在 Web 开发的世界中,Angular 已经成为构建动态和强大 Web 应用程序最受欢迎的框架之一。凭借其丰富的特性和强大的功能,Angular 使开发者能够创建无缝的用户体验。 然而,确保这些应用程序的可靠性和功能需要彻底的测试,这正是Cypress 发挥作用的地方。

Cypress 是一个多功能的端到端 (E2E) 测试框架,它能够无缝地集成到 Angular 项目中,允许开发者编写和执行覆盖整个应用程序流程的组件测试。 在本章中,我们将探讨 Cypress 在 Angular 项目端到端测试中的作用,并指导您通过发现、设置和编写第一个组件测试的过程来使用 Cypress。 使用 Cypress。

我们将首先了解 Cypress,并理解其独特的特性和优势。从其直观的 API 到实时重新加载和调试功能,Cypress 为开发者提供了一种无缝且高效的测试体验。 通过掌握 Cypress 的核心概念及其与 Angular 的集成,开发者可以利用其力量确保其 Angular 应用程序的质量和可靠性。 Angular 应用程序。

接下来,我们将深入了解在 Angular 项目中设置 Cypress 的过程。我们将探讨必要的配置和依赖项,指导您通过步骤将 Cypress 无缝集成到您的 Angular 开发工作流程中。 在打下坚实的基础后,您将准备好在您的Angular 项目中充分利用 Cypress 的全部潜力。

最后,我们将指导您在 Angular 项目中使用 Cypress 编写第一个端到端组件测试的过程。我们将涵盖编写有效组件测试的必要方面,包括选择元素、与应用程序交互以及断言预期行为

概括来说,以下是本章将涵盖的主要主题:

  • 发现 Cypress 及其在 Angular 项目中的作用

  • 在 Angular 项目中设置 Cypress

  • 在 Angular 项目中使用 Cypress 编写第一个端到端组件测试

技术要求

为了跟随本章中的示例和练习,你需要对 Angular 和 TypeScript 有基本的了解,以及以下 技术要求:

  • Node.js 和 Node 包管理器 (npm) 安装在你电脑上

  • Angular CLI 全局安装

  • 安装在你电脑上的代码编辑器,例如 Visual Studio Code。

本章的代码文件可以在 以下位置找到 https://github.com/PacktPublishing/Mastering-Angular-Test-Driven-Development/tree/main/Chapter%207

探索 Cypress 及其在 Angular 项目中的作用

测试在确保应用程序的 质量和可靠性方面发挥着至关重要的作用。 当涉及到 Angular 项目时,开发者需要一个与框架组件无缝集成的强大测试框架,并提供高效全面的测试体验。 这就是 E2E 测试框架 Cypress 发挥作用的地方。 在本节中,我们将探讨 Cypress 及其在 Angular 项目中的作用,揭示其独特的特性和优势。

了解 Cypress

Cypress 是一个基于 JavaScript 的强大 测试框架,允许开发者编写和执行 Web 应用的端到端测试。 Cypress 与其他测试框架的不同之处在于其能够在浏览器中直接运行测试,实现实时重新加载和调试。 这一特性使得 Cypress 成为 Angular 项目的理想选择,开发者可以在真实环境中测试他们的应用程序,模拟真实的 用户交互。

与 Angular 无缝集成

Cypress 与 Angular 项目无缝集成,使其成为测试 Angular 应用程序的绝佳选择。 利用 Angular 的强大组件、指令和服务,允许开发者编写涵盖整个应用程序流程的测试。 无论是测试单个组件、验证复杂的 用户流程,还是确保跨浏览器兼容性,Cypress 都提供了完成这些 任务所需的工具和能力。

高效的测试工作流程

Cypress 的一个关键优势是它直观的 API,它简化了编写测试的过程。 凭借其声明性语法,开发者可以轻松选择元素,与应用程序交互,并断言预期的行为。 Cypress 还提供了一套全面的内置命令和断言,使得编写表达性和可读性强的测试更加容易。 此外,Cypress 还提供了强大的调试功能,允许开发者检查元素,逐步执行代码,并在 实时中排查问题。

实时重新加载和调试

Cypress 的实时 重新加载和调试功能在 Angular 项目中尤其有价值。 随着开发者对代码的修改,Cypress 会自动重新加载应用程序,实时反映更新。 此功能显著加快了测试过程,因为开发者可以立即看到他们更改的影响,无需手动刷新浏览器。 此外,Cypress 强大的调试工具使开发者能够快速定位和解决问题,从而提高测试工作流程的整体效率。

在下一节中,我们将学习如何在我们的 Angular 项目中配置 Cypress。

在我们的 Angular 项目中设置 Cypress

现在我们已经了解了 Cypress 是什么以及它在 Angular 项目中的作用,我们将探讨如何在 我们的项目中配置它。

安装 Cypress

首先,您需要在您的机器上安装 Node.js 和 npm。如果您还没有安装,请从官方 Node.js 网站下载并安装。

一旦您安装了 Node.js 和 npm,打开您的终端并导航到您的 Angular 项目的根目录。 运行以下命令将 Cypress 作为 开发依赖项安装:

 $ npm install cypress --save-dev

之前的命令将在您的项目中下载并安装 Cypress。 这是在 终端中的工作方式:

图 7.1 – Cypress 安装

图 7.1 – Cypress 安装

安装后,您可以在 package.json 文件中看到依赖关系:

图 7.2 – Cypress 在 package.json 中的依赖关系

图 7.2 – Cypress 在 package.json 中的依赖关系

在下一节中,我们将探讨如何 配置 Cypress。

配置 Cypress

如前所述,我们将逐步了解如何 配置 Cypress:

  1. 在安装 Cypress 后,从你的 项目根目录 运行以下命令:

    <st c="7266">$ npx cypress open</st>
    

图 7.3 – 在命令行中打开 Cypress

图 7.3 – 在命令行中打开 Cypress

现在你将看到浏览器启动,并显示如图 图 7**.4* 所示的界面:

图 7.4 – Cypress 启动界面

图 7.4 – Cypress 启动界面

  1. 点击 继续,你将被重定向到以下界面:

图 7.5 – Cypress E2E 测试首选界面

图 7.5 – Cypress E2E 测试首选界面

  1. 选择 E2E 测试 ,你将被重定向到如图 图 7**.6* 所示的界面:

图 7.6 – Cypress E2E 测试控制面板

图 7.6 – Cypress E2E 测试控制面板

这些文件是针对 Web 应用程序的 Cypress 测试设置的一部分。 让我们分析一下每个 文件的功能:

  • <st c="9013">cypress.config.ts</st>: <st c="9038">cypress.config.ts</st> 文件允许你根据项目需求自定义 Cypress,包括设置代理配置、配置网络请求以及指定自定义命令行标志。

  • <st c="9241">cypress/support/e2e.ts</st>: 此文件通常包含可在所有 E2E 测试中使用的全局命令和实用工具。 这是一个可以定义执行常见操作(如登录、在页面间导航或以一致的方式与元素交互)的函数的地方。 在这里定义这些命令,你可以确保它们在每一个测试文件中都是可用的,无需重新定义。 它们。

  • <st c="9652">cypress/support/comman</st><st c="9675">ds.ts</st>:与 <st c="9695">e2e.ts</st>类似, <st c="9707">commands.ts</st> 文件用于使用自定义命令扩展 Cypress 的内置命令。 此文件专门用于添加可在您的 测试套件中使用的 新命令。 这些命令可以封装复杂的交互或一系列动作,这些动作您发现自己需要在多个 测试中重复。 定义自定义命令有助于保持您的测试 DRY (不要重复自己),使它们更容易维护和了解。

  • <st c="10142">cypress/fixtures/example.json</st>:固定文件夹用于存储包含测试中使用的数据的 JSON 文件。 这些可能是模拟 API 响应、用于数据库播种的样本数据或任何其他测试运行所需的静态数据。 <st c="10392">example.json</st> 文件将是一个这样的固定文件,包含测试可能需要与之交互的示例数据。

  1. 点击 继续后,您将看到此界面以选择您首选的 E2E 测试浏览器,如图 图 7**.7

图 7.7 – Cypress 中的浏览器选择界面

图 7.7 – Cypress 中的浏览器选择界面

  1. 一旦完成所有配置,你应该能够访问界面,如图 所示 图 7**.8

图 7.8 – Cypress 端到端测试仪表板界面

图 7.8 – Cypress 端到端测试仪表板界面

  1. 在 Angular 项目目录中,创建了一个 <st c="11126">cypress</st> 文件夹 如下:

图 7.9 – Cypress 支持文件夹

图 7.9 – Cypress 支持文件夹

  1. 最后,我们将把这个脚本添加到 <st c="11310">package.json</st> 中,以避免多次执行 <st c="11339">npx cypress open</st>

图 7.10 – Cypress 命令在 package.json 脚本中

图 7.10 – Cypress 命令在 package.json 脚本中

在下一节中,我们将探讨如何编写我们的第一个 端到端测试。

编写您的第一个端到端测试

一旦 Cypress 已正确配置用于端到端测试,我们就可以开始工作了。 让我们看看如何编写我们的第一个 端到端测试:

  1. 现在 Cypress 已安装并配置好,你可以开始编写你的第一个测试。 你可以点击 创建新规范 在此界面中,如图 图 7**.11

Figure 7.11 – Cypress 中的规范创建界面

图 7.11 – Cypress 中的规范创建界面

  1. 完成这些后,你可以在 图 7**.12 中看到一个名为 <st c="12159">spec.cy.ts</st>的新文件,它位于你的项目的 <st c="12186">e2e</st> 文件夹中的 <st c="12215">cypress</st> 文件夹:

图 7.12 – 规范文件

图 7.12 – 规范文件

此文件包含以下内容:

图 7.13 – 我们的规范文件的源代码

图 7.13 – 我们的规范文件的源代码](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/ms-ng-tst-dvn-dev/img/B21146_07_13.jpg)

  1. 现在,当你返回到启动端到端测试的浏览器时,你可能会感到惊讶,如图 图 7**.14

Figure 7.14 – 端到端测试因编译错误而失败

图 7.14 – 端到端测试因编译错误而失败错误

  1. 要解决这个问题,你只需从你的 <st c="14896">sourceMap: true</st> 中删除 <st c="14922">tsconfig.json</st> 文件, 然后你将得到这个:

     {
      "compileOnSave": false,
      "compilerOptions": {
        "baseUrl": "./",
        "outDir": "./dist/out-tsc",
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "noImplicitOverride": true,
        "noPropertyAccessFromIndexSignature": true,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "declaration": false,
        "downlevelIteration": true,
        "experimentalDecorators": true,
        "moduleResolution": "node",
        "importHelpers": true,
        "target": "ES2022",
        "module": "ES2022",
        "useDefineForClassFields": false,
        "lib": [
          "ES2022",
          "dom"
        ]
      },
      "angularCompilerOptions": {
        "enableI18nLegacyMessageIdFormat": false,
        "strictInjectionParameters": true,
        "strictInputAccessModifiers": true,
        "strictTemplates": true
      }
    }
    

    正如你所见 现在它工作正常:

图 7.15 – 显示所有规范文件的界面

图 7.15 – 显示所有规范文件的界面

  1. 然后,你可以点击 <st c="15769">spec.cy.ts</st> 文件,如果一切顺利,你将得到这个:

图 7.16 – 我们端到端测试的成功完成

图 7.16 – 我们端到端测试的成功完成

恭喜! 你刚刚编写了你的第一个端到端测试! 技术上,我们用 Cypress 的便捷性生成了它。 的。

现在我们知道了如何编写端到端测试,本章的目标已经实现。 我们将在下一节中总结我们所学到的所有内容。

总结

本章涵盖了发现 Cypress、配置它以及将其应用于编写 E2E 测试的 Angular 项目中的过程。 章节从 Cypress 工具的介绍开始,解释了为什么它是 Angular 社区中最广泛使用的工具,然后展示了如何在 Angular 项目中配置 Cypress,最后逐步教你如何使用 Cypress 在 Angular 中编写第一个 E2E 测试。

下一章,我们将深入探讨使用 Cypress 在测试驱动开发方法中编写 E2E 测试,同时我们将改进和重构与我们的组件相关的先前测试,并遵循 Cypress 的良好实践。 逐步进行。

在下一章中,我们将更深入地探讨使用 Cypress 在测试驱动开发方法中编写 E2E 测试,同时我们将改进和重构与我们的组件相关的先前测试,并尊重 Cypress 的良好实践。

第九章:8

使用 Cypress 编写有效的端到端组件测试

在不断发展的 Web 开发世界端到端 (E2E) 测试在确保 Web 应用程序的可靠性和健壮性方面发挥着至关重要的作用。 端到端测试模拟真实世界的用户场景,涵盖应用程序的所有方面,包括用户界面、API、数据库和 其他集成。

开发者经常使用的端到端测试工具之一是 Cypress。Cypress 是一个免费提供的 全面的 Web 应用程序测试解决方案,旨在简化并优化开发者的测试工作流程。 Cypress 与其他测试框架的区别在于其能够在浏览器中直接执行测试,为开发者提供对正在测试的应用程序的增强控制和洞察。

在本章中,我们将重点介绍如何使用 Cypress 为计算器应用程序编写有效的端到端测试。我们将探讨 Cypress 的基础知识、测试结构以及一些高级技术。 我们的目标是指导开发者编写全面的端到端测试,以有效地验证计算器应用程序的功能,确保它在 不同条件下表现如预期。

总结来说,本章将涵盖以下主要主题:

  • 结构化 端到端测试

  • 编写端到端 测试用例

  • 使用 Cypress 自定义命令

技术要求

为了跟随本章中的示例和练习,你需要对 Angular 和 TypeScript 有基本的了解,以及以下 技术要求:

  • Node.js 和 npm 已安装在你的 计算机上

  • 全局安装的 Angular CLI

  • 安装在你的 计算机上的代码编辑器,例如 Visual Studio Code

本章的代码文件可以在 以下位置找到 https://github.com/PacktPublishing/Mastering-Angular-Test-Driven-Development/tree/main/Chapter%208

端到端测试的结构化

Cypress 测试使用 the <st c="1926">describe(</st><st c="1935">)</st>, <st c="1939">context(</st><st c="1947">)</st>, <st c="1951">it()</st>, 和 <st c="1958">specify()</st> 函数来构建,这些函数是从 Mocha(Mocha 是一个功能丰富的 JavaScript 测试框架,在 Node.js 和浏览器上运行,旨在使异步测试变得简单和愉快。 它因其在前端和后端测试应用中的多功能性而备受推崇,为开发者提供了广泛的好处)。 The <st c="2325">describe()</st> 函数用于组合相关的测试s, <st c="2378">context()</st> 类似于 <st c="2399">describe()</st> <st c="2419">it()</st> <st c="2428">specify()</st> 用于编写单个 测试用例。

我们将围绕我们的计算器应用程序的功能来构建我们的测试。 我们的计算器执行四个操作: 加法, 减法, 乘法, 和 除法。在我们的 <st c="2671">e2e</st> 文件夹中的 <st c="2689">cypress</st> 文件夹中,我们将创建一个名为 <st c="2732">calculator.cy.ts</st> 的文件:

图 8.1 – Cypress 文件夹中的 e2e 文件夹下的 calculator.cy.ts 文件

图 8.1 – Cypress 文件夹中的 e2e 文件夹下的 calculator.cy.ts 文件

一旦文件创建完成,我们将添加 以下这些代码行,我们将在 稍后解释:

 describe('Calculator Functionality', () => {
context('Addition', () => {
it('adds two positive numbers correctly', () => {
// Test case for addition
});
it('adds two negative numbers correctly', () => {
// Test case for addition
});
});
it('add one positive number and one negative number correctly', () => {
// Test case for addition
});
});
context('Subtraction', () => {
it('subtracts two positive numbers correctly', () => {
// Test case for subtraction
});
it('subtracts two negative numbers correctly', () => {
// Test case for subtraction
});
it('subtracts one positive number and one negative number correctly', () => {
// Test case for subtraction
});
});
});

前面的代码行以一个名为 <st c="3715">Calculator Functionality</st>的测试套件的定义开始。 测试套件 是一组相关的 测试。 在这种情况下,描述块中包含的所有测试都与计算器的 功能相关联。

<st c="3897">上下文</st> 函数用于将相关的测试组合在一起。 在这里,它将所有与计算器的 <st c="4009">加法</st> <st c="4022">减法</st> 功能相关的测试组合在一起。

“The“it“function defines“an individual test scenario.Here, it defines a test case for the addition of two positive numbers or two negative numbers and “their subtraction.”

“Now, we’ll add the other operations (namely,” “Multiplication“and” “Division”“), and we’ll finalize the implementation of our “test suite:

 context('Multiplication', () => {
it('multiplies one positive number and zero correctly', () => {
// Test case for multiplication
});
it('multiplies two positive numbers correctly', () => {
// Test case for multiplication
});
it('multiplies two negative numbers correctly', () => {
// Test case for multiplication
});
it('multiplies one positive number and one negative number correctly', () => {
// Test case for multiplication
});
});
context('Division', () => {
it('divides a positive non-zero number by another positive non-zero number', () => {
// Test case for division
});
it('divides a negative non-zero number by another positive non-zero number', () => {
// Test case for division
});
it('divides a negative non-zero number by another negative non-zero number', () => {
// Test case for division
});
it('divides a positive non-zero number by another negative non-zero number', () => {
// Test case for division
});
it('divides a positive non-zero number by zero', () => {
// Test case for division
});
it('divides a negative non-zero number by zero', () => {
// Test case for division
});
it('divide zero by zero', () => {
// Test case for division
});
});

“This code sets up a structure” “for testing the addition, subtraction, multiplication, and division functionalities of a calculator application.” “Each operation has its own group of tests, and each test case describes a” “specific scenario.”

“In our browser, we have the” “following result:”

Figure 8.2 – Calculator E2E tests succeeded after setting up a structure for testing addition, subtraction, multiplication, and division

“Figure 8.2 – Calculator E2E tests succeeded after setting up a structure for testing addition, subtraction, multiplication, and division”

“The result in the browser proves” “that our test cases written in the E2E context are without errors.” “However, we haven’t yet written the corresponding logic in our” “test cases.”

“In the next section, we’ll now write the corresponding tests for “each context.”

“Writing test cases”

“When writing test cases, you should” “try to cover different scenarios, boundary cases, and potential errors.” “For a calculator application, you may want to test addition, subtraction, multiplication, and division operations under normal conditions and in borderline cases (such as” *“division” *“by zero”“).”

“Let’s complete our different contexts with our” “different tests.”

“Addition context”

“In this section, we’ll look at the various E2E test cases related” “to addition.”

“Adds two positive numbers correctly”

“In this test case, we will” “see how to write the E2E test to sum two” “positive numbers:”

 it('adds two positive numbers correctly', () => {
cy.visit('http: //localhost:4200/');
cy.get('input').first().type('5');
cy.get('select').select('+').should('have.value', '+');
cy.get('input').last().type('3');
cy.get('button').click();
cy.get('p').should('have.text', '8');
});

“In the preceding co”“de,” the “it()” “function is used to define an individual test case.” “The first argument is a string describing what the test case is supposed to do.” “In this case, it is” <st c="8112">“'adds two positive”</st> <st c="8131">“numbers correctly'”</st>“.”

使用 <st c="8155">cy.visit()</st> 命令来访问一个 URL。 在这里,这是通向我们的计算器用户界面的 URL。

以下行用于在计算器的第一个输入字段中输入 <st c="8300">'5'</st> 使用 <st c="8347">cy.get()</st> 函数从 DOM 中获取元素。 然后 <st c="8407">first()</st> 函数用来获取相应元素集合的第一个元素。 接着 <st c="8503">type()</st> 命令用来在文本输入字段中输入。

然后,选择 <st c="8681">'</st><st c="8682">+'</st>. 使用 <st c="8690">select()</st> 命令从下拉菜单中选择一个选项。 然后 <st c="8759">should()</st> 函数用来对应用程序的状态做出声明。

以下行用于在计算器的第一个输入字段中输入 <st c="8880">'3'</st> 使用 <st c="8927">cy.get()</st> 函数来获取 DOM 元素。 然后 <st c="8978">last()</st> 函数用来获取相应元素集合的最后一个元素。 接着 <st c="9076">type()</st> 命令用来在文本输入字段中输入。

然后,点击按钮进行计算。 使用 <st c="9190">click()</st> 命令来模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="9315">'8'</st>)是否正确显示。 然后 <st c="9350">should()</st> 函数再次被用来进行这个断言。

在我们的浏览器中,我们得到 以下结果:

图 8.3 – “正确添加两个正数”的端到端测试成功

8.3 – “正确添加两个正数”的端到端测试成功

在下一节中,我们将学习如何正确地添加两个 负数。

正确地添加两个负数

在这个测试用例中,我们将看到如何编写添加两个 负数的端到端测试: 负数:

 it('adds two negative numbers correctly', () => {
cy.visit('http: //localhost:4200/');
cy.get('input').first().type('-5');
cy.get('select').select('+').should('have.value', '+');
cy.get('input').last().type('-3');
cy.get('button').click();
cy.get('p').should('have.text', '-8');
});

<st c="10220">it()</st> 函数用于定义一个单独的测试用例。 第一个参数是一个字符串,描述测试用例应该做什么。 在这种情况下,它是 <st c="10377">'正确添加两个正数'</st> `

<st c="10420">cy.visit()</st> 命令用于访问一个 URL。 在这里,它是通向我们的计算器用户界面的 URL。

以下行用于在计算器的第一个输入字段中输入 <st c="10565">'-5'</st> <st c="10613">cy.get()</st> 函数用于从 DOM 中获取元素。 <st c="10676">first()</st> 函数用于获取相应元素集合的第一个元素。 <st c="10772">type()</st> 命令用于在文本输入字段中输入。

然后,从我们用户界面的下拉菜单中选择 <st c="10845">'+'</st> 运算符,并检查所选值确实是 <st c="10948">'+'</st>。 “ <st c="10957">select()</st> 命令用于从下拉菜单中选择一个选项。 <st c="11029">should()</st> 函数用于对应用程序的状态做出声明。

以下行用于在计算器的第一个输入字段中输入 <st c="11146">'-3'</st> <st c="11194">cy.get()</st> 函数用于获取 DOM 元素。 <st c="11249">last()</st> 函数用于获取相应元素集合的最后一个元素。 <st c="11343">type()</st> 命令用于在文本输入字段中输入。

然后点击按钮进行计算。 <st c="11456">click()</st> 命令用于模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="11581">'-8'</st>)是否正确显示。 <st c="11617">should()</st> 函数再次用于进行此断言。

在我们的浏览器中,我们有以下结果: 以下结果:

图 8.4 – “正确添加两个负数”的端到端测试成功

图 8.4 – “正确地添加两个负数”的端到端测试成功

在下一节中,我们将学习如何正确地添加一个正数和一个 负数。

正确地添加一个正数和一个负数

在这个测试用例中,我们将看到如何 编写端到端测试来添加一个正数和一个 负数:

 it('adds one positive number and one negative number correctly', () => {
cy.visit('http: //localhost:4200/');
cy.get('input').first().type('5');
cy.get('select').select('+').should('have.value', '+');
cy.get('input').last().type('-3');
cy.get('button').click();
cy.get('p').should('have.text', '2');
});

The <st c="12612">it()</st> 函数用于定义一个单独的测试用例。 第一个参数是一个字符串,描述了测试用例应该做什么。 在这种情况下,它是 <st c="12769">'正确地添加两个正数'</st> <st c="12788">数字'</st>

The <st c="12812">cy.visit()</st> 命令用于访问一个 URL。 在这里,这是通向我们的计算器 用户界面的 URL。

以下行用于在计算器的第一个输入字段中输入数字 <st c="12968">'5'</st> The <st c="13015">cy.get()</st> 函数用于从 DOM 中获取元素。 The <st c="13078">first()</st> 函数用于获取相应元素集合中的第一个元素。 The <st c="13173">type()</st> 命令用于在文本 输入字段中输入。

然后,从我们的用户界面上方的下拉菜单中选择 <st c="13246">'+'</st> 运算符,并检查所选值确实是 <st c="13349">'+'</st>。 The <st c="13358">select()</st> 命令用于从下拉菜单中选择一个选项。 The <st c="13430">should()</st> 函数用于对应用程序的状态做出声明。

以下行用于在计算器的第一个输入字段中输入数字 <st c="13558">'-3'</st> The <st c="13606">cy.get()</st> 函数用于获取 DOM 元素。 The <st c="13660">last()</st> 函数用于获取相应元素集合中的最后一个元素。 The <st c="13753">type()</st> 命令用于在文本 输入字段中输入。

然后,点击按钮执行计算。 The <st c="13867">click()</st> 命令用于模拟鼠标点击。

最后,代码的最后一行检查 计算结果(<st c="13992">'2'</st>)是否正确显示。 The <st c="14027">should()</st> 函数再次用于做出 这个断言。

在我们的浏览器中,我们得到以下结果: 以下结果:

图 8.5 – “正确地加一个正数和一个负数”的端到端测试成功

图 8.5 – “正确地加一个正数和一个负数”的端到端测试成功

在下一节中,我们将探讨减法的上下文。 subtraction.

减法上下文

在本节中,我们将探讨与减法相关的各种端到端测试用例。 related to subtraction.

正确减去两个正数

在这个测试用例中,我们将看到如何 编写减去两个正数的端到端测试: positive numbers:

 it('subtracts two positive numbers correctly', () => {
cy.visit('http: //localhost:4200/');
cy.get('input').first().type('5');
cy.get('select').select('-').should('have.value', '-');
cy.get('input').last().type('3');
cy.get('button').click();
cy.get('p').should('have.text', '2');
});

The <st c="15025">it()</st> 函数用于定义一个单独的测试用例。 第一个参数是一个字符串,描述了测试用例应该做什么。 在这种情况下,它是 <st c="15182">'subtracts two positive</st> <st c="15206">numbers correctly'</st>

The <st c="15230">cy.visit()</st> 命令用于访问一个 URL。 在这里,这是指向我们计算器用户界面的 URL。 user interface.

以下行用于在计算器的第一个输入字段中输入数字 <st c="15386">'5'</st> The <st c="15433">cy.get()</st> 函数用于从 DOM 中获取元素。 The <st c="15496">first()</st> 函数用于获取相应元素集合的第一个元素。 The <st c="15591">type()</st> 命令用于在文本输入字段中输入。 input field.

然后,从我们用户界面的下拉菜单中选择 <st c="15664">'-'</st> 运算符,并检查所选值确实是 <st c="15767">'-'</st>select()<st c="15784">命令用于从下拉菜单中选择一个选项。</st> <st c="15844">The</st>should()` 函数用于对应用程序的状态做出声明。

以下行用于在计算器的第一个输入字段中输入 数字 <st c="15976">'3'</st> 使用 <st c="16023">cy.get()</st> 函数获取 DOM 元素。 使用 <st c="16077">last()</st> 函数获取相应元素集合的最后一个元素。 使用 <st c="16170">type()</st> 命令在文本输入字段中输入。

然后,点击按钮执行计算。 使用 <st c="16284">click()</st> 命令模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="16409">'2'</st>)是否正确显示。 再次使用 <st c="16444">should()</st> 函数来执行这个断言。

在我们的浏览器中,我们得到以下结果:

图 8.6 – “正确减去两个正数”的端到端测试成功

图 8.6 – “正确减去两个正数”的端到端测试成功

在下一节中,我们将学习如何正确减去两个 负数。

正确减去两个负数

在这个测试用例中,我们将 了解如何编写减去两个 负数的端到端测试:

 it('subtracts two negative numbers correctly', () => {
cy.visit('http: //localhost:4200/');
cy.get('input').first().type('-5');
cy.get('select').select('-').should('have.value', '-');
cy.get('input').last().type('-3');
cy.get('button').click();
cy.get('p').should('have.text', '-2');
});

在前面的代码中,使用 <st c="17366">it()</st> 函数定义一个单独的测试用例。 第一个参数是一个字符串,描述测试用例应该做什么。 在这种情况下,它是 <st c="17523">'subtracts two negative</st> <st c="17547">numbers correctly'</st>

使用 <st c="17571">cy.visit()</st> 命令来访问一个 URL。 在这里,这是通向我们的计算器用户界面的 URL。

以下行用于在计算器的第一个输入字段中输入数字 <st c="17727">'-5'</st> 使用 <st c="17775">cy.get()</st> 函数从 DOM 中获取元素。 使用 <st c="17838">first()</st> 函数获取相应元素集合的第一个元素。 使用 <st c="17933">type()</st> 命令在文本输入字段中输入。

然后,从我们用户界面的下拉菜单中选择“ <st c="18006">'-'</st> ”运算符,并检查所选值确实是 <st c="18109">'-'</st>。 “ <st c="18118">select()</st> 命令用于从下拉菜单中选择一个选项。 <st c="18190">should()</st> 函数用于对应用程序的状态做出声明。

以下行用于在计算器的第一个输入字段中输入数字 <st c="18318">'-3'</st> <st c="18366">cy.get()</st> 函数用于获取 DOM 元素。 <st c="18420">last()</st> 函数用于获取相应元素集合中的最后一个元素。 <st c="18513">type()</st> 命令用于在文本输入字段中输入。

然后,点击按钮执行计算。 <st c="18627">click()</st> 命令用于模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="18752">'-2'</st>)是否正确显示。 <st c="18788">should()</st> 函数再次用于进行这个断言。

在我们的浏览器中,我们得到以下结果: 这是:

图 8.7 – “正确减去两个负数”端到端测试成功

图 8.7 – “正确减去两个负数”端到端测试成功

在下一节中,我们将学习如何正确减去一个正数和一个 负数。

正确减去一个正数和一个负数

在这个测试用例中,我们将看到如何 编写减去一个正数和一个 负数的端到端测试:

 it('subtracts one positive number and one negative number correctly', () => {
cy.visit('http: //localhost:4200/');
cy.get('input').first().type('5');
cy.get('select').select('-').should('have.value', '-');
cy.get('input').last().type('-3');
cy.get('button').click();
cy.get('p').should('have.text', '8');
});

<st c="19746">it()</st> 函数用于定义一个单独的测试用例。 第一个参数是一个字符串,描述了测试用例应该做什么。 在这种情况下,它是 <st c="19903">'正确减去一个正数和一个负数'</st> `

<st c="19974">cy.visit()</st> 命令用于访问一个 URL。 在这里,这是通向我们的计算器用户界面的 URL。 用户界面。

以下行用于在计算器的第一个输入字段中输入数字 <st c="20130">'5'</st> 然后 <st c="20177">cy.get()</st> 函数用于从 DOM 中获取元素。 接着 <st c="20240">first()</st> 函数用于获取相应元素集合中的第一个元素。 此外 <st c="20336">type()</st> 命令用于在文本输入字段中输入。

然后,从我们用户界面的下拉菜单中选择 <st c="20409">'-'</st> 运算符,并检查所选值确实是 <st c="20508">indeed</st> <st c="20513">'-'</st>。这里 <st c="20522">select()</st> 命令用于从下拉菜单中选择一个选项。 此外 <st c="20595">should()</st> 函数用于对应用程序的状态做出声明。

以下行用于在计算器的第一个输入字段中输入数字 <st c="20723">'-3'</st> 然后 <st c="20771">cy.get()</st> 函数用于获取 DOM 元素。 接着 <st c="20825">last()</st> 函数用于获取相应元素集合中的最后一个元素。 此外 <st c="20919">type()</st> 命令用于在文本输入字段中输入。

然后,点击按钮执行计算。 这里 <st c="21034">click()</st> 命令用于模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="21159">'8'</st>)是否正确显示。 这里 <st c="21195">should()</st> 函数再次用于进行这个断言。

在我们的浏览器中,我们得到以下结果:

图 8.8 – “正确减去两个负数”的端到端测试成功

图 8.8 – “正确减去两个负数”的端到端测试成功

在下一节中,我们将探讨乘法的 上下文。

乘法上下文

在本节中,我们将探讨与乘法相关的各种端到端测试用例。

正确地乘以非零数和零

在这个测试用例中,我们将看到如何编写端到端测试来乘以一个非零数和零:

 it('multiplies non-zero number and zero correctly', () => {
cy.visit('http: //localhost:4200/');
cy.get('input').first().type('5');
cy.get('select').select('*').should('have.value', '*');
cy.get('input').last().type('0');
cy.get('button').click();
cy.get('p').should('have.text', '0');
});

The <st c="22213">it()</st> 函数用于定义一个单独的测试用例。 第一个参数是一个字符串,描述了测试用例应该做什么。 在这种情况下,它是 <st c="22370">'乘以非零数和</st> <st c="22402">正确地乘以零'</st>

The</st> cy.visit()` 命令用于访问一个 URL。 这里,它是通向我们的计算器用户界面的 URL。

以下行用于在计算器的第一个输入字段中输入数字 <st c="22579">'5'</st> cy.get() 函数用于从 DOM 中获取元素。 first() 函数用于获取对应元素集合中的第一个元素。 type() 命令用于在文本输入字段中输入。

然后,从我们用户界面的下拉菜单中选择 <st c="22857">'*'</st> 运算符,并检查所选值是否确实为“*” <st c="22961">。 select() 命令用于从下拉菜单中选择一个选项。 should() 函数用于对应用程序的状态做出声明。

以下行用于在计算器的第一个输入字段中输入数字 <st c="23171">'0'</st> cy.get() 函数用于获取 DOM 元素。 last() 函数用于获取对应元素集合中的最后一个元素。 type() 命令用于在文本输入字段中输入。

然后,点击按钮进行计算。 click() 命令用于模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="23605">'0'</st>)是否正确显示。 <st c="23641">should()</st> 函数再次用于做出这个断言。

在我们的浏览器中,我们 这个结果:

图 8.9 – “正确乘以非零数和零”端到端测试成功

图 8.9 – “正确乘以非零数和零”端到端测试成功

在下一节中,我们将学习如何正确地乘以两个 正数。

正确乘以两个正数

在这个测试用例中,我们将看到 如何编写乘以两个 正数的端到端测试:

 it('multiplies two positive numbers correctly', () => {
cy.visit('http: //localhost:4200/');
cy.get('input').first().type('5');
cy.get('select').select('*').should('have.value', '*');
cy.get('input').last().type('3');
cy.get('button').click();
cy.get('p').should('have.text', '15');
});

<st c="24575">it()</st> 函数用于定义一个单独的测试用例。 第一个参数是一个字符串,描述了测试用例应该做什么。 在这种情况下,它是<st c="24732">'multiplies two positive</st> <st c="24757">numbers correctly'</st>

<st c="24781">cy.visit()</st> 命令用于访问一个 URL。 在这里,这是通向我们的计算器用户界面的 URL。

以下行用于在计算器的第一个输入字段中输入数字 <st c="24937">'5'</st> <st c="24984">cy.get()</st> 函数用于从 DOM 中获取元素。 <st c="25048">first()</st> 函数用于获取相应元素集合中的第一个元素。 <st c="25143">type()</st> 命令用于在文本输入字段中输入。

然后,从我们的用户界面上面的下拉菜单中选择 <st c="25216">'*'</st> 运算符,并确认所选的值是确实 <st c="25320">'*'</st><st c="25329">select()<st c="25337">命令用于从下拉菜单中选择一个选项。</st>should()</st> 函数用于对应用程序的状态做出声明。

以下行用于在计算器的第一个输入字段中输入数字 <st c="25515">'3'</st> cy.get()函数用于获取 DOM 元素。 last()函数用于获取相应元素集合中的最后一个元素。 type()命令用于在文本输入字段中输入。

然后,点击按钮进行计算。计算 命令用于模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="25965">'15'</st>)是否正确显示。 再次使用 should()函数来做出这个断言。

在我们的浏览器中,我们得到以下结果:

图 8.10 – “正确乘以两个正数”的端到端测试成功

图 8.10 – “正确乘以两个正数”的端到端测试成功

在下一节,我们将学习如何正确地乘以两个 负数。

正确乘以两个负数

在这个测试用例中,我们将看到 如何编写乘以两个 负数的端到端测试:

 it('multiplies two negative numbers correctly', () => {
cy.visit('http: //localhost:4200/');
cy.get('input').first().type('-5');
cy.get('select').select('*').should('have.value', '*');
cy.get('input').last().type('-3');
cy.get('button').click();
cy.get('p').should('have.text', '15');
});

it()函数用于定义一个单独的测试用例。 第一个参数是一个字符串,描述了测试用例应该做什么。 在这种情况下,它是 <st c="27048">'正确乘以两个负</st> <st c="27073">数'</st>

cy.visit()命令用于访问一个 URL。 在这里,这是通向我们的计算器用户界面的 URL。

以下行用于在计算器的第一个输入字段中输入数字 <st c="27253">'-5'</st> cy.get()函数用于从 DOM 中获取元素。 first()函数用于获取相应元素集合中的第一个元素。 type()命令用于在文本输入字段中输入。

然后,从我们的用户界面的下拉菜单中选择 <st c="27534">'*'</st> 运算符,并检查所选值确实是 <st c="27637">'*'</st>。</st> select()<st c="27654">命令用于从下拉菜单中选择一个选项。</st> should()` 函数用于对应用程序的状态做出声明。

以下行用于在计算器的第一个输入字段中输入数字 <st c="27847">'-3'</st> <st c="27895">cy.get()</st> 函数用于获取 DOM 元素。 <st c="27949">last()</st> 函数用于获取对应元素集合中的最后一个元素。 <st c="28039">type()</st> 命令用于向文本输入字段中输入。

然后,点击按钮执行计算。 <st c="28154"> <st c="28158">click()</st> 命令用于模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="28283">'15'</st>)是否正确显示。 <st c="28303"> <st c="28315"> <st c="28319">should()</st> 函数再次用于做出 `这个断言。

在我们的浏览器中,我们得到 以下结果:

图 8.11 – “正确乘以两个负数”的端到端测试成功

图 8.11 – “正确乘以两个负数”的端到端测试成功

在下一节中,我们将学习如何正确地乘以一个正数和一个 负数。

正确乘以一个正数和一个负数

在这个测试用例中,我们将看到如何 编写乘以一个正数和一个 负数的端到端测试:

 it('multiplies one positive number and one negative number correctly', () => {
cy.visit('http: //localhost:4200/');
cy.get('input').first().type('5');
cy.get('select').select('*').should('have.value', '*');
cy.get('input').last().type('-3');
cy.get('button').click();
cy.get('p').should('have.text', '-15');
});

<st c="29281">it()</st> 函数用于定义一个单独的测试用例。 <st c="29338">第一个参数是一个字符串,描述了测试用例应该做什么。</st> 在这种情况下,它是 <st c="29438">'multiplies one positive number and one negative</st> <st c="29487">number correctly'</st>`。

<st c="29510">cy.visit()</st> 命令用于访问一个 URL。 <st c="29553"> 这里是通向我们的计算器用户界面的 URL。

以下行用于输入数字 <st c="29666">'5'</st> 在计算器的第一个输入字段中。 使用 <st c="29713">cy.get()</st> 函数从 DOM 中获取元素。 使用 <st c="29777">first()</st> 函数来获取对应元素集合中的第一个元素。 使用 <st c="29873">type()</st> 命令来在文本输入字段中输入。

然后,从我们的用户界面上方的下拉菜单中选择 <st c="29946">'*'</st> 运算符,并检查所选值确实是 <st c="30041">indeed</st> <st c="30049">'*'</st>。使用 <st c="30058">select()</st> 命令从下拉菜单中选择一个选项。 使用 <st c="30131">should()</st> 函数对应用程序的状态做出声明。

以下行用于输入数字 <st c="30259">'-3'</st> 在计算器的第一个输入字段中。 使用 <st c="30307">cy.get()</st> 函数来获取 DOM 元素。 使用 <st c="30361">last()</st> 函数来获取对应元素集合中的最后一个元素。 使用 <st c="30455">type()</st> 命令来在文本输入字段中输入。

然后,点击按钮执行 计算。 使用 <st c="30569">click()</st> 命令来模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="30694">'-15'</st>)是否正确显示。 再次使用 <st c="30732">should()</st> 函数来制作 这个断言。

在我们的浏览器中,我们有 以下结果:

图 8.12 – “正确地乘以一个正数和一个负数”的端到端测试成功

图 8.12 – “正确地乘以一个正数和一个负数”的端到端测试成功

在下一节中,我们将查看除法的上下文。

除法上下文

在本节中,我们将查看与除法相关的各种端到端测试用例。

将一个正非零数除以另一个正非零数

在这个测试用例中,我们将看到如何编写 端到端测试来除以另一个正非零数: 非零数:

 it('divides a positive non-zero number by another positive non-zero number', () => {
      cy.visit('http: //localhost:4200/');
      cy.get('input').first().type('5');
      cy.get('select').select('/').should('have.value', '/');
      cy.get('input').last().type('2');
      cy.get('button').click();
      cy.get('p').should('have.text', '2.5');
    });

The <st c="31827">it()</st> 函数用于定义一个单独的测试用例。 第一个参数是一个字符串,描述了测试用例应该做什么。 在这种情况下,它是 <st c="31984">'divides a positive non-zero number by another positive</st> <st c="32040">non-zero number'</st>

The <st c="32062">cy.visit()</st> 命令用于访问一个 URL。 在这里,这是指向我们计算器用户界面的 URL。 用户界面。

以下行用于在计算器的第一个输入字段中输入数字 <st c="32218">'5'</st> The <st c="32265">cy.get()</st> 函数用于从 DOM 中获取元素。 The <st c="32328">first()</st> 函数用于获取对应元素集合中的第一个元素。 The <st c="32423">type()</st> 命令用于在文本输入字段中输入。

然后,从我们用户界面的下拉菜单中选择 <st c="32496">'/'</st> 运算符,并检查所选值确实是 <st c="32599">'/'</st>。 The <st c="32608">select()</st> 命令用于从下拉菜单中选择一个选项。 The <st c="32680">should()</st> 函数用于对应用程序的状态做出声明。

以下行用于在计算器的第一个输入字段中输入数字 <st c="32808">'2'</st> The <st c="32855">cy.get()</st> 函数用于获取 DOM 元素。 The <st c="32909">last()</st> 函数用于获取对应元素集合中的最后一个元素。 The <st c="33002">type()</st> 命令用于在文本输入字段中输入。

然后,点击按钮进行计算。 The <st c="33116">click()</st> 命令用于模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="33241">'2.5'</st>)是否正确显示。 The <st c="33278">should()</st> 函数再次用于创建 这个断言。

<st c="33333">在我们的浏览器中,我们有</st> <st c="33362">以下结果:</st>

图 8.13 – “将一个正非零数除以另一个正非零数”的端到端测试成功

<st c="33617">图 8.13 – “将一个正非零数除以另一个正非零数”的端到端测试成功</st>

<st c="33617">在下一节中,我们将查看将非零负数除以非零正数的测试用例。</st>

<st c="33846">将一个负非零数除以一个正非零数</st>

<st c="33911">在这个测试用例中,我们将看到如何</st> <st c="33946">编写将一个负非零数除以另一个正非零数的端到端测试:</st>

 it('divides a negative non-zero number by another positive non-zero number', () => {
      cy.visit('http: //localhost:4200/');
      cy.get('input').first().type('-5');
      cy.get('select').select('/').should('have.value', '/');
      cy.get('input').last().type('2');    cy.get('button').click();
      cy.get('p').should('have.text', '-2.5');
    });

<st c="33726">The</st> <st c="33731">it()</st> <st c="33735">函数用于定义一个单独的测试用例。</st> <st c="33806">第一个参数是一个字符串,描述了测试用例应该做什么。</st> <st c="33878">在这种情况下,它是</st> <st c="33898">'divides a negative non-zero number by another positive'</st> <st c="33964">non-zero number'</st> <st c="33980">。</st>

<st c="34596">The</st> <st c="34601">cy.visit()</st> <st c="34611">命令用于访问一个 URL。</st> <st c="34644">在这里,它是通向我们的计算器用户界面的 URL。</st> <st c="34694">用户界面。</st>

<st c="34709">以下行用于在计算器的第一个输入字段中输入数字</st> <st c="34757">'-5'</st> <st c="34761">。</st> <st c="34801">The</st> <st c="34805">cy.get()</st> <st c="34813">函数用于从 DOM 获取元素。</st> <st c="34864">The</st> <st c="34868">first()</st> <st c="34875">函数用于获取相应元素集合中的第一个元素。</st> <st c="34958">The</st> <st c="34962">type()</st> <st c="34968">命令用于在文本输入字段中输入。</st>

<st c="35018">然后,从我们的用户界面的下拉菜单中选择</st> <st c="35034">'/'</st> <st c="35037">运算符,并检查所选值确实是</st> <st c="35127">'/'</st> <st c="35130">。</st> <st c="35134">The</st> <st c="35140">select()</st> <st c="35148">命令用于从下拉菜单中选择一个选项。</st> <st c="35208">The</st> <st c="35212">should()</st> <st c="35220">函数用于对应用程序的状态做出声明。</st>

<st c="35286">以下行用于在计算器的第一个输入字段中输入数字</st> <st c="35334">'2'</st> <st c="35337">。</st> <st c="35377">The</st> <st c="35381">cy.get()</st> <st c="35389">函数用于获取 DOM 元素。</st> <st c="35431">The</st> <st c="35435">last()</st> <st c="35443">函数用于获取相应元素集合中的最后一个元素。</st> <st c="35524">The</st> <st c="35528">type()</st> <st c="35534">命令用于在文本输入字段中输入。</st>

然后,点击按钮执行计算。 <st c="35652"> <st c="35656">click()</st> 命令用于模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="35781">'-2.5'</st>)是否正确显示 然后 <st c="35819">should()</st> 函数再次被用来进行这个断言。

在我们的浏览器中,我们得到以下结果:

图 8.14 – “将一个负的非零数除以另一个正的非零数”的端到端测试成功

图 8.14 – “将一个负的非零数除以另一个正的非零数”的端到端测试成功

在下一节中,我们将查看将非零负数除以另一个非零负数的测试用例。

将一个负的非零数除以另一个负的非零数

在这个测试用例中,我们将看到如何编写一个端到端测试来将一个负的非零数除以另一个负的非零数:

 it('divides a negative non-zero number by another negative non-zero number', () => {
      cy.visit('http: //localhost:4200/');
      cy.get('input').first().type('-5');
      cy.get('select').select('/').should('have.value', '/');
      cy.get('input').last().type('-2');
      cy.get('button').click();
      cy.get('p').should('have.text', '2.5');
    });

<st c="36879">it()</st> 函数用于定义一个单独的测试用例。 <st c="36936">第一个参数是一个字符串,描述了测试用例应该做什么。</st> 在这种情况下,它是<st c="37036">'divides a negative non-zero number by another negative</st> <st c="37092">non-zero number'</st>

<st c="37114">cy.visit()</st> 命令用于访问一个 URL。 在这里,这是指向我们计算器用户界面的 URL。

以下行用于在计算器的第一个输入字段中输入数字<st c="37270">'-5'</st> <st c="37318">cy.get()</st> 函数用于从 DOM 中获取元素。 <st c="37381">first()</st> 函数用于获取相应元素集合的第一个元素。 <st c="37476">type()</st> 命令用于在文本输入字段中输入。

然后,从用户界面的下拉菜单中选择<st c="37549">'/'</st> 运算符,并检查所选的值确实是<st c="37652">'/'</st> <st c="37661">select()</st> 命令用于从下拉菜单中选择一个选项。 <st c="37733">should()</st> 函数用于对应用程序的状态做出声明。

以下行用于在计算器的第一个输入字段中输入数字 <st c="37861">'-2'</st> <st c="37909">cy.get()</st> 函数用于获取 DOM 元素。 <st c="37963">last()</st> 函数用于获取相应元素集合中的最后一个元素。 <st c="38052">type()` 命令用于在文本输入字段中输入。

然后,点击 按钮进行计算。 点击 <st c="38170">click()</st> 命令用于模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="38295">'2.5'</st>)是否正确显示。 <st c="38332">should()</st> 函数再次用于进行这个断言。

在我们的浏览器中,我们有 这个结果:

图 8.15 – “将一个负非零数除以另一个负非零数”的端到端测试成功

图 8.15 – “将一个负非零数除以另一个负非零数”的端到端测试成功

在下一节中,我们将查看将非零正数除以非零负数的测试用例。

将一个正非零数除以一个负非零数

在这个测试用例中,我们将看到如何编写将正非零数除以另一个负非零数的端到端测试:

 it('divides a positive non-zero number by another negative non-zero number', () => {
      cy.visit('http: //localhost:4200/');
      cy.get('input').first().type('5');
      cy.get('select').select('/').should('have.value', '/');
      cy.get('input').last().type('-2');
      cy.get('button').click();
      cy.get('p').should('have.text', '-2.5');
    });

<st c="39489">it()</st> 函数用于定义一个单独的测试用例。 第一个参数是一个字符串,描述了测试用例应该做什么。 在这种情况下,它是 <st c="39646">'将一个正非零数除以另一个负非零数'</st> `

<st c="39724">cy.visit()</st> 命令用于访问 URL。 在这里,这是通向我们的计算器用户界面的 URL。

以下行用于在计算器的第一个输入字段中输入数字 <st c="39880">'5'</st> 然后 <st c="39927">cy.get()</st> 函数用于从 DOM 中获取元素。 然后 <st c="39990">first()</st> 函数用于获取相应元素集合的第一个元素。 然后 <st c="40085">type()</st> 命令用于在文本输入字段中输入。

然后,从用户界面的下拉菜单 中选择 <st c="40158">'/'</st> 运算符,并检查所选值确实是 <st c="40261">'/'</st>。然后 <st c="40270">select()</st> 命令用于从下拉菜单中选择一个选项。 然后 <st c="40342">should()</st> 函数用于对应用程序的状态做出声明。

以下行用于在计算器的第一个输入字段中输入数字 <st c="40470">'-2'</st> 然后 <st c="40518">cy.get()</st> 函数用于获取 DOM 元素。 然后 <st c="40572">last()</st> 函数用于获取相应元素集合的最后一个元素。 然后 <st c="40665">type()</st> 命令用于在文本输入字段中输入。

然后,点击按钮进行计算。 然后 <st c="40779">click()</st> 命令用于模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="40904">'-2.5'</st>)是否正确显示。 然后 <st c="40942">should()</st> 函数再次用于进行此断言。

在我们的浏览器中,我们得到以下结果:

图 8.16 – “将一个正非零数除以另一个负非零数”的端到端测试成功

图 8.16 – “将一个正非零数除以另一个负非零数”的端到端测试成功

在下一节中,我们将查看除以零的非零正数的测试用例。

将一个正非零数除以零

在这个测试用例中,我们将看到如何编写端到端测试来除以一个正非零数 并除以零:

 it('divides a positive non-zero number by zero', () => {
      cy.visit('http: //localhost:4200/');
      cy.get('input').first().type('5');
      cy.get('select').select('/').should('have.value', '/');
      cy.get('input').last().type('0');
      cy.get('button').click();
      cy.get('p').should('have.text', 'Infinity');
    });

The <st c="41986">it()</st> 函数用于定义一个单独的测试用例。 第一个参数是一个字符串,描述了测试用例应该做什么。 在这种情况下,它是 <st c="42143">'将一个正非零数除以零'</st> `

<st c="42193">cy.visit()</st> 命令用于访问一个 URL。 <st c="42236">这里,它是通向我们的计算器用户界面的 URL。

下一行用于在计算器的第一个输入字段中输入数字 <st c="42349">'5'</st> <st c="42392">cy.get()<st c="42404">函数用于从 DOM 中获取元素。</st><st c="42459">first()</st> 函数用于获取相应元素集合中的第一个元素。 <st c="42550">type()` 命令用于在文本输入字段中输入。

然后,从我们的用户界面上面的下拉菜单中选择 <st c="42627">'/'</st> 运算符,并检查所选值确实是 <st c="42730">'/'</st><st c="42739">select()</st> 命令用于从下拉菜单中选择一个选项。 <st c="42807">should()` 函数用于对应用程序的状态做出声明。

下一行用于在计算器的第一个输入字段中输入数字 <st c="42939">'0'</st> <st c="42986">cy.get()</st> 函数用于获取 DOM 元素。 <st c="43036">last()<st c="43046">函数用于获取相应元素集合中的最后一个元素。</st><st c="43133">type()</st> 命令用于在文本输入字段中输入。

然后,点击按钮执行计算。 <st c="43243">click()` 命令用于模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="43372">'Infinity'</st>)是否正确显示。 <st c="43410">should()<st c="43422">函数再次用于做出</st>这个断言。`

在我们的浏览器中,我们有 <st c="43497">这个结果:</st>

图 8.17 – “将一个正非零数除以零”端到端测试成功

图 8.17 – “将一个正非零数除以零”端到端测试成功

在下一节中,我们将查看除以非零负数除以零的测试用例。 除以零。

除以一个负的非零数除以零

在这个测试用例中,我们将看到如何编写端到端测试来除以一个负的非零数 除以零:

 it('divides a negative non-zero number by zero', () => {
      cy.visit('http: //localhost:4200/');
      cy.get('input').first().type('-5');
      cy.get('select').select('/').should('have.value', '/');
      cy.get('input').last().type('0');
      cy.get('button').click();
      cy.get('p').should('have.text', '-Infinity');
    });

The <st c="44346">it()</st> 函数用于定义一个单独的测试用例。 第一个参数是一个字符串,描述了测试用例应该做什么。 在这种情况下,它是 <st c="44503">'除以一个负的非零数</st> <st c="44539">除以零'</st>

The <st c="44553">cy.visit()</st> 命令用于访问一个 URL。 <st c="44596">Here, it’s the URL that leads to our calculator’s</st> <st c="44646">用户界面。</st>

以下行用于在计算器的第一个输入字段中输入数字 <st c="44709">'-5'</st> <st c="44753">The</st> <st c="44757">cy.get()</st> 函数用于从 DOM 中获取元素。 <st c="44816">The</st> <st c="44820">first()</st> 函数用于获取相应元素集合的第一个元素。 <st c="44911">The</st> <st c="44915">type()</st> 命令用于在文本 <st c="44958">输入字段</st> 中输入。

然后,从我们的用户界面上面的下拉菜单中选择“/” '/' 运算符,并检查所选的值确实是 <st c="45091">'/'</st><st c="45100">select()</st> 命令用于从下拉菜单中选择一个选项。 <st c="45168">The</st> <st c="45172">should()</st> 函数用于对应用程序的状态做出声明。

以下行用于在计算器的第一个输入字段中输入数字 <st c="45300">'0'</st> <st c="45343">The</st> <st c="45347">cy.get()</st> 函数用于获取 DOM 元素。 <st c="45397">The</st> <st c="45401">last()</st> 函数用于获取相应元素集合的最后一个元素。 <st c="45490">The</st> <st c="45494">type()</st> 命令用于在文本 <st c="45537">输入字段</st> 中输入。

然后,点击按钮执行计算。 <st c="45604">The</st> <st c="45608">click()</st> 命令用于模拟鼠标点击。

最后,代码的最后一行检查计算结果(<st c="45733">'-Infinity'</st>)是否正确显示。 这里 <st c="45776">should()</st> 函数再次被用来进行 这个断言。

在我们的浏览器中,我们有 这个结果:

图 8.18 - 负非零数除以零端到端测试成功

图 8.18 - 负非零数除以零端到端测试成功

在下一节中,我们将查看除以零的非零负数的测试用例。

零除以零

在这个测试用例中,我们将看到如何编写除以零的端到端测试:

 it('divide zero by zero', () => {
      cy.visit('http: //localhost:4200/');
      cy.get('input').first().type('0');
      cy.get('select').select('/').should('have.value', '/');
      cy.get('input').last().type('0');
      cy.get('button').click();
      cy.get('p').should('have.text', 'NaN');
});

这里使用 <st c="46745">it()</st> 函数来定义一个单独的测试用例。 第一个参数是一个字符串,描述了测试用例应该做什么。 在这种情况下,它是 <st c="46902">'divide zero</st> <st c="46915">by zero'</st>

这里使用 <st c="46929">cy.visit()</st> 命令来访问一个 URL。 在这里,这是通向我们的计算器用户界面的 URL。

以下行用于在计算器的第一个输入字段中输入数字 <st c="47085">'0'</st> 这里使用 <st c="47132">cy.get()</st> 函数从 DOM 中获取元素。 这里使用 <st c="47195">first()</st> 函数来获取相应元素集合的第一个元素。 这里使用 <st c="47290">type()</st> 命令在文本输入字段中输入。

然后,从我们的用户界面上方的下拉菜单中选择 <st c="47363">'/'</st> 运算符,并检查所选的值确实是 <st c="47466">'/'</st>。这里使用 <st c="47475">select()</st> 命令从下拉菜单中选择一个选项。 这里使用 <st c="47547">should()</st> 函数来对应用程序的状态做出声明。

<st c="47627">以下行用于在计算器的第一个输入字段中输入数字 <st c="47675">'0'</st> <st c="47678">。</st> <st c="47722">cy.get()</st> <st c="47730">函数用于获取 DOM 元素。</st> <st c="47776">last()</st> <st c="47782">函数用于获取相应元素集合中的最后一个元素。</st> <st c="47865">type()</st> <st c="47875">命令用于在文本输入字段中输入。</st>

然后,点击按钮执行计算。<st c="47983">click()</st> <st c="47990">命令用于模拟鼠标点击。</st>

最后,代码的最后一行检查计算结果(<st c="48108">'NaN'</st>)是否正确显示。<st c="48145">should()</st> <st c="48153">函数再次用于创建此断言。</st>

在我们的浏览器中,我们得到以下结果:

图 8.19 – “除以零”端到端测试成功

<st c="48357">图 8.19 – “除以零”端到端测试成功</st>

在下一节中,我们将探讨 Cypress 自定义命令是什么,以及如何使用它们使代码更容易维护和阅读。

<st c="48550">使用 Cypress 自定义命令</st>

Cypress 自定义命令 是用户定义的动作和断言,它们扩展了 Cypress 测试框架的功能。它们使测试人员能够封装重复性操作,简化自动化工作流程,并针对特定需求优化测试脚本。自定义命令可以添加或替换,提供了一种灵活的方式与 Web 应用程序交互,并提高测试脚本的效率和可读性。

要在 Cypress 中创建自定义命令,您使用<st c="49055">Cypress.Commands.add()</st> <st c="49077">方法。</st> 此方法允许您定义一个可以在整个测试套件中使用的新的命令。

例如,我们可以创建一个自定义命令来测试 <st c="49232">add()</st> <st c="49237">操作。</st> 为了实现这一点,我们需要在 <st c="49293">commands.ts</st> <st c="49304">文件中添加一些代码,该文件位于 support 文件夹中,该文件夹位于 <st c="49349">cypress</st> <st c="49356">文件夹内:</st>

 Cypress.Commands.add(
  'performCalculation',
  (firstNumber, operator, secondNumber) => {
    cy.visit('http: //localhost:4200/');
    cy.get('input').first().type(firstNumber);
    cy.get('select').select(operator).should('have.value', operator);
    cy.get('input').last().type(secondNumber);
    cy.get('button').click();
  }
);
declare namespace Cypress {
  interface Chainable<Subject = any> {
    performCalculation(
      firstNumber: string,
      operator: string,
      secondNumber: string
    ): Chainable<any>;
  }
}

<st c="49839">以下是代码的分解:</st> <st c="49862">方法。</st>

  • <st c="49871">Cypress.Commands.add('performCalculation', (firstNumber, operator, secondNumber) => {…})</st>: 这定义了一个新的自定义命令 名为 <st c="50003">performCalculation</st>。此命令接受三个参数: <st c="50060">firstNumber</st>, <st c="50073">operator</st>, <st c="50087">secondNumb</st>er`

  • <st c="50102">cy.get('input').first().type(firstNumber)</st>: 这将选择页面上的第一个输入元素并输入 <st c="50211">firstNumb</st>er` 值。

  • <st c="50230">cy.get</st><st c="50237">('select').select(operator).should('have.value', operator)</st>: 这将选择一个下拉列表(或选择元素)并选择与操作符参数相对应的选项。 然后断言所选值对应于 操作符。

  • <st c="50485">cy.get('input').last().type(secondNumber)</st>: 这将选择页面上的最后一个输入元素并输入 <st c="50595">secondNum</st>ber` 值。

  • <st c="50615">cy.get('button').click()</st>: 这将点击一个按钮,可能用于执行 计算。

现在,在 我们的 <st c="50732">calculator.cy.ts</st> 文件级别,我们将执行以下操作来设置我们的端到端测试: 测试:

 it('adds two positive numbers correctly', () => {
  cy.performCalculation('5', '+', '3');
  cy.get('p').should('have.text', '8');
});

以下是代码的 分解:

  • <st c="50967">cy.performCalculation('5', '+', '3')</st>: 这是一个自定义命令,可能用于在测试的应用程序中执行计算操作。 它接受三个参数:第一个数字、操作符和第二个数字。 在这种情况下,它是将 5 和 3 相加。

  • <st c="51225">cy.get('p').should('have.text', '8')</st>: 一个断言,检查所选元素是否具有指定的文本。 在这里,它正在检查段落元素是否包含文本 <st c="51406">'8'</st>

现在,在我们的浏览器中,我们有 以下结果:

图 8.20 – 使用 Cypress.Command 成功执行了“正确添加两个正数”的端到端测试

图 8.20 – 使用 Cypress.Command 成功执行了“正确添加两个正数”的端到端测试

现在让我们总结一下 本章内容。

摘要

本章涵盖了使用 Cypress(一个流行的网络应用程序测试框架)构建、编写和改进端到端测试的基本方面。 它从解释结构化端到端测试的重要性开始,以确保它们是完整的、可维护的和易于理解的。 这包括使用 <st c="52071">describe</st> <st c="52084">it</st> 块将测试组织成逻辑组,这些块有助于根据测试的功能或特性进行分类。

在本章中,编写端到端测试用例是一个关键方面。 它侧重于使用 Cypress 命令和断言与网页元素交互并验证其行为。 本章提供了如何使用以下命令的示例 <st c="52420">cy.visit()</st>, <st c="52432">cy.get()</st>, <st c="52442">cy.contains()</st>, <st c="52457">cy.type()</st>, 和 <st c="52472">cy.should()</st> 来导航应用程序、与元素交互以及验证应用程序状态。 它强调了以这种方式结构化测试的重要性,即从请求到命令或断言的转换,确保测试是确定性和可靠的。

本章还探讨了使用 Cypress 自定义命令来提高端到端测试的可读性和可维护性的方法。 自定义命令允许开发者将重复的模式和操作封装在可重用的函数中,使测试更易于阅读和理解。 这是通过使用 <st c="53067">Cypress.Commands.add()</st>来定义自定义命令实现的,然后可以在测试中用它来代替标准 Cypress 命令。 本章提供了如何为诸如通过标签选择表单元素等操作创建自定义命令的示例,这简化了测试代码,并使其与真实用户执行的操作更一致。

总之,本章提供了使用 Cypress 结构化、编写和改进端到端测试的全面指南,强调了清晰测试组织、有效使用 Cypress 命令和断言以及创建自定义命令以提高测试可读性和可维护性的重要性。

在下一章中,我们将学习关于 持续集成 (CI) 和 持续 部署 (CD).

第四部分:Angular 应用程序的持续集成和持续部署

在本节中,你将首先了解 持续集成 (CI) 和 持续部署 (CD) 的基本原理。 接下来,你将深入探索 TDD 的最佳实践和模式。 最后,你将学习关于重构和 代码改进 的内容。

本部分包含以下章节:

  • 第九章理解持续集成和持续部署(CI/CD)

  • 第十章Angular TDD 的最佳实践和模式

  • 第十一章通过 TDD 重构和改进 Angular 代码

第十章:9

理解持续集成和持续部署(CI/CD)

在软件开发中,持续集成和部署 (CI/CD)实践与测试驱动开发 (TDD)的结合已被证明是快速交付高质量软件的强大组合。 持续集成 (CI)和 TDD 协同工作,以自动化软件开发周期,促进测试文化,并使团队能够迭代和 有信心地构建和部署代码。

CI/CD 涵盖了从开发者做出的代码更改到在生产中部署这些更改的一系列自动化步骤。 通过实施 CI/CD 实践,开发团队能够减少人为错误,提高团队成员之间的协作,加快交付速度,并提高整体 软件质量。

在本章中,我们将探讨 CI/CD 的基本概念,探讨采用这些实践的好处,讨论 CI/CD 管道的关键组件,并提供组织如何 成功实施 CI/CD 流程以简化其 软件开发生命周期 周期 (SDLC).

总之,本章将涵盖以下主要主题:

  • 理解持续集成和 持续部署

  • 使用 GitHub Actions 设置 CI/CD 管道以自动化构建

  • 使用 GitHub Actions 设置 CI/CD 管道以自动化测试

  • 使用 GitHub Actions 设置 CI/CD 管道以自动化部署流程

技术要求

为了跟随本章中的示例和练习,你需要对 Angular 和 TypeScript 有基本的了解,以及以下 技术要求:

  • 安装在你电脑上的 Node.js 和 npm

  • 全局安装 Angular CLI

  • 安装在你电脑上的代码编辑器,例如 Visual Studio Code

本章所需的代码文件可以在 GitHub 上找到 https://github.com/PacktPublishing/Mastering-Angular-Test-Driven-Development/tree/main/Chapter%209

理解 CI 和 CD

CI 和 CD 已成为现代软件开发中的基本 实践,使团队能够快速高效地交付高质量的代码。 本全面 指南涵盖了 CI/CD 的基本概念,探讨了它们的益处,讨论了最佳实践,并概述了成功的实施策略。 从理解基础知识到优化 CI/CD 管道,本章旨在为读者提供他们需要利用 CI/CD 的力量并改变 软件交付的知识和工具。

但在我们继续使用这些实践之前,让我们 了解它们。

什么是 CI?

CI 是一种 DevOps 软件开发实践,其中开发者定期将他们的代码更改合并到中央存储库中。 每次合并后,都会运行自动构建和测试,以确保新代码无错误并符合项目的质量标准。 这个过程对于快速识别和纠正错误、提高软件质量以及减少验证和发布新软件更新所需的时间至关重要。 CI 鼓励频繁的代码集成,通常每天几次,以便在开发周期早期识别集成问题并更容易地纠正。 它还鼓励文化上的转变,向更频繁的代码交付转变,这对于我们充分利用 CI 的自动化 和效率至关重要。

持续集成(CI)是 CI/CD 管道的第一阶段,是更广泛的软件开发 DevOps 方法的一部分。 它遵循敏捷软件开发方法,将工作分解成小而可管理的任务,这些任务可以频繁完成和集成。 使用 CI 工具,如 GitHub Actions、Jenkins、Buildbot、Go、Travis CI 和 GitLab CI,简化了构建和测试过程的自动化,使开发者更容易将他们的更改与项目其他部分集成,并在开发早期阶段识别问题。 开发过程中。

持续集成的好处包括 提高开发者生产力、更快地交付更新以及更可预测的交付计划。 它还改善了跨团队协作和系统集成,减少了测试错误,并提高了软件开发周期的效率。 然而,持续集成的主要挑战主要涉及团队采用和 CI 工具的初始技术安装。 克服这些挑战并有效地实施持续集成实践对于实现持续集成在改进软件开发过程 和结果中的全部潜力至关重要。

在下一节中,我们将更深入地探讨持续集成(CI)对 开发团队的好处。

持续集成(CI)对开发团队的好处

持续集成(CI)对开发团队的好处是多方面的,包括效率、质量和客户满意度。 以下是这些好处的详细概述:

  • 更快的迭代和问题解决:持续集成使团队能够更频繁地集成代码更改,加快迭代速度并促进问题解决。 小的代码更改更容易管理,从而减少了可能出现的复杂问题的复杂性。

  • 提高代码质量和减少错误:通过频繁集成和测试代码,持续集成能够在开发周期早期识别并纠正错误。 结果是代码质量更高,缺陷更少,从而改善了用户体验并 减少了停机时间。

  • 提高效率和降低成本:由持续集成驱动的自动化减少了手动任务,为开发者节省了时间。 这不仅提高了效率,还降低了与手动测试和错误管理相关的成本。 因此,工程师可以有更多时间投入到 增值活动中。

  • 提高透明度和协作:持续集成通过提供对代码质量和集成问题的持续 反馈来促进透明度。 它还通过确保代码更改定期集成和测试来促进更好的团队协作,从而在团队成员之间实现更好的协调。

  • 更快的上市时间:通过自动化构建、测试和部署流程,持续集成(CI)使团队能够更快地将新特性和更新交付给最终用户。 这种响应性使开发团队能够保持竞争力,并确保客户能够从 最新的增强功能中受益。

  • 提高客户满意度:更少的错误和缺陷最终进入生产,改善了用户体验。 持续集成还使团队能够快速响应客户反馈,使团队能够更高效地进行调整和改进

  • 缩短平均故障恢复时间(MTTR):持续集成使问题能够更快地被检测和解决,从而降低 MTTR。 这确保了软件的稳定性和可靠性, 最小化了停机时间。

  • 提高测试可靠性:在持续集成框架内进行持续测试,通过允许执行更精确的测试来提高测试可靠性。 这确保了软件经过彻底测试并准备好投入生产,提高了对 软件质量}的信心。

  • 竞争优势:采用 商业智能 (BI) 的组织具有竞争优势 ,因为它们可以更快地部署功能,从而节省资金。 这种早期反馈和自动化有助于缩短交货期、部署频率和变更失败率,进而提高 业务成果。

  • 团队内部提高透明度和问责制:CI/CD 实践提高了团队内部的透明度和问责制,使问题能够迅速识别和解决,包括施工失败和架构挫折。 这种持续反馈循环提高了整体 产品质量。

总之,持续集成在效率、质量和客户满意度方面为开发团队提供了显著的好处。 它简化了开发过程,降低了成本,并增强了 协作,最终导致能够更快、更可靠地交付高质量的软件产品。 在下一节中,我们将探讨持续集成实施的关键原则。

持续集成(CI)实施的关键原则

持续集成实施的关键原则旨在提高软件开发效率、质量和速度。 以下是 关键原则:

  • 自动化一切:持续集成关注自动化构建、测试和集成过程。 自动化减少了人工工作量,最小化了错误,并加速了 开发周期。

  • 频繁集成:频繁地将代码更改集成到共享仓库中,理想情况下每天几次。 这种做法可以使集成问题在开发周期的早期就被识别和解决。

  • 使构建过程快速:构建过程应该尽可能快,以确保快速反馈。 快速构建意味着问题可以更快地被发现和解决,从而促进 持续改进。

  • 即时反馈:持续集成依赖于自动化构建和测试的即时反馈。 这种反馈对于在开发过程的早期识别和解决问题至关重要。

  • 从小处着手,逐步发展:从简单的持续集成配置开始,根据需要逐步添加其他工具和实践。 这种方法鼓励灵活性和实验,使团队能够在其 特定环境中找到最佳方案。

  • 定义成功指标:明确定义持续集成过程的成功指标,例如加速代码构建或降低错误和工作率。 使用这些指标来衡量持续集成实践的有效性,并 指导改进。

  • 文档:记录持续集成过程和所有开发人员和利益相关者使用的工具。 良好的文档确保每个人都了解如何 为持续集成过程做出贡献并高效地解决 问题。

  • 运维和开发之间的协作:鼓励运维和开发紧密协作的文化。 这种协作对于从两个角度理解软件可靠性和性能至关重要。

  • 可扩展性:持续集成通过自动化代码集成和沟通,打破了增长障碍,使组织能够扩展其开发团队、代码库和 基础设施。

  • 学习曲线的投资:成功实施持续集成涉及在版本控制和自动化等领域学习新技能。 然而,这些技能很容易获得,持续集成的益处超过了 初始投资。

这些原则指导 CI 的实施,确保它成为软件开发过程的组成部分,提高生产力、质量和速度。 在下一节中,我们将学习什么是 持续部署 (CD)。

什么是 CD?

CD 是一种自动化软件 发布实践,其中代码更改在通过预定义测试的不同阶段自动部署。 CD 的目的是通过在部署过程中最小化人工干预来加速生产发布。 这种方法是更广泛的 DevOps 实践的一部分,该实践旨在通过将自动化应用于 SDLC 的每个阶段来加速创新和价值创造

软件设计需要在整个应用程序设计和开发过程中进行严格的测试、团队之间的紧密协作、先进工具和流程流程。 当成功实施时,CD 使组织能够快速响应客户请求并快速交付软件更新,通常在验证代码更改后的几分钟内。 此过程包括在单个 工作流程中自动化构建、测试和部署,目的是在生产中自动化软件部署

CD 的好处包括完全自动化的部署周期,使组织能够将更多时间花在软件创建上,而不是发布准备上。 它还导致更频繁的增量部署,促进更快的产品开发和持续改进模型。 此外,CD 为新功能、更新和代码更改提供快速反馈循环,使组织能够快速接收和整合 用户反馈。

CD 比 CI 更进一步,它自动化了从部署本身之前的所有事情,需要人工干预来设置部署。 CD 自动化了整个过程,包括软件本身的发布,如果管道设置得当并设计用于测试软件产品的所有元素,那么它就是 CD 的自然演变 在发布之前。

持续交付(CD)管道通过自动构建、测试和将代码更改直接部署到生产环境中,简化了软件交付过程。 它涉及在整个管道中进行自动化测试和监控,以检测潜在的错误、功能问题和缺陷,提供实时警报,防止问题到达主软件分支或生产环境。 这种方法强调了 DevOps 的主要目标:为最终用户提供价值。 最终用户。

在实践中,这意味着开发者对云应用程序所做的更改,只要通过自动化测试,就可以在编写后几分钟内投入生产。 这使得接收和整合用户反馈变得更加容易。 然而,交付价值在很大程度上取决于精心设计的测试自动化,这可能需要大量的初始投资。 初始投资。

总的来说,持续交付(CD)是 DevOps 方法的一个基本方面,使组织能够快速、高效地发布软件更新,加速创新和价值创造。 加速创新和价值创造 为最终用户。

持续交付(CD)对开发团队的好处

持续交付(CD)为开发团队提供了几个关键好处,促进了更高效、敏捷和响应的软件开发过程。 以下是 主要好处:

  • 完全自动化的部署周期:持续交付(CD)使组织能够自动化整个 部署过程,减少人工干预,并允许开发团队更多地专注于编码,而不是发布准备。 这种自动化加快了新功能和更新的部署,使团队能够更快、更高效地交付软件。 更高效地。

  • 更频繁、增量部署:通过自动化部署,持续交付(CD)使得小规模的增量变更能够更频繁地发布。 这种方法能够加速产品开发,并促进持续改进模型,在该模型中,团队可以根据用户反馈和市场需求快速迭代他们的软件。 市场需求。

  • 对新功能快速反馈循环:持续交付(CD)为新功能、更新和代码更改提供实时反馈。 这种即时反馈循环对于使团队能够快速适应和改进他们的软件至关重要,确保最终产品符合用户的期望和要求。 和需求。

  • 事件响应:持续交付(CD)使团队能够快速响应生产中的系统错误、安全事件或开发中可能开发的新功能。 将代码立即发布到生产环境使组织能够更快地解决和解决问题,MTTR 等指标使响应时间得以评估和随着时间的推移而改进 over time.

  • 简化发布周期,加快上市时间:通过自动化部署过程,持续交付(CD)使软件开发团队能够快速将新特性和错误修复提供给最终用户。 这种自动化减少了人为错误的风险,并允许快速部署小型、频繁的更新,从而加快上市时间,为公司提供 竞争优势。

  • 通过自动化测试实现问题的早期发现:持续交付(CD)强调在整个软件开发过程中自动化测试的重要性。 通过进行持续测试,开发者可以快速识别和解决任何潜在问题,从而保证软件的稳定性和可靠性。 这种早期发现有助于减少生产中成本高昂的错误的可能性,并增强开发团队的信心。 of the software. This early detection helps reduce the likelihood of costly errors in production and instills confidence in the development team.

  • 持续反馈循环以实现持续改进:持续交付(CD)通过在开发者和最终用户之间建立反馈循环,培养了一种持续改进的文化。 这个迭代过程使组织能够适应和响应不断变化的需求,确保其软件保持相关性和竞争力。 and competitive.

  • 改进协作和沟通:持续交付(CD)促进团队成员之间的协作和沟通,提高了开发过程的整体效率。 通过自动化部署管道,开发者可以专注于他们的核心任务,促进不同团队之间的无缝集成,从而实现更快、更高效的 软件发布。

简而言之,持续交付(CD)为开发团队提供了快速交付软件的能力,通过自动化测试确保高质量,并保持响应和敏捷的开发过程。 这些好处共同促进了一个更高效、创新和以客户为中心的软件开发周期。 development cycle.

持续交付(CD)实施的关键原则

总的来说,持续交付(CD)是 DevOps 方法的一个基本方面,使组织能够快速高效地发布软件更新,加速创新和价值创造,为最终用户提供更多价值。 持续交付(CD)实施的关键原则对于创建一个简化和自动化的软件发布流程至关重要。 这些原则源于敏捷和最佳组织实践的组合,旨在尽可能快地将软件交付给最终用户,从他们的经验中学习,并将他们的反馈纳入下一次发布。 以下是基本原则

  • 构建质量:这一原则强调从一开始就将质量构建到产品中,而不是依赖于检查来实现。 它涉及创建和演变反馈循环,以便在问题被记录在版本控制系统之前尽早发现。 应使用自动化测试在问题恶化之前检测缺陷。 自动化测试应在问题恶化之前检测缺陷。 随着时间的推移,应使用自动化测试来检测缺陷。

  • 小批量工作:持续交付(CD)鼓励使用小而可管理的变更,而不是大而稀少的发布。 这种方法减少了获取反馈所需的时间,促进了问题的识别和解决,并提高了效率和动力。 目标是改变软件交付的经济性,使其能够在小批量中工作。

  • 计算机执行重复性任务,人类解决问题:这一原则强调了自动化重复性任务,如回归测试的重要性,以便人类可以专注于解决问题。 目标是创造一种平衡,其中计算机处理简单、重复的任务,而人类处理更复杂、更具创造性的任务。

  • 持续改进:持续交付(CD)推崇持续改进的理念,或称为精益运动**中的kaizen**,。 这关乎将改进工作视为日常工作的重要组成部分,并不断努力使事物变得更好。 这关乎不满足于现状,并始终寻找改进的机会。

  • 人人有责:在成功的组织中,每个人都对其构建的软件的质量和稳定性负责。 这一原则鼓励一种协作方法,其中开发人员、运营团队和其他利益相关者共同努力实现组织的目标,而不是优化他们自己团队的成功。 它强调了基于客户反馈和组织影响的快速反馈循环的重要性。

实施这些原则需要在组织内部进行文化变革,培养一个鼓励每个人确保最终用户获得的产品是最高 可能质量的协作环境。 这意味着在早期处理过程中解决繁琐或易出错的任务,以避免加剧问题并优化资源的使用。 在下一节中,我们将学习如何使用 GitHub Actions 设置 CI/CD 管道以自动化构建。

使用 GitHub Actions 设置 CI/CD 管道以自动化构建

使用 GitHub Actions 设置 CI/CD 管道涉及几个步骤,每个步骤对于自动化构建过程都至关重要。 以下是一个逐步指南,帮助您开始。

步骤 1 – 创建或选择一个仓库和项目

首先,在 您希望设置 CI/CD 管道的 仓库中选择一个。这可以是一个现有项目,或者您正在工作的一个新项目。 在我们的例子中,它将是 GitHub 上的这个仓库: https://github.com/PacktPublishing/Mastering-Angular-Test-Driven-Development/tree/main

图 9.1 – 计算器仓库项目

图 9.1 – 计算器仓库项目

步骤 2 – 在您的项目仓库中打开 GitHub Actions

现在,转到您仓库顶部导航栏中的 GitHub Actions 选项卡。 在这里,您将找到各种针对您项目技术栈定制的 CI/CD 自动化模板和工作流程。 GitHub Actions 提供了广泛的预定义工作流程,并允许您从头开始创建自己的。 GitHub Actions 提供了广泛的预定义工作流程,并允许您从头开始创建自己的。

图 9.2 – GitHub Actions 管道模板

图 9.2 – GitHub Actions 管道模板

步骤 3 – 定义您的 CI/CD 工作流程

我们的项目是一个 Angular 项目,因此它运行在 Node.js 上。 因此,我们将选择 GitHub Actions 模板 专门针对 Node.js,我们将根据需要对其进行修改,以满足我们的需求。 您需要在 GitHub Actions 模板搜索栏中使用 <st c="22144">node</st> 关键字进行搜索,并通过 持续集成 **类别进行筛选:

图 9.3 – GitHub Actions 管道模板

图 9.3 – GitHub Actions 管道模板

如图 图 9**.4所示,Node.js 在列表中:

图 9.4 – GitHub Actions 中的 Node.js

图 9.4 – GitHub Actions 中的 Node.js

现在,我们可以点击 配置 按钮 ,然后将被重定向到如图 图 9**.5所示的界面:

图 9.5 – Node.js 基本管道模板

图 9.5 – Node.js 基本管道模板

现在,我们可以开始修改了。 首先,我们将把文件顶部的名称改为 <st c="23765">angular-tdd.yml</st>,如图 图 9**.6所示:

图 9.6 – 工作流程名称

图 9.6 – 工作流程名称

接下来,我们可以修改文件开头的name 值。 而不是 <st c="24004">Node.js CI</st>,我们将称之为 <st c="24030">Angular</st> <st c="24038">TDD CI/CD</st>

图 9.7 – 管道名称

图 9.7 – 管道名称

接下来,我们可以将 <st c="24127">–version: [14.x, 16.x, 18.x]</st> 数组节点更改为 <st c="24170">node-version: [18.x]</st>

图 9.8 – 管道 Node.js 版本

图 9.8 – 管道 Node.js 版本

最后,我们将删除文件的最后一行(即 <st c="24395">- run: npm test</st>),因为我们目前没有任何测试。 这是文件的最终内容:

 # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https: //docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Angular TDD CI/CD
on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18.x]
        # See supported Node.js release schedule at https: //nodejs.org/en/about/releases/
    steps:
    - uses: actions/checkout@v3
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
        cache-dependency-path: '**/package-lock.json'
    - run: npm ci
    - run: npm run build --if-present

现在,我们可以通过点击 提交 更改 按钮来保存文件。

此工作流程旨在自动化安装 Node.js 依赖项的过程,缓存它们以加快未来的构建,并构建 Angular 项目。 以下是工作流程关键组件 及其功能的分解:

  • <st c="25663">构建</st>。此作业在 GitHub Actions 提供的最新 Ubuntu 虚拟机上运行。

  • <st c="25773">默认值</st> 部分将构建作业的所有阶段的当前工作目录设置为 <st c="25852">./</st>``<st c="25854">第九章</st>``<st c="25864">/getting-started-angular-tdd/</st>。这确保了命令在您的 Angular 项目 所在的正确位置执行。

  • <st c="26020">策略</st> 部分定义了一个矩阵,该矩阵多次运行作业,每次使用不同的 Node.js 版本。 在此示例中,矩阵仅包含一个版本: <st c="26187">18.x</st>。您可以扩展它以包含更多版本,以进行更广泛的 兼容性测试。

  • <st c="26300">uses: actions/checkout@v3</st>) 使用官方的 GitHub Actions <st c="26362">checkout</st> 操作将存储库代码克隆到 运行器上。

  • <st c="26460">uses: actions/setup-node@v3</st>) 使用官方的 GitHub Actions <st c="26524">setup-node</st> 操作来安装和配置指定的 Node.js 版本 (<st c="26598">18.x</st>) 在 运行器上。

  • 缓存 <st c="26625">参数</st> 设置为 <st c="26651">npm</st> 以启用工作流程运行之间 Node.js 模块的缓存,可能加快后续执行。 <st c="26762">cache-dependency-path</st> 设置为 <st c="26794">**/package-lock.json</st> 以确保当 <st c="26857">package-lock.json</st> 文件更改(表示依赖项发生变化)时,缓存失效。

  • <st c="26966">run: npm ci</st>) 执行 <st c="26990">npm ci</st> 命令,从 <st c="27052">package-lock.json</st> 文件中安装项目的依赖项。 这确保了在不同环境中保持依赖项状态的一致性。

  • <st c="27191">run: npm run build --if-present</st>) 条件性地运行 <st c="27249">npm run build</st> 命令,如果它在项目的 <st c="27301">package.json</st> 文件中存在。 这为不同项目设置提供了灵活性,并不是所有项目都定义了 <st c="27413">build</st> 脚本。

然而,需要注意的是 如果你从课程仓库克隆项目,必须在 <st c="27564">runs-on: ubuntu-latest</st>: 之后添加以下内容:

 defaults:
      run:
        working-directory: "./Chapter 9/getting-started-angular-tdd"

最终渲染是通过从 书籍仓库 克隆项目获得的:

 # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https: //docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Angular TDD CI/CD
on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
jobs:
  build:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: "./Chapter 9/getting-started-angular-tdd"
    strategy:
      matrix:
        node-version: [18.x]
        # See supported Node.js release schedule at https: //nodejs.org/en/about/releases/
    steps:
    - uses: actions/checkout@v3
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
   cache-dependency-path: '**/package-lock.json'
    - run: npm ci
    - run: npm run build --if-present

最后,如果过程顺利,这是你在 GitHub Actions 界面中会得到的结果:

图 9.9 – 管道成功完成

图 9.9 – 管道成功完成

在下一节中,我们将学习 如何使用 GitHub Actions 设置 CI/CD 管道来自动化测试。

使用 GitHub Actions 设置 CI/CD 管道来自动化测试

在这个新部分,我们将更新我们之前用于运行测试的工作流程。 逻辑上讲,测试应该在构建之前运行。 以下是测试工作流程:

 test:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: './Chapter 9/getting-started-angular-tdd/'
    strategy:
      matrix:
        node-version: [18.x]
        # See supported Node.js release schedule at https: //nodejs.org/en/about/releases/
    steps:
    - uses: actions/checkout@v3
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
        cache-dependency-path: '**/package-lock.json'
    - run: npm ci
    - run: npm run test

现在,让我们将两个 工作流程,测试和构建,结合起来;以下是基本术语中的样子:

 name: Angular TDD CI/CD
on:
push:
    branches: [ "main" ]
pull_request:
    branches: [ "main" ]
jobs:
test:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: './Chapter 9/getting-started-angular-tdd/'
    strategy:
      matrix:
        node-version: [18.x]
        # See supported Node.js release schedule at https: //nodejs.org/en/about/releases/
    steps:
    - uses: actions/checkout@v3
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
        cache-dependency-path: '**/package-lock.json'
    - run: npm ci
    - run: npm run test
build:
    needs: test
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: './Chapter 9/getting-started-angular-tdd/'
    strategy:
      matrix:
        node-version: [18.x]
        # See supported Node.js release schedule at https : //nodejs.org/en/about/releases/
    steps:
    - uses: actions/checkout@v3
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
        cache-dependency-path: '**/package-lock.json'
    - run: npm ci
    - run: npm run build --if-present

如前述代码 块中所述,测试工作流程在构建工作流程之前。 然而,有一个方面立即引起了注意。 那就是测试和构建中许多序列的重复。 基于 <st c="31191">test-and-build</st>原则,例如。 以下是它的样子:

 # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https: //docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Angular TDD CI/CD
on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
jobs:
  test-and-build:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: './Chapter 9/getting-started-angular-tdd/'
    strategy:
      matrix:
        node-version: [18.x]
        # See supported Node.js release schedule at https: //nodejs.org/en/about/releases/
    steps:
    - uses: actions/checkout@v3
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
        cache-dependency-path: '**/package-lock.json'
    - run: npm ci
    - run: npm run test --if-present
    - run: npm run build --if-present

现在,当我们运行管道时,我们 注意到 <st c="32199">npm run test --if-present</st> 任务在一个类似这样的 块中运行:

图 9.10 – 管道运行中

图 9.10 – 管道运行中

实际上,我们遇到这个问题是很正常的,因为 <st c="32498">npm run</st> test 执行了 <st c="32524">ng</st> test。 由于我们处于 Angular 项目中,它试图在管道中启动 Chrome。 不幸的是,它找不到它 因为我们没有 图形用户界面 (GUI). 因此,我们得到了 以下错误:

图 9.11 – 管道失败

图 9.11 – 管道失败

为了解决这个问题,我们将对项目进行一些修改,特别是对 <st c="34629">angular.json</st> 文件,通过添加一个测试配置的配置 如下:

 "configurations": {
          "ci": {
              "watch": false,
              "progress": false,
              "browsers": "ChromeHeadlessCI"
        }
}

这是完整的 测试配置:

 "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "polyfills": [
              "zone.js",
              "zone.js/testing"
            ],
            "tsConfig": "tsconfig.spec.json",
            "inlineStyleLanguage": "scss",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.scss"
            ],
            "scripts": []
          },
          "configurations": {
            "ci": {
              "watch": false,
              "progress": false,
              "browsers": "ChromeHeadlessCI"
            }
          }
        }

之后,我们需要在我们的 Angular 项目的 <st c="35290">src</st> 文件夹中创建一个 <st c="35264">karma.conf.js</st> 文件,如果该文件 尚未存在。 在这个文件中,我们将放置与 Karma 的配置 相关的源代码:

 // Karma configuration file, see link for more information
// https: //karma-runner.github.io/1.0/config/configuration-file.html
process.env.CHROME_BIN = require("puppeteer").executablePath();
module.exports = function (config) {
  config.set({
    basePath: "",
    frameworks: ["jasmine", "@angular-devkit/build-angular"],
    plugins: [
      require("karma-jasmine"),
      require("karma-chrome-launcher"),
      require("karma-jasmine-html-reporter"),
      require("karma-coverage-istanbul-reporter"),
      require("@angular-devkit/build-angular/plugins/karma"),
    ],
    client: {
      clearContext: false, // leave Jasmine Spec Runner output visible in browser
    },
    coverageIstanbulReporter: {
      dir: require("path").join(__dirname, "../coverage"),
      reports: ["html", "lcovonly"],
      fixWebpackSourcePaths: true,
    },
    reporters: ["progress", "kjhtml"],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ["Chrome"],
    customLaunchers: {
      ChromeHeadlessCI: {
        base: "ChromeHeadless",
        flags: ["--no-sandbox", "--disable-gpu"],
      },
    },
    singleRun: false,
  });
};

接下来,我们需要在 开发模式下安装两个包,即 <st c="36518">puppeteer</st> <st c="36532">karma-coverage-istanbul-reporter</st>,通过以下操作:

 $ npm i --save-dev puppeteer karma-coverage-istanbul-reporter

最后,在我们的 GitHub Actions 管道中,我们将 <st c="36704">npm run test –if-present</st> 替换为 <st c="36734">npm run test -- --configuration=ci</st>,以下是 结果:

图 9.12 – 管道成功完成

图 9.12 – 管道成功完成

做得好! 在下面的内容中,你将 找到迄今为止所有更改的流程总结,以及相关的 源代码:

 # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https: //docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Angular TDD CI/CD
on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
jobs:
  test-and-build:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: './Chapter 9/getting-started-angular-tdd/'
    strategy:
      matrix:
        node-version: [18.x]
        # See supported Node.js release schedule at https: //nodejs.org/en/about/releases/
    steps:
    - uses: actions/checkout@v3
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
        cache-dependency-path: '**/package-lock.json'
    - run: npm ci
    - run: npm run test -- --configuration=ci
    - run: npm run build --if-present

在下一节中,我们将学习 如何使用 GitHub Actions 设置 CI/CD 管道来自动化部署流程。

使用 GitHub Actions 自动部署流程的 CI/CD 管道设置

CD 是一种实践,即在更改通过生产管道后自动部署到生产环境。 这包括测试、构建和部署的自动化流程。 GitHub Actions 支持 CD,使您能够高效地自动化这些 流程。

我们不会在这个项目中这样做,但我们会看看我们如何能这样做。 首先,这将像其他两个一样是一个测试和构建阶段。 部署自然发生在开发结束时,因此工作流程也将适用。 在我们的当前工作流程结束时,构建之后,我们将添加以下内容以在远程服务器上部署: 远程服务器:

 - name: Upload build files to remote server
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.SI_HOST }}
          username: ${{ secrets.SI_USERNAME }}
          password: ${{ secrets.SI_PASSWORD }}
          port: ${{ secrets.SI_PORT }}
          source: "[SORUCE_FOLDER]"
          target: "[DESTINATION_TARGET_ON_YOUR_SERVER]"

提供的 GitHub Actions 工作流程片段旨在自动化将构建文件上传到远程服务器的过程。 这是 CI/CD 管道中部署应用程序的常见步骤。 以下是工作流程步骤的 分解:

  • <st c="39490">appleboy/scp-action@master</st>: 这指定了 此步骤使用 <st c="39559">scp-action</st> 动作来自 <st c="39586">appleboy</st> GitHub 仓库。 此动作旨在使用 <st c="39755">@master</st> 标签表示动作应使用存储库的 master 分支中的代码 存储库。

  • <st c="39854">主机</st>: 将上传文件到的远程服务器的地址。 此值从名为 <st c="39980">SI_HOST</st>. 的 GitHub 机密中检索。

  • <st c="39988">用户名</st>: 用于与远程服务器进行身份验证的用户名。 此值从名为 <st c="40107">SI_USERNAME</st>. 的 GitHub 机密中检索。

  • <st c="40119">密码</st>: 用于与远程服务器进行身份验证的密码。 此值从名为 <st c="40238">SI_PASSWORD</st>. 的 GitHub 机密中检索。

  • <st c="40250">端口</st>: 连接到远程服务器的端口号。 此值从名为 <st c="40362">SI_PORT</st>. 的 GitHub 机密中检索。

  • <st c="40370">源</st>: 将上传的文件路径。 在这种情况下,它被设置为上传源 构建目录中的所有文件。

  • <st c="40498">目标</st>: 文件将上传到的远程服务器上的目标路径。

对于其他面向云的平台,GitHub Actions 通过提供大多数这些平台的部署模板来简化任务。 这些平台。

摘要

总结来说,本章涵盖了 SDLC 中 CI 和 CD 实践的基本概念,强调了它们的重要性及益处。我们首先理解到 CI 是一个旨在自动化代码更改集成到共享存储库的过程,从而促进自动构建和测试,以便快速检测和纠正问题。</st c="41076">这种实践对于早期发现错误和缺陷至关重要,促进了更快的反馈循环,鼓励协作,并提高代码质量。</st c="41220">

然后,CD 被介绍为 CI 的扩展,专注于自动化部署过程,同时确保软件始终处于可发布状态。这种实践使得在不同环境中快速可靠地发布软件成为可能,降低了部署错误的风险,并缩短了上市时间。</st c="41532">

本章还探讨了使用 GitHub Actions 设置 CI/CD 管道的实际方面,GitHub Actions 是一个流行的流程自动化工具。它解释了如何自动化构建过程,包括安装依赖项、编译代码和运行测试,以及自动化部署过程。</st c="41837">

连续流程的关键概念和实践被考察,包括进行小而迭代的变更的重要性、采用主干开发、维护快速构建和测试阶段,以及将部署与生产发布解耦。这些实践对于建立高效、可靠的连续流程至关重要,这些流程可以加速开发周期、提高软件质量,并更快地向客户交付价值。</st c="42291">

此外,本章讨论了测试在 CI/CD 流程中的作用,强调了不同类型测试的重要性,如冒烟测试、单元测试、集成测试、系统测试和验收测试。这些测试对于保证软件质量和稳定性至关重要,它们可以快速提供代码库状态反馈,并帮助在开发过程中早期发现和纠正问题。</st c="42709">

在下一章中,我们将学习关于测试驱动开发的最佳实践和模式。

第十一章:10

Angular TDD 的最佳实践和模式

测试驱动开发 (TDD) 在 Angular 中是一种强调在实现代码之前编写测试的方法论。这种方法确保代码经过彻底测试并满足特定要求。 在快速变化的 frontend 开发领域,如 Angular 这样的框架使得构建复杂的企业级应用成为可能,良好的测试的重要性不容忽视。 TDD 产生高质量的生成代码和健壮的代码库,确保应用按预期工作并能经受时间的考验。 TDD 导致高质量的生成代码和健壮的代码库,确保应用按预期工作并能经受时间的考验。

Angular 开发者经常发现自己正在导航单元测试的复杂性,尤其是在处理组件、服务和管道时。 为 Angular 应用编写单元测试的过程涉及理解 Angular 单元测试的结构、测试模块的需求,以及使用 TDD 构建 Angular 服务的迭代过程。 这种迭代过程,即开发者交替编写测试和通过测试所需的最小代码量,是 TDD 的基石。 它允许以小、可验证的增量构建功能,确保每个功能部分都经过彻底测试。

为了在 Angular 中有效地实施 TDD,开发者需要意识到常见的陷阱,例如编写过于依赖实现细节的测试。 这可能导致脆弱的测试,每次重构时都会中断,即使外部行为保持一致。 通过关注期望的行为而不是具体的实现,开发者可以编写清晰、精确的期望,从而指导开发过程。 此外,编写测试和代码实现迭代的流程有助于以小、可验证的增量构建功能,从而形成一个全面的测试套件,阐述系统各部分的职责和预期行为。

本章将涵盖以下主题:

  • Angular 项目中 TDD 的最佳实践

  • 任何 Angular 项目中实现 TDD 的模式探索

  • 为您的 Angular 项目选择正确的 TDD 模式

Angular 项目中 TDD 的最佳实践

TDD 是一种优先考虑测试创建 而非实现编码的方法。 这种策略确保代码经过彻底测试并与定义的需求保持一致。 以下是 Angular 中实现 TDD 的一些推荐实践和模型

  • 在实现前编写测试:首先编写一个针对尚未存在的功能或功能的测试。 这个测试最初会失败,因为该功能尚未实现。 测试应关注期望的行为,而不是具体的实现,确保测试清晰 且简洁。

  • <st c="3020">TestBed.configureTestingModule</st> 用于创建一个用于测试的模拟模块,并且 <st c="3092">TestBed.inject</st> 用于初始化该模块中的服务。 这种配置确保服务与外部依赖隔离,从而 实现精确测试。

  • 在测试中避免实现细节:测试应从最终用户或 API 消费者的角度验证功能的行为,而不假设对功能内部运作的了解。 这种方法有助于创建对实现变化具有弹性的测试。

  • 在编写测试和实现之间迭代:在编写测试和通过测试所需的最小代码量之间切换。 这种迭代过程有助于以小、可验证的增量构建功能,确保每个功能部分都 经过彻底测试。

  • 使用模拟、间谍和存根:为确保测试不与外部依赖耦合,请使用模拟、间谍和存根。 模拟提供对依赖的控制性替代实现,间谍跟踪函数调用,存根提供预定的行为。 这些工具有助于将正在测试的组件或服务从 外部因素中隔离出来。

  • <st c="4357">RouterTestingModule</st> 用于模拟导航事件并验证组件是否按预期对路由变化做出反应。 对于组件之间的交互,使用 <st c="4584">TestBed</st>模拟组件通过输入和输出进行通信的场景。

  • 维护和重构测试:定期审查和重构测试,确保它们保持相关并反映应用程序的当前状态。 与应用程序代码同步重构测试,确保测试经历与生产代码相同的改进严谨性。 使用包括测试更新作为功能分支一部分的版本控制策略,以尽早捕捉破坏性 更改。

  • 优化测试性能:通过逻辑分组测试、在适用的情况下使用防抖和节流技术以及高效处理依赖项来优化单元测试的性能。 利用 Angular 的分层注入器在正确的级别提供服务模拟,减少测试之间的冗余。 定期审计测试套件,删除过时的测试,并重构那些可以合并 或简化的测试。

  • <st c="5585">beforeEach()</st> 块有效地设置每个测试所需的条件,而不会对其他测试产生副作用。 编写允许组件或服务根据应用程序要求扩展的测试,而无需不断 重写测试。

  • 持续改进:持续优化生产和测试代码库,确保您的测试与它们验证的功能一样易于维护和高效。 反思您的测试如何正确适应业务逻辑,以正确表示不断发展的应用程序,确保您的测试保持稳健和具有代表性。

总之,通过这些最佳实践,Angular 开发者 可以培养一种可持续的测试文化,这种文化能够适应变化,同时将质量放在首位。 在下一节中,我们将探讨在任意 Angular 项目中实施 TDD 的模式。

探索在任意 Angular 项目中实施 TDD 的模式

探索在 Angular 项目中实施 TDD 的模式 是确保您的应用程序健壮性和可靠性的关键步骤。 TDD 是一种方法论,它涉及在实现代码之前编写测试,确保代码经过彻底测试并满足指定要求。 这种方法不仅导致高质量的生成代码,而且培养了一种持续改进和适应变化的韧性文化。

TDD 模式是在 Angular 项目上下文中实施 TDD 的策略或方法。 这些模式可以根据项目的具体需求、应用程序的复杂性和团队对 TDD 实践的熟悉程度而有所不同。 一些最常见的 TDD 模式如下:

  • 单元测试:此模式侧重于测试 单独的组件或服务。 这是至关重要的,因为它确保您的应用程序的每个部分在独立于系统其他部分的情况下按预期工作。

  • 组件测试:Angular 应用程序是围绕组件构建的 ,使它们成为用户界面的重要组成部分。 在 Angular 项目中实施 TDD 时,从测试组件开始可以确保它们正确渲染,处理用户交互,并按预期更新 UI

  • 服务测试:Angular 中的服务封装 业务逻辑和数据操作功能。 为服务编写测试确保它们执行预期的功能,正确与外部资源交互,并优雅地处理 错误。

  • 集成测试:此模式涉及测试 应用程序不同部分之间的交互,例如组件和服务。 它有助于识别当应用程序的不同部分协同工作时可能出现的潜在问题。

  • 端到端 (E2E) 测试:此模式模拟用户 在实际场景中与应用程序的交互,测试从开始到结束的整个应用程序流程。 这是至关重要的,因为它确保应用程序从用户的角度来看表现如预期。

总之,探索在 Angular 项目中实现 TDD 的模式是一次构建高质量、可扩展和可维护应用程序的旅程。 通过采用 TDD 最佳实践和模式,开发者可以提高其开发过程的效率,提高代码质量,并提供卓越的用户体验。 随着软件开发领域的持续发展,TDD 在 Angular 项目中的应用成为培养卓越文化和持续改进基石的关键。

在下一节中,我们将学习如何为您的 Angular 项目选择 TDD 模式。

为您的 Angular 项目选择 TDD 模式

为您的 Angular 项目选择 TDD 模式对于确保 应用程序的可靠性、可维护性和可扩展性至关重要。 如前所述,TDD 是一种涉及在实现代码之前编写测试的方法,确保代码得到彻底测试并满足指定要求。 然而,TDD 模式的选择可以显著影响开发过程、测试策略和整体项目成果。 本节将帮助您了解可用的 TDD 模式以及为什么它们可能适合您的 Angular 项目。 为您的 Angular 项目选择 TDD 模式取决于 几个因素: 几个因素:

  • 项目复杂性:对于具有众多组件和服务的复杂应用程序,可能需要结合单元、集成和端到端测试模式。 这种方法确保了应用程序的每个部分在隔离状态下以及在整个应用程序的上下文中都得到了彻底的测试。 整个应用程序。

  • 团队专长:团队对 TDD 实践以及可用于 Angular 的特定测试工具和框架的熟悉程度会影响 TDD 模式的选择。 例如,Angular 提供了强大的测试工具和库,便于进行单元和集成测试。

  • 项目需求:您项目的具体需求,如性能、安全性和用户体验,也可以指导 TDD 模式的选择。 例如,端到端测试对于需要高度用户交互和真实世界测试的项目特别有用。

总之,为您的 Angular 项目选择 TDD 模式是一个战略决策,应基于项目的复杂性、团队的专长以及应用程序的具体要求。 您的 Angular 项目 选择 TDD 模式是一个战略决策,应基于项目的复杂性、团队的专长以及应用程序的具体要求。 应用程序。

摘要

在本章中,我们探讨了在 Angular 项目中有效实施 TDD 的最佳实践和模式。 TDD 鼓励在编写实际代码之前编写测试,确保功能清晰并降低回归风险。 的风险。

关键课程包括 TDD 周期及其红-绿-重构阶段,以及如 Arrange-Act-AssertAAA)结构等模型,用于组织测试和高效模拟依赖关系。通过遵循这些模式和最佳实践,例如编写专注的测试、持续重构、测试自动化以及在团队内部促进协作,开发者可以提高代码质量,简化开发过程,并交付更好的用户体验。采用 TDD 在 Angular 项目中不仅是一种技术,更是一种培养持续改进和软件开发卓越文化的思维方式。

在下一章中,我们将学习如何通过 TDD(测试驱动开发)来重构和改进 Angular 代码。

第十二章:11

通过 TDD 重构和改进 Angular 代码

使用测试驱动开发 (TDD)重构您的 Angular 代码是一种系统化和有效的提高代码质量的方法。 TDD 涉及在编写实际代码之前编写测试,从而确保代码满足所需的条件,并且是健壮的、可维护的和可靠的。 这种方法对于 Angular 应用程序尤其有益,因为它确保代码结构良好、效率高且易于维护。

在本节中,我们深入探讨采用“先测试”策略的重要性,探讨重构过程中的 TDD 优势,选择要创建的最佳测试,理解代码异味构成的内容,强调在 Angular 项目中解决代码异味的重要性,以及识别 Angular 应用程序中普遍存在的代码异味。

总结来说,以下是本章将涵盖的主要主题:

  • 通过 TDD 重构 Angular 代码

  • 在 Angular 应用程序中识别代码异味和改进区域

  • 迭代改进 – 用于持续代码增强的红色-绿色-重构周期

技术要求

要跟随本章的示例和练习,你需要对 Angular 和 TypeScript 有基本的了解,以及以下技术要求:

  • 在您的计算机上安装 Node.js 和 npm

  • 全局安装 Angular CLI

  • 在您的计算机上安装代码编辑器,例如 Visual Studio Code

本章所需代码文件可以在https://github.com/PacktPublishing/Mastering-Angular-Test-Driven-Development/tree/main/Chapter%2011找到。

使用 TDD 重构 Angular 代码

在复杂的 Angular 应用程序中重构现有 代码可能是一项令人紧张的任务。 你希望在不妨碍现有功能的情况下改进代码的结构和组织。 这就是测试驱动开发(TDD)发挥作用的地方,它提供了一种结构化的方法,以自信地导航重构。 在本节中,我们将看到测试优先方法的力量,TDD 在重构中的好处,以及如何选择正确的测试 来编写。

测试优先方法的力量

TDD 翻转了 传统的编码脚本。 与传统先编写代码再测试不同,TDD 强调在修改代码之前先编写测试。 这些测试本质上定义了你打算重构的代码的预期行为。 以下是它是如何 工作的:

  • 红色状态:你首先为想要重构的代码中的特定功能编写一个测试。这个初始测试很可能会失败,表明期望的行为尚未实现。这个 红色 状态作为一个 起点。

  • 绿色状态:以失败的测试为指南,你只需编写足够的代码来使测试通过。这种初始实现可能很基础,但重点是确保它准确反映了测试定义的预期行为。 现在,测试处于 绿色 状态,表示 成功实现。

  • 重构:这里是魔法发生的地方。有了通过测试的安全网,你现在可以重构代码,以提高其可读性、可维护性和效率。这可能包括以下内容: 以下内容:

    • 将长方法分解为更小、 定义良好的函数

    • 从大型组件中提取可重用组件或服务

    • 应用设计模式以实现更好的 代码组织

    • 简化逻辑以 增强清晰度

在整个 重构阶段,通过测试确保这些更改不会引入任何意外的副作用。 本质上,你是在不改变外部行为(由测试验证)的情况下改进代码的内部工作方式。 作为一个例子,让我们从我们的 <st c="3768">calculator.component.ts</st> 组件<st c="3809">第九章</st> 文件夹(https://github.com/PacktPublishing/Mastering-Angular-Test-Driven-Development/tree/main/Chapter%209/getting-started-angular-tdd)中的这一段代码开始:

 calculate(): void {
    if (this.calculatorForm.get('operator')?.value === '+') {
      this.add(
        this.calculatorForm.get('operand1')?.value,
        this.calculatorForm.get('operand2')?.value
      );
    }
    if (this.calculatorForm.get('operator')?.value === '-') {
      this.substract(
        this.calculatorForm.get('operand1')?.value,
        this.calculatorForm.get('operand2')?.value
      );
    }
    if (this.calculatorForm.get('operator')?.value === '*') {
      this.multiply(
        this.calculatorForm.get('operand1')?.value,
        this.calculatorForm.get('operand2')?.value
      );
    }
    if (this.calculatorForm.get('operator')?.value === '/') {
      this.divide(
        this.calculatorForm.get('operand1')?.value,
        this.calculatorForm.get('operand2')?.value
      );
    }
  }

以下是 对应于我们 <st c="4711">calculator.component.spec.ts</st> 文件中此函数的测试源代码:

 it('should be valid when all of the fields are filled in correctly', () => {
    calculator.calculatorForm.get('operand1')?.setValue(123);
    calculator.calculatorForm.get('operand2')?.setValue(456);
    calculator.calculatorForm.get('operator')?.setValue('+');
    expect(calculator.calculatorForm.valid).toBe(true);
  });
  it('should be invalid when one of the field is not filled in correctly', () => {
    calculator.calculatorForm.get('operand1')?.setValue(123);
    calculator.calculatorForm.get('operator')?.setValue('+');
    expect(calculator.calculatorForm.valid).toBe(false);
  });
  it('should add when the + operator is selected and the calculate button is clicked', () => {
    calculator.calculatorForm.get('operand1')?.setValue(2);
    calculator.calculatorForm.get('operand2')?.setValue(3);
    calculator.calculatorForm.get('operator')?.setValue('+');
    calculator.calculate();
    expect(calculator.result).toBe(5);
  });
  it('should subtract when the - operator is selected and the calculate button is clicked', () => {
    calculator.calculatorForm.get('operand1')?.setValue(2);
    calculator.calculatorForm.get('operand2')?.setValue(3);
    calculator.calculatorForm.get('operator')?.setValue('-');
    calculator.calculate();
    expect(calculator.result).toBe(-1);
  });
  it('should multiply when the * operator is selected and the calculate button is clicked', () => {
    calculator.calculatorForm.get('operand1')?.setValue(2);
    calculator.calculatorForm.get('operand2')?.setValue(3);
    calculator.calculatorForm.get('operator')?.setValue('*');
    calculator.calculate();
    expect(calculator.result).toBe(6);
  });
  it('should divide when the / operator is selected and the calculation button is clicked.', () => {
    calculator.calculatorForm.get('operand1')?.setValue(3);
    calculator.calculatorForm.get('operand2')?.setValue(2);
    calculator.calculatorForm.get('operator')?.setValue('/');
    calculator.calculate();
    expect(calculator.result).toBe(1.5);
  });

提醒一下,所有 测试都是绿色的,如下面在浏览器上启动的 Karma 的截图所示:

图 11.1 – 计算器组件测试在终端中成功

图 11.1 – 计算器组件测试在终端中成功

我们现在 将重构 代码 中的 <st c="8056">calculate()</st> 函数<st c="8084">calculator.component.ts</st> 组件 中,如下所示:

 calculate(): void {
  const operator = this.calculatorForm.get('operator')?.value;
  const operand1 = this.calculatorForm.get('operand1')?.value;
  const operand2 = this.calculatorForm.get('operand2')?.value;
  if (!operator ||!operand1 ||!operand2) return;
  switch (operator) {
    case '+':
      this.add(operand1, operand2);
      break;
    case '-':
      this.subtract(operand1, operand2);
      break;
    case '*':
      this.multiply(operand1, operand2);
      break;
    case '/':
      this.divide(operand1, operand2);
      break;
    default:
      console.error(`Unsupported operator: ${operator}`);
      break;
  }
}

现在,我们的测试情况如何呢? 它们仍然都是绿色的,如下面的 截图所示:

图 11.2 – 绿色测试

图 11.2 – 绿色测试

这意味着我们的重构是正确的,因为如果不是这样,我们的测试就会变成红色。 我们现在理解了 TDD 的重要性,因为我们可以在没有担忧的情况下专注于重构我们的方法。 在下一节中,我们将学习一些在重构中 TDD 的好处。 在重构中,TDD 有以下好处:

重构中 TDD 的好处

本节深入探讨了在 Angular 代码重构期间使用 TDD 的有益好处。 以下是一些 好处:

  • 增加信心:通过测试,你可以作为一个安全网,让你在不用担心破坏现有功能的情况下尝试不同的重构技术。 这增强了你在过程中的信心。

  • 改进设计:首先考虑测试可以鼓励你编写具有良好定义功能的模块化代码。 这导致代码在长期运行中更加干净和易于维护。

  • 增强可维护性:一个全面的测试套件成为代码预期行为的活文档。 这简化了未来的修改和错误修复,因为你可以依赖测试来 捕获回归。

  • 更好的代码覆盖率:TDD 自然会鼓励你专注于用测试覆盖各种代码路径。 这导致更健壮的应用程序,隐藏的 bug 更少。

行动中的示例

让我们看看如何 将 TDD 应用于 Angular 中常见的重构场景

  • 重构长服务方法:想象一个负责获取和处理大量数据的服务方法。 你可以编写一个专注于数据处理逻辑特定方面的测试。 最初,这个测试会失败。 然后,你会重构服务方法,将逻辑提取到单独的、经过良好测试的函数中。 这提高了代码的可读性和可维护性,同时测试确保核心功能保持完整。

  • 转换神组件:“神组件”指的是一个过于复杂的组件,通常通过在自身内部处理过多的责任或功能来违反单一职责原则。 这个术语用来强调组件变得过大且难以管理、测试和理解的负面方面。 这类组件往往注入许多服务,执行多项任务,并可能导致重大的维护挑战,随着时间的推移。 这种执行大量任务的组件可以通过创建专门针对特定功能的服务来重新设计。 因此,可以编写测试来验证重构组件和新 创建的服务的行为。

选择要编写的正确测试

在优先考虑 使用 TDD 进行重构的测试时,考虑 以下策略:

  • 关注痛点:从导致代码库中出现问题(如易出错或难以理解的区域)的功能开始。 这些区域通常会导致代码难以维护、测试和理解。

  • 从小处着手:从较小、定义良好的测试开始,这些测试针对特定的功能。 这允许更快地通过 红-绿-重构周期。

  • 测试集成点:当重构与服务或其他组件交互的组件时,编写测试以验证这些交互以及组件本身。

在下一节中,我们将解释如何识别 Angular 应用程序中的代码异味和改进区域。 Angular 应用程序。

在 Angular 应用程序中识别代码异味和改进区域

虽然你的 Angular 应用程序可能表面上看起来功能正常,但可能存在潜在的等待爆发的問題。 这些问题,被称为代码异味,不一定导致立即的问题,但表明你的代码库中 可能从重构中受益的区域。 就像杂乱无章的房间会让人感到压力和低效一样,有异味的代码会使维护、理解和扩展应用程序变得困难。 本节深入探讨了 Angular 应用程序中的代码异味世界。 我们将探讨它们是什么,为什么它们很重要,以及如何主动识别它们。 通过理解这些代码异味,你将能够使用 TDD 优先考虑重构工作,最终导致一个更干净、更可维护和更健壮的 代码库。

什么是代码异味?

想象一下 走进一个厨房,脏盘子堆满了水池,香料散落在台面上,过期的食物留在冰箱里。 这个令人不愉快的场景可能不会阻止你做一顿基本的饭菜,但肯定不会是一个愉快或高效的经历。 代码异味在软件开发世界中类似于这个杂乱的厨房。

由马丁·福勒在其著作中提出, 重构:改进现有代码的设计,代码异味是代码库中潜在问题的指标。 它们不一定代表导致应用程序崩溃的功能性错误。 相反,它们标志着 可能需要改进以获得更好的可读性、可维护性和代码长期健康性的区域。

代码异味不是错误,但它们可能会在未来吸引错误。 它们就像红旗,警告你潜在的麻烦区域,这些区域可能会随着你的 应用程序的发展而变得有问题。

为什么我们应该关注 Angular 中的代码异味呢?

忽视杂乱的厨房可能会导致烹饪时出现不愉快的气味、果蝇和挫败感。 同样,忽视 Angular 应用程序中的代码异味可能会导致 几个 负面后果:

  • 降低可维护性:随着时间的推移,有异味的代码变得难以理解和修改。 随着您的应用程序的增长和功能的添加,错综复杂的代码的复杂性可能会使更改变得繁琐并 容易出错。

  • 增加调试时间:当出现有异味的代码中的错误时,确定根本原因可能具有挑战性。 缺乏清晰的结构和组织使得寻找错误就像在 haystack 中寻找一根针一样,浪费了宝贵的 开发者时间。

  • 降低团队生产力:与有异味的代码一起工作可能会让开发者感到沮丧和缺乏动力。 解读错综复杂的逻辑的认知负担会减慢开发速度并 阻碍协作。

  • 技术债务:随着时间的推移,未处理的代码异味会积累,最终形成需要解决的技术债务。 这种债务可能成为一个重大的负担,需要专门的资源,并可能延迟新 功能开发。

通过积极 地识别和重构代码异味,您可以做到以下:

  • 提高代码可读性:干净且结构良好的代码更容易被您和其他在项目上工作的开发者理解。 这减少了新团队成员的入职时间,并促进了 更好的协作。

  • 增强可维护性:重构后的代码更容易修改和适应,因为您的 应用程序需求发生变化。 这使您能够更有效地引入新功能和错误修复

  • 减少调试时间:清晰关注点分离的干净代码在出现错误时更容易隔离和修复。 当出现错误时。

  • 提高团队生产力:与结构良好的代码一起工作可以改善开发者的体验和满意度。 这导致更高的生产力和更积极 的开发环境。

  • 最小化技术债务:通过早期解决代码异味,您可以防止它们积累并成为未来的一大负担。

本质上,优先考虑代码异味重构是对你 Angular 应用程序 长期健康和可维护性的投资。

识别 Angular 应用程序中最常见的代码异味

现在我们 理解了识别代码异味的重要性 ,让我们看看你可能在你的 Angular 应用程序 中遇到的常见违规行为:

  • 漫长而曲折的方法:想象一下你的服务中的一个方法,它跨越了数十行,处理各种任务。 这是一个长方法的典型例子,一个表明缺乏模块化的代码异味。 这些方法可能难以理解、测试和修改。 重构涉及将这些巨无霸分解成更小、更明确的函数,每个函数都专注于特定的任务。 这提高了代码的可读性 和可维护性。

  • 神组件:你是否遇到过责任过重的组件? 这是一个“神组件”,从数据获取到复杂的 UI 逻辑都由它处理。 这样的组件成为维护噩梦,因为一个区域的变化可能会在整个组件中产生连锁反应,导致 意外的后果。 重构可能包括 以下内容:

    • 创建专用服务:将与数据访问、业务逻辑或计算相关的功能提取到单独的服务中。 这些服务可以被多个组件重用 ,促进 更好的组织。

    • 拆分组件:将神组件拆分成更小、更专注的组件,每个组件处理 UI 或功能的一个特定方面。

  • <st c="19648">10</st> 用于分页,但其目的不明确。 重构涉及用命名变量或常量替换这些魔法数字。 例如,使用 <st c="19802">ITEMS_PER_PAGE</st> 而不是 <st c="19828">10</st>,使代码更具自文档性,更容易 理解。

  • 意大利面代码迷宫:想象一下蜿蜒曲折、缺乏清晰结构和组织的代码。 这就是意大利面代码,使其在导航、理解和修改时具有挑战性。测试驱动开发(TDD)可以成为对抗意大利面代码的有力工具。通过首先编写测试,然后重构代码以满足这些测试,你可以引入结构并改善代码库的整体组织。

在下一节中,我们将学习迭代改进:持续代码增强的红色-绿色-重构循环。

迭代改进 – 持续代码增强的红色-绿色-重构循环

在 Angular 应用程序中重构现有代码可能是一项艰巨的任务。你希望改进代码的结构和组织,但引入回归(错误)的恐惧常常笼罩心头。这就是 TDD 介入的地方,它提供了一种结构化和迭代的途径,以自信地导航重构。在 TDD 的核心是“红色-绿色-重构”循环,这是一种强大的技术,可以在确保功能完整的同时,对代码库进行增量改进。它通过在重构代码之前编写测试,然后将代码重构以满足这些测试,可以引入结构并改善代码库的整体组织。在 TDD 的核心是“红色-绿色-重构”循环,这是一种强大的技术,可以在确保功能完整的同时,对代码库进行增量改进。它通过在重构代码之前编写测试,然后将代码重构以满足这些测试,可以引入结构并改善代码库的整体组织。它通过在重构代码之前编写测试,然后将代码重构以满足这些测试,可以引入结构并改善代码库的整体组织。

想象你是一位正在处理一块大块大理石的雕塑家。红色-绿色-重构循环就像你的路线图,将这块原材料变成杰作。以下是每个阶段及其在重构 Angular 代码的上下文中的重要性的分解:

红色 – 使用失败的测试设定舞台

循环从红色开始 ,表示失败的测试。 这可能会显得有些不合常理——为什么编写注定要失败的测试呢?这个初始红色测试的目的是定义你打算重构的代码的预期行为。 把它想象成一份概述你想要实现的功能的蓝图。 以下是创建红色测试涉及的内容:

  • 确定重构目标:首先确定 Angular 应用程序中一个特定的区域,该区域表现出代码异味或需要改进。 这可能是服务中的长方法、处理许多任务的上帝组件,或者重复的代码片段。

  • 定义预期结果:清楚地概述重构后的代码应该做什么。 它应该处理哪些数据? 它应该如何与 UI 交互? 编写一个反映这种预期行为的测试。 记住,这个测试最初会失败,因为所需的功能尚未实现。

失败的红色测试 发挥着至关重要的作用。 它建立了一个基线——对当前缺失的功能有清晰的理解。 这为重构过程提供了一个安全网。 当你进行代码更改时,失败的测试确保你走在正确的轨道上,并且没有意外破坏 现有功能。

绿色——用最少的代码使测试通过

一旦你 设置了红色测试,就到了进入绿色阶段的时候。 在这里,目标是编写足够的代码来使失败的测试通过。 在这个阶段,不要陷入编写完美优化或优雅的代码。 专注于测试定义的核心功能。 以下是绿色阶段的一些关键考虑因素:

  • 简单实现:你最初编写的代码以使测试通过可能是基本的。 它不必是最有效或结构良好的解决方案。 优先级是让测试通过并建立重构的基线

  • 关注功能:确保你编写的代码满足测试中概述的特定行为。 在这个阶段,不要引入不必要的功能或 逻辑。

通过绿色测试标志着重要的里程碑。 它验证了你正在重构的核心功能现在已实现,尽管可能只是基本形式。 这个绿色测试在整个重构过程中充当安全网。 当你进行进一步的代码更改时,你可以依赖这个测试来确保你没有偏离 预期的结果。

重构——自信地转换代码

通过一个 通过绿色测试作为安全网,你已经到达了循环的核心——重构阶段。 在这里,你可以发挥你的重构技能,提高代码的可读性、可维护性和效率。 以下是一些你可能想要在重构过程中关注的潜在领域: 重构:

  • 模块化长方法:将那些长而单一的方法分解成更小、更明确的函数。 这增强了代码的可读性,并使理解逻辑流程更容易。 逻辑流程。

  • 提取可重用组件和服务:如果你的组件已经变成了处理众多任务的上帝组件,考虑将功能提取到专门的服务或可重用组件中。 这促进了更好的组织和关注点的分离

  • 消除重复:识别并重构重复的代码片段到可重用的组件、服务或实用函数中。 这减少了代码冗余并 简化了维护。

  • 应用设计模式:考虑整合促进代码结构和组织更好的设计模式。 这可以使你的代码更易于维护,并且更容易被其他开发者理解。 其他开发者。

  • 简化逻辑:寻找机会简化复杂的逻辑,并增强代码的清晰度。 这可能包括使用更具描述性的变量名,分解复杂的条件语句,或利用 辅助函数。

在整个重构过程中,牢记绿色测试。 迭代重复这个循环,一次解决代码的一个方面。 每个完成的循环都会让你拥有更干净、更易于维护的代码,以及保证其持续功能的健壮测试。 对小而渐进的变化的强调促进了更受控、更少错误的 重构过程。

摘要

总结来说,通过 TDD 重构和改进 Angular 代码是一种强大的方法,可以提升 Angular 应用程序的质量、可维护性和效率。遵循 TDD 方法论,开发者可以确保他们的代码健壮、结构良好且易于理解。这种方法不仅有助于识别和解决代码问题,而且为未来的增强和修改奠定了坚实的基础。TDD 鼓励开发者先编写测试,然后再编写实际代码,确保代码满足定义的要求并按预期行为。编写测试、使测试通过,然后对代码进行重构以改进,这是 TDD 的核心。它培养了一种持续改进的文化,代码始终处于准备进一步开发的状态。

此外,TDD(测试驱动开发)促进了独立、可测试的代码单元的开发,使得在开发早期阶段更容易识别和修复问题。这在 Angular 应用程序中尤其有益,因为组件、服务和模块通常具有复杂的依赖关系和交互。通过单独测试这些单元,开发者可以确保在将它们集成到更大的系统之前,应用程序的每个部分都能正确工作。

此外,TDD 通过鼓励开发者编写清晰、简洁且文档齐全的测试,促进了高质量、可维护的代码的开发。这些测试作为文档,使得其他开发者(甚至未来的原始开发者)更容易理解代码的目的和功能。

在 Angular 的背景下,TDD 在开发服务、组件和管道方面特别有效,如提供的示例所示。通过明确定义代码应该做什么,开发者可以编写指导实现过程的测试,确保代码满足所需的规范。这种方法不仅导致代码设计得更好,而且使开发过程更加高效和愉快。

总结来说,通过 TDD 重构和改进 Angular 代码是一种有价值的实践,可以显著提升 Angular 应用程序的质量。通过采用 TDD,开发者可以确保他们的代码健壮、可维护,并准备好未来的增强。这种方法不仅有利于当前的开发周期,而且为未来的开发工作奠定了坚实的基础,对于任何 Angular 开发者来说都是一项值得的投资。

这是本书的结尾。 本书为希望采用或改进他们在 Angular 项目中 TDD 实践的开发商提供了一本全面的指南。 通过遵循本书中描述的原则和技术,开发商可以显著提高他们 Angular 应用程序的可靠性、性能和可维护性。 本书强调了全面测试策略的价值、使用正确工具和实践的重要性,以及采用测试优先的开发方法的好处。 无论你是 Angular 的新手还是一个希望磨练技能的有经验的开发者,本书都提供了掌握 Angular 中 TDD 的宝贵见解和实用建议。

posted @ 2025-09-09 11:31  绝不原创的飞龙  阅读(10)  评论(0)    收藏  举报