精通-Angular-测试驱动开发-全-
精通 Angular 测试驱动开发(全)
原文:
zh.annas-archive.org/md5/1ba27f2d07743700638425110cd3d10b译者:飞龙
第一章:前言
-
红色:在这个初始阶段,开发者为他们打算实现的功能编写测试。 由于还没有相应的代码,这个测试最初会失败,因此用“红色”这个词来表示测试的失败状态。 -
绿色:在红色阶段之后,开发者编写必要的最少代码以使测试通过。 这一阶段的目标是快速从失败的测试(红色)过渡到通过的测试(绿色),重点是满足测试条件,而不一定优化代码。 -
重构:在测试成功通过后,代码随后得到改进和优化。 这一阶段涉及改进代码的设计、结构和效率,同时确保测试保持绿色状态,即继续通过。 重构对于在不改变代码外部行为的情况下提高代码质量、可维护性和性能至关重要,正如通过测试所证实的那样。
本书面向对象
本书涵盖内容
为了充分利用本书
*
| 本书涵盖的软件/硬件 操作系统要求 |
|---|
| 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
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“现在我们可以通过点击提交 更改 按钮来保存文件。”
提示或重要注意事项
如此显示。
联系我们
读者反馈始终受欢迎。
分享您的想法
免费下载本书的 PDF 副本
扫描二维码或访问以下 链接

-
提交您的购买证明 -
就是这样! 我们将直接将您的免费 PDF 和其他福利发送到您的 电子邮件
第一部分:在 Angular 中开始测试驱动开发
-
第一章 , 使用 TDD 的第一步 -
第二章 , 使用 Jasmine 和 Karma 测试 Angular 应用程序
第二章:1
使用 TDD 的第一步
<st c="842">karma.conf.js</st>
-
理解 TDD 及其在 Angular 中的作用 -
设置 开发环境 -
创建一个新的 Angular 项目 -
探索与 编写测试 相关的不同文件
技术要求
理解 TDD 及其在 Angular 中的作用
什么是 Angular 和 TDD?
-
编写失败的测试 :通过编写一个故意失败的测试来启动循环。 这个测试作为所需功能的规范。 -
禁止过于复杂的测试 :强调创建仅必要复杂的测试。 避免不必要的复杂性确保测试始终专注于特定的功能,增强清晰度和可维护性。 。 -
最小化代码实现 :编写通过失败测试所需的最少代码。 这种极简方法确保代码专注于满足指定的要求。
红绿重构周期
-
红色 – 编写失败的测试 : 红绿重构周期的第一步是编写一个失败的测试。 测试应定义代码的预期行为,并且应该以它最初失败的方式来编写。 这被称为“红色”步骤 因为测试预期 将失败。 -
绿色 – 编写通过测试的代码 : 第二步是编写使测试通过的代码。 代码应该是最小的,并且只应该编写来使测试通过。 这被称为“绿色”步骤 因为测试预期 将通过。 -
重构 – 不改变功能的情况下改进代码 : 一旦测试通过,开发者可以通过消除重复、简化代码和改进其可读性来重构代码,从而增强其设计、可读性和可维护性。 关键是不要改变测试所覆盖的功能。
红绿重构周期的益处
-
增强代码质量 :红-绿-重构循环确保代码正确、可靠,并满足测试中预定义的要求 的测试 -
加速开发 :红-绿-重构循环允许开发者在开发过程中早期捕捉错误,这节省了时间并减少了修复错误的成本 的修复错误 -
更好的协作 :红-绿-重构循环鼓励开发人员、测试人员和其他利益相关者之间的协作,这改善了沟通并有助于确保每个人都处于 同一页 -
简化维护 :红-绿-重构循环生成的代码更容易维护和扩展,这减少了未来开发的成本和努力 的未来开发
TDD 在 Angular 开发中的作用
设置开发环境
在 Windows 或 macOS 上安装 Node.js
访问官方 Node.js 网站( https://nodejs.org/en/ ),然后点击 LTS 版本的 下载 按钮。 这将下载适用于 Windows 或 macOS 的最新版本 Node.js:


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

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

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

图 1.5 – Node.js 安装 – 步骤 4
-
选择您想要创建 Node.js 开始菜单快捷方式的文件夹。
-
点击安装按钮开始安装过程。

图 1.6 – Node.js 安装 – 步骤 5
- 安装完成后,您应该会看到一个消息,表明 Node.js 已成功安装。

图 1.7 – Node.js 安装 – 步骤 6
-
为了验证 Node.js 是否已正确安装,打开命令提示符并运行以下命令:
<st c="13704">$ node –v</st>这应该会显示您刚刚安装的 Node.js 的版本号:

图 1.8 – 检查 npm 版本
在下一节中,我们将学习如何在 Linux 上安装 Node.js。
安装 Node.js 到 Linux
按照以下步骤在 Linux 上安装 Node.js:
-
打开您的终端并运行以下命令以更新包管理器:
<st c="14050">$ sudo apt update</st>前面的命令将给出以下输出:

图 1.9 – 更新 Ubuntu 软件包
-
运行以下命令以安装 Node.js:
<st c="16348">$ sudo apt install nodejs</st>前面的命令将给出以下输出:

-
为了验证 Node.js 是否已正确安装,请运行以下命令: : : <st c="17816">$ node –v</st>这应该显示你刚刚安装的 Node.js 的版本号: : : :

-
npm 是 Node.js 的包管理器。 如果你还没有与 Node.js 一起安装它,请运行以下命令: : : <st c="18111">$ sudo apt install npm</st>这里是 输出结果:

-
为了验证 npm 是否已正确安装,请运行以下命令: : : : : <st c="20695">$ npm –v</st>你应该看到以下输出: :

创建一个新的 Angular 项目
$ ng new getting-started-angular-tdd --routing
<st c="21442">getting-started-angular-tdd</st>
$ 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> <st c="22639">函数定义的。</st><st c="22687">it()</st> <st c="22702">例如,如果你正在测试一个名为</st> <st c="22764">》的组件,你将创建一个名为</st>
<st c="22880">The</st> <st c="22885">describe()</st> 函数用于将相关的测试用例组合在一起,它接受两个参数:一个描述测试套件的字符串和一个定义该套件内测试用例的函数。<st c="23085">it()</st> 函数用于定义单个测试用例,它接受两个参数:一个描述测试用例的字符串和一个包含测试用例代码的函数。<st c="23311">expect()</st> 函数来定义你代码的预期行为。 <st c="23375">例如,你可能使用</st> <st c="23445">来测试组件的</st>
<st c="23516">*.spec.ts</st> <st c="23633">例如,如果你正在测试一个使用了</st> <st c="23702">服务的组件,你需要从</st><st c="23783">导入组件和</st>
karma.conf.js 文件
<st c="23846">karma.conf.js</st>
<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>
test.ts 文件
<st c="25255">test.ts</st> <st c="25430">zone.js</st>
<st c="25577">test.ts</st> <st c="25618">src</st>
<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> <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> <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> <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>
摘要
<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>
在下一章中,我们将学习使用 Jasmine 编写和执行单元测试的过程,同时涵盖测试套件、测试规范
第三章:2
使用茉莉和卡玛测试 Angular 应用程序
-
掌握茉莉的单元 测试技术 -
编写你的 第一个与 Angular 相关的单元测试,关于 测试驱动开发 (TDD) ( TDD ) -
利用卡玛的代码覆盖率和测试结果分析
技术要求
-
在您的计算机上安装了 Node.js LTS 和 npm LTS -
安装了 Angular 17 或更高版本的 CLI 全局 -
在 您的计算机上安装了代码编辑器,例如 Visual Studio Code
掌握 Jasmine 的单元测试技术
什么是 Jasmine?
使用 Jasmine,开发者可以使用 BDD 风格来结构化他们的测试,这使得编写既描述性强又易于阅读的测试变得简单。
编写描述性的测试套件
<st c="6183">calculateTotal</st>
选择有意义的名称
结构化测试套件
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 项目中编写第一个单元测试
-
通过运行以下命令创建一个名为 `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> -
现在,我们将 使用 <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); }); }); -
您将在代码编辑器中遇到一个错误,提示您 `add 函数 不存在:

<st c="14838">CalculatorComponent</st>
-
接下来,我们将实现 <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 服务器上看到以下结果:


利用 Karma 的代码覆盖率和测试结果分析
-
步骤 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 将报告失败的 测试结果。
代码覆盖率可视化
<st c="21183">$ ng test –code-coverage</st>
如果一切顺利,在终端中我们将看到以下内容:

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

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

<st c="22695">index.html</st>





通过使用代码覆盖率以及 Karma 的测试结果分析,开发者可以提升他们的测试实践并确保全面的代码覆盖率。
总结
第二部分:编写有效的单元测试
-
第三章 , 为 Angular 组件、服务和指令编写有效的单元测试 -
第四章 , 在 Angular 测试中模拟和存根依赖 -
第五章 , 测试 Angular 管道、表单和响应式编程
第四章:3
为 Angular 组件、服务和指令编写有效的单元测试
-
Angular 单元测试的高级技术:生命周期钩子和 依赖关系 -
Angular 单元测试的高级技术: Angular 服务 -
使用严格的指令测试来确保适当的渲染 和功能
技术要求
-
在您的计算机上安装了 Node.js 和 npm
-
全局安装 Angular CLI
-
在您的计算机上安装了代码编辑器,例如 Visual Studio Code
高级 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.
实际应用
<st c="4744">0</st><st c="4813">ngOnInit()</st>
<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>

<st c="5617">result</st>

<st c="6264">0</st>

<st c="7046">result</st> <st c="7062">0</st> <st c="7071">ngOnInit()</st>
ngOnInit(): void {
this.result = 0;
}

<st c="7526">ngOnDestroy()</st>
<st c="7780">ngOnDestroy()</st>
<st c="8030">CalculatorComponent</st>
<st c="8198">CalculatorService</st> <st c="8236">我们的</st>

<st c="8583">providers</st>


<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>

<st c="10245">应该正确添加两个数字</st>
<st c="10571">add()</st> <st c="10607">add()</st>
<st c="10678">add()</st>


<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>

Angular 单元测试的先进技术 – Angular 服务
在本节中,我们将
测试服务方法
add()
<st c="14833">calculator.component.spec.ts</st>

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

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

图 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 中添加减法方法
《st c="16542">在我们的 <st c="16561">calculator.component.spec.ts</st> 测试文件中的结果与预期相符:</st c="16589">

图 3.16 – 没有错误的减法方法测试用例
当我们在终端中浏览时,我们得到以下预览:


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

《st c="17826">图 3.18 – 添加乘法和除法方法测试用例</st c="17826">
然后在我们的 <st c="17898">calculator.service.ts</st> 服务中,我们有以下内容:

《st c="18094">图 3.19 – 在 calculator.service.ts 中添加乘法和除法方法</st c="18094">
最后,在我们的 <st c="18188">calculator.component.ts</st> 中,我们有以下内容:



<st c="20101">result</st>
使用严格的指令测试以确保适当的渲染和功能
实现颜色更改指令
<st c="21274">colorChange</st>
-
在项目 <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:

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

为 colorChange 指令编写测试
<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>

<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>被视为一个错误:

<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() {}
}

<st c="28206">calculator.component.spec.ts</st>

<st c="32213">calculator.component.spec.ts</st>


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> <st c="33848">ElementRef</st> <st c="33863">Renderer2</st>

总结
<st c="35003">TestBed</st> <st c="35015">ComponentFixture</st>
第五章:4
在 Angular 测试中模拟和存根依赖
为了为 Angular 应用编写有效和可靠的测试,理解如何处理依赖是至关重要的。
在本章中,我们将探讨间谍和方法替代的概念。
接下来,我们将看看 TestBed 提供者和它们如何允许我们将模拟依赖注入到测试中。<st c="933">TestBed</st>
最后,我们将探讨在设置依赖时如何处理异步操作和复杂场景。<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>

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

<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> <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> <st c="10418">是</st><st c="10440">类型,而不是</st>
<st c="10524">CalculatorComponent</st>


<st c="11301">core</st> <st c="11326">stubs</st> <st c="11352">calculator.service.stub.ts</st>

<st c="11598">calculator.component.spec.ts</st> <st c="11636">calculator.service.stub.ts</st>

-
在 <st c="12141">calculator.service.stub.ts</st>中创建一个名为 <st c="12175">CalculatorServiceStub</st>的类。 -
实现我们 计算器应用程序 的所有运算符方法。以下是源代码的 样子: 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; } } -
在更新我们的模拟服务后,我们将进入我们的 <st c="12683">calculator.component.spec.ts</st>测试文件,以替换提供者 如下: import { CalculatorServiceStub } from 'src/core/stubs/calculator.service.stub'; ... providers: [ { provide: CalculatorService, useClass: CalculatorServiceStub }, ], ... -
现在我们可以取消注释我们 <st c="12970">calculator.component.spec.ts</st>文件中的所有方法,除了 测试用例 <st c="13016">it('should display error message for division by zero')</st>。 注意,所有 我们的测试都是绿色的,如图所示:

使用 TestBed 提供者注入模拟依赖项
<st c="15794">CalculatorService</st>
<st c="16123">MockSquareRootService</st><st c="16169">mocks</st> <st c="16240">stubs</st>

export class MockSquareRootService {
calculateSquareRoot(value: number): number {
// Perform a predefined square root calculation based on the input value
return Math.sqrt(value);
}
}
<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>
<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>
<st c="19058">TestBed.configureTestingModule</st><st c="19217">useClass</st> <st c="19229">useValue</st>
处理异步操作和复杂场景
在本节中,我们将探讨在 Angular 应用程序中测试异步操作(如承诺和可观察者)以及复杂场景的重要性。
理解异步操作
<st c="22617">承诺</st>
<st c="23085">async</st><st c="23092">await</st> <st c="23301">async</st> <st c="23386">await</st>
处理异步操作
<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:

<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>

<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>

<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>

<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>

<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>

<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>

<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>

<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>

<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>

<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>

<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>

<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 管道的测试
, 我们的项目 -
将测试驱动开发应用于我们的响应式表单
, 响应式表单
技术要求
为了跟随本章中的示例和练习,您需要对 Angular 和 TypeScript 有一个基本的了解,以及访问以下内容
-
在您的计算机上安装的 Node.js 和 npm
, 您的计算机 -
安装全局的 Angular CLI
, 全局安装 -
在您的计算机上安装的代码编辑器,例如 Visual Studio Code
, 您的计算机
本章的代码文件可以在以下位置找到
测试我们在项目中使用的 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>

<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();
});
});
-
一个将正数格式化为 百分比字符串 的测试 -
一个将负数格式化为 百分比字符串 的测试 -
一个将小数格式化为 百分比字符串 的测试 -
一个将非数字格式化为 百分比字符串 的测试
正数转换为百分比字符串格式化测试
<st c="3669">123</st><st c="3704">12300%</st><st c="3719">percent.pipe.spec.ts</st>
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>


<st c="5633">percent.pipe.ts</st>

transform (value: number): string {
const formattedValue = value * 100;
return formattedValue + '%';
}
<st c="6317">%</st>


负数转换为百分比字符串格式化测试
<st c="8188">-123</st><st c="8224">-12300%</st><st c="8244">percent.pipe.spec.ts</st>
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>


小数转换为百分比字符串格式化测试
<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>


非数字转换为百分比字符串格式化测试
<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.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.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 实现
所有的东西都工作得非常完美!
在我们的计算器应用中实现响应式表单的 TDD 测试
-
动态和响应式 :响应式表单可以用来创建动态和响应式的用户界面。 例如,你可以使用响应式表单创建一个计算器,当用户在输入字段中输入值时,它会更新结果。 -
有效性 :响应式表单提供验证功能,可以用来确保用户输入是有效的。 例如,你可以使用响应式表单创建一个计算器,验证操作数是否为数字,以及运算符是否为有效的 数学运算符。 -
可测试性 :响应式表单易于使用 TDD 进行测试。 这确保了你的表单是有效的,并且计算器组件按预期工作。
编写计算器表单的测试
<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>


<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>

<st c="22322">calculatorForm</st> <st c="22358">CalculatorComponent</st>

<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]),
});

<st c="23332">ng test</st>


<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>


实现用户界面
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>
<st c="27581">ng serve -o</st>

calculate()
为计算器组件编写测试
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);
});


<st c="29965">calculate()</st> <st c="30010">CalculatorComponent</st><st c="30264">calculate()</st> <st c="30290">calculator.component.ts</st>

<st c="30617">ng test</st>


<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>


<st c="34029">calculate()</st> <st c="34092">CalculatorComponent</st><st c="34340">calculate()</st> <st c="34366">calculator.component.ts</st>

<st c="34684">ng test</st>


<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>


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


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


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


最后,我们将设置最后一个操作,即 <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> 命令后,我们将得到以下结果:


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


我们的测试失败了,这是完全正常的,因为我们的 <st c="42079">calculate()</st> 方法目前还没有处理 <st c="42139">CalculatorComponent</st> 中的除法。我们现在需要添加最少的代码来使其功能正常。<st c="42387">calculate()</st> 方法中更新 <st c="42413">calculator.component.ts</st> 文件时的样子:

<st c="42662">ng test</st>


<st c="44292">ng serve -o</st>

摘要
第三部分:端到端测试
-
第六章 , 使用 Protractor、Cypress 和 Playwright 探索端到端测试 -
第七章 , 理解 Cypress 及其在 Web 应用程序端到端测试中的作用 -
第八章 , 使用 Cypress 编写有效的端到端组件测试
第七章:6
使用 Protractor、Cypress 和 Playwright 探索端到端测试
-
发现 端到端测试 -
分析在 项目中 E2E 测试的优势 -
探索可用于端到端测试的不同工具,如 Protractor、Cypress、Playwright
技术要求
-
在你的计算机上安装 Node.js 和 npm -
全局安装 Angular CLI -
在你的计算机上安装代码编辑器,例如 Visual Studio Code
理解端到端测试
发现端到端测试的好处
-
早期发现缺陷 :端到端测试能够在开发周期的早期发现缺陷,减少后期纠正所需的成本和努力。 -
增强用户体验 :端到端测试是一种确保软件应用从用户角度出发按预期工作的方法。 这种方法提高了整体用户体验并最小化了不满意的风险 。 -
提高软件质量信心 :端到端测试增强了软件质量的信心,降低了生产中意外问题的风险,并保护了软件开发团队的声誉 。 -
简化开发流程 :端到端测试可以自动化,简化开发流程,促进 持续集成和持续交付 ( CI/CD )实践,并 使软件能够更快、更高效地发布。
探索不同的端到端测试方法
-
基于脚本的测试 :基于脚本的测试是一种软件测试方法,其中使用自动化脚本来执行测试用例。 这种方法保证了测试流程的一致性、可重复性和效率,使其成为确保软件应用程序平稳运行的可信工具。 有几个流行的基于脚本的测试框架,包括 Protractor、Cypress 和 Playwright。 这些框架各有其特点和优势: -
Protractor 是一个用于 Angular 和 AngularJS 应用程序的端到端测试框架。 它的目标是简化测试设置过程,使测试更易于阅读,并产生更容易理解的结果。 -
Cypress 是一款为现代网络设计的下一代前端测试工具。 它提供了一套完整的测试解决方案,包括直接在 JavaScript 中编写测试的能力,无需额外的预处理器或编译器。 Cypress 以其易用性和快速安装而闻名。 -
Playwright ,由微软开发和维护,是一个开源的基于 Node.js 的端到端测试自动化框架。 它被创建出来是为了满足跨多个浏览器进行自动化端到端测试的需求。 Playwright 的主要目标是运行在主要的浏览器引擎上——Chromium、WebKit 和 Firefox。 它提供了广泛的本地移动测试功能,支持在 Android 和 iOS 平台上的移动自动化测试。
-
-
探索性测试 :探索性测试是一种动态灵活的软件测试方法,涉及测试人员积极探索应用程序并尝试不同的场景、输入和交互来识别错误和问题。 与基于脚本的测试不同,后者遵循预定义的测试用例和步骤,探索性测试基于测试人员的好奇心、创造力和直觉。 这种方法在关于软件确切应该做什么或在实际情况下应该如何表现存在许多未知因素时特别有用。 在现实生活中的情况下。 探索性测试通常在应用程序复杂或不断演变时使用。 当需要快速反馈、需求不明确或时间紧迫时,它也非常有用。 的精髓。 探索性测试有不同的类型——自由式测试、基于场景的测试和 基于策略的测试: -
自由式测试 :这在您需要快速熟悉 应用程序 时很有用 -
基于场景的测试 :这侧重于 现实世界的 使用场景 -
基于策略的测试 :这结合了 探索性测试 与 已知的 测试方法
探索性测试有时在识别正式测试可能遗漏的更细微的缺陷方面更有用。 然而,它需要高度的专业技能和对 应用程序 的理解。 -
基于脚本的测试和探索性测试的比较
-
一致性和效率 :基于脚本的测试保证了测试过程的 一致性和可重复性 ,这在需要大量测试的大规模项目中至关重要。 它也可以更高效,因为它消除了手动 测试执行 的需要。 -
灵活性和发现 :另一方面,探索性测试提供了更大的灵活性,因为它允许测试人员自由探索应用程序。 这可能导致发现脚本测试可能遗漏的潜在问题,尤其是在复杂应用程序或应用程序频繁更改的情况下。 frequent changes. -
文档 :两种方法之间的一个显著区别是文档的级别。 基于脚本的测试通常涉及详细的测试用例和步骤的文档,而探索性测试可能涉及较少的文档,这可能导致关键错误 被忽略。
基于脚本的测试和探索性测试的限制
利用端到端测试工具的力量
Selenium – 经证实的 Web 应用程序测试强大工具
Cypress – 简洁和快速进行 Web 测试
Appium – 在多个平台上进行移动应用程序测试
Protractor – Angular 应用程序的无缝自动化
Playwright – 具有性能的跨浏览器测试
-
Selenium 的灵活性和广泛的浏览器兼容性使其成为全面 Web 应用程序测试 的可靠选择 -
Cypress 的简洁和速度吸引了寻求简化 Web 测试 方法的团队 -
Appium 满足移动测试需求,支持在 iOS 和 Android 平台 上自动化 -
量角器简化了 Angular 应用程序的 自动化 -
Playwright 提供高性能的 跨浏览器测试
在下一节中,我们将分析项目中进行端到端测试的好处。
分析项目中进行端到端测试的好处
采用端到端测试作为软件开发项目的一部分揭示了众多好处,这些好处提高了软件产品的质量和用户体验。
尽管端到端测试提供了许多优点,但它们的实施并非没有问题。理解这些障碍对于制定克服它们的有效策略至关重要:
-
复杂性 :端到端测试通常具有错综复杂的结构,这使得它们难以创建和维护。这种复杂性源于需要测试多个应用程序组件和准确模拟真实用户的行为。 -
扩展性 :端到端测试容易不稳定,这种特性表现为间歇性故障,原因难以理解。这种间歇性可能会损害端到端测试结果的可信度。 -
缓慢 :由于端到端测试具有全局性质,需要与整个应用程序交互,并可能因等待外部系统响应而出现延迟,因此端到端测试通常运行速度较慢。
尽管存在固有的挑战,但端到端测试仍然是提高软件质量的无价工具。通过采用经过验证的策略,开发人员和测试人员可以有效地应对这些挑战,并收获端到端测试的许多好处:
-
尽早参与 :在开发周期早期启动端到端测试至关重要。这种积极主动的方法有助于及早发现缺陷,使缺陷的处理更加容易和成本效益更高。 -
利用自动化 :自动化通过简化执行过程和最大限度地减少时间和精力的消耗来加强端到端测试。自动化还便于将端到端测试无缝集成到CI/CD管道中。 -
细致的测试设计 :对端到端测试的设计进行细致的关注对于确保其有效性和效率至关重要。这意味着专注于测试最关键的用户流程和识别最可能发生故障的点。 -
利用专用工具 :有大量工具可供支持端到端测试。 选择一个符合项目需求并提供 用户友好功能的工具至关重要。
探索 Protractor、Cypress 和 Playwright 进行端到端测试
量角器
什么是 Protractor?
为什么选择 Protractor?
Protractor 提供了一套 Angular 特定的 API,这使得与 Angular 元素交互以及执行等待 Angular 进程完成、处理异步操作和管理与 Angular
您可以模拟真实用户交互,例如点击按钮、填写表单、在页面间导航以及验证 Angular 元素的行为。
它会在执行下一步之前自动等待 Angular 进程完成,从而消除了显式等待的需要,并减少了测试中的不可靠性。
它支持为 Angular 应用程序专门设计的各种定位器,包括模型、绑定、重复器和 CSS 选择器。
它允许您在包括 Chrome、Firefox 和 Safari 在内的多个浏览器中运行测试,从而实现 Angular 应用程序的跨浏览器测试。
将其与 CI 系统集成后,您可以在每次代码提交时自动执行测试,确保您的 Angular 应用程序保持稳定和功能正常。
Protractor 测试功能
通过提供对异步操作的内置支持,包括回调、承诺和 async/await,Protractor 使开发者能够做到以下事项:
-
提升测试执行速度 :实现更快的端到端测试,特别是对于具有动态元素的 Angular Web 应用程序 dynamic elements -
增强测试可读性 :使用现代异步编程方法编写更干净、更易于维护的测试 programming methods -
提高测试可靠性 :简化处理动态行为并提高测试稳定性 e test stability
Protractor 的一个关键特性是其
-
<st c="19558">wait</st>语句在您的测试中的每个动作之间。 -
Angular 同步 :Protractor 与 Angular 框架集成,以了解应用程序何时完成处理并准备好交互。 这确保了您的测试不会在元素完全加载和可用之前尝试与之交互 。
-
<st c="20047">ExpectedConditions</st>API 用于定义等待的特定条件,例如等待元素可点击或特定文本 出现。 -
长时间等待 :默认的隐式等待时间可能对于某些场景来说太短。 您可以为特定的测试用例配置自定义的等待时间。 。 Protractor 支持 Angular 和非 Angular 应用程序。 它为使用 AngularJS 构建的 Web 应用程序提供综合的端到端测试。 这使得它成为一个多功能的工具,可以用于测试广泛的 Web 应用程序。
Cypress
什么是 Cypress?
为什么选择 Cypress?
-
简化设置 :与 Selenium 等其他工具相比,Cypress 需要的最小配置。 通常只需安装包并使用 JavaScript 编写测试 。 -
快速高效:Cypress 直接在浏览器中运行测试,消除了单独设置 WebDriver 和启动浏览器的需求。
这使得测试执行速度显著提高,改善了开发和 测试工作流程。 -
自动等待:Cypress 自动处理等待元素加载和变为交互式的过程。
你不需要在测试中编写显式的等待或暂停,这减少了代码复杂性,并使测试更加健壮。 -
易于调试:Cypress 提供了一个可视化调试器,允许你逐步执行测试,检查元素属性,并快速识别问题。
这简化了调试过程。 -
时间旅行调试:使用 Cypress 的时间旅行调试功能,你可以回放或快进测试执行,以确定失败发生的确切时刻,使故障排除更加高效。
-
跨浏览器兼容性:Cypress 支持开箱即用的跨浏览器测试(Chrome、Firefox、Edge 等)。
你可以配置测试在多种浏览器上运行,或使用 CI 平台来自动化浏览器测试。 -
与开发工具集成:Cypress 与流行的开发者工具(如 DevTools)无缝集成,允许你在测试环境中利用现有的调试技能。
-
活跃的社区和支持:Cypress 拥有一个庞大且活跃的社区,提供广泛的文档、教程和支持资源。
这使得学习、使用和需要时获得帮助变得更加容易。
Cypress 测试功能
Cypress 拥有丰富的功能,有助于其在 Web 应用程序测试中的有效性:
-
命令链 API:提供了一种使用 JavaScript 语法编写测试的自然方式,使测试易于阅读和维护。
-
自动断言:简化了对预期元素属性和行为的检查,确保你的测试验证了正确的功能。
-
截图和视频录制:允许在测试期间捕获截图或录制视频,这有助于可视化错误和调试问题。
-
网络模拟 :允许 模拟服务器响应,便于测试 API 交互、边缘情况和各种 网络场景 -
自定义命令 :授予创建可重用代码块的能力,用于频繁使用的测试操作,促进代码重用和模块化 和模块化 -
集成测试 :Cypress 可用于 Web 应用程序的单元和集成测试,提供全面的 测试覆盖率
Playwright
什么是 Playwright?
为什么选择 Playwright?
Playwright 测试的特点
-
Playwright 在不同浏览器之间提供一致和可靠的自动化,确保你的网络应用程序在所有 主要平台上都能良好运行。 -
Playwright 以其速度和效率而闻名,使其成为测试和自动化 网络交互的绝佳选择。 -
你可以以无头模式(没有可见的浏览器 UI)运行 Playwright 脚本以实现更快的执行,或者以有头模式进行调试 和交互。 -
剧本编写者支持流行的编程语言,如 TypeScript、Python 和 Java。 这意味着你可以用你最 舒适的语言编写自动化脚本。 -
Playwright 使用原生 浏览器自动化 API 准确地模拟用户交互,从而产生更 可靠的测试。
总结
-
Protractor : 专为 Angular 应用程序设计,提供用于与元素交互和处理异步操作的 Angular 特定 API -
Cypress : 一款用户友好的工具,支持异步/await 语法和并发测试执行(尽管免费版本存在一些限制) -
Playwright : 提供了一种现代的异步/await 测试脚本方法,鼓励同时执行测试以加快执行速度,并拥有一个简单的 API 以实现高效的测试开发
第八章:理解 Cypress 及其在 Web 应用端到端测试中的作用
在 Web 开发的世界中,Angular 已经成为构建动态和强大 Web 应用程序最受欢迎的框架之一。
Cypress 是一个多功能的
我们将首先了解 Cypress,并理解其独特的特性和优势。
接下来,我们将深入了解在 Angular 项目中设置 Cypress 的过程。
最后,我们将指导您在 Angular 项目中使用 Cypress 编写第一个端到端组件测试的过程。
概括来说,以下是本章将涵盖的主要主题:
-
发现 Cypress 及其在 Angular 项目中的作用
-
在 Angular 项目中设置 Cypress
-
在 Angular 项目中使用 Cypress 编写第一个端到端组件测试
技术要求
-
Node.js 和 Node 包管理器 ( npm ) 安装在你电脑上 -
Angular CLI 全局安装 -
安装在你电脑上的代码编辑器,例如 Visual Studio Code。
探索 Cypress 及其在 Angular 项目中的作用
了解 Cypress
与 Angular 无缝集成
高效的测试工作流程
实时重新加载和调试
在我们的 Angular 项目中设置 Cypress
安装 Cypress
首先,您需要在您的机器上安装 Node.js 和 npm。
$ npm install cypress --save-dev


配置 Cypress
-
在安装 Cypress 后,从你的 项目根目录 运行以下命令:<st c="7266">$ npx cypress open</st>


点击 继续 ,你将被重定向到以下界面:

选择 E2E 测试 ,你将被重定向到如图 图 7 **.6 * 所示的界面:

-
<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>文件将是一个这样的固定文件,包含测试可能需要与之交互的示例数据。
点击 继续 后,您将看到此界面以选择您首选的 E2E 测试浏览器,如图 图 7 **.7 :

一旦完成所有配置,你应该能够访问界面,如图 所示 图 7 **.8 :

在 Angular 项目目录中,创建了一个 <st c="11126">cypress</st>文件夹 如下:

最后,我们将把这个脚本添加到 <st c="11310">package.json</st>中,以避免多次执行 <st c="11339">npx cypress open</st>:

编写您的第一个端到端测试
现在 Cypress 已安装并配置好,你可以开始编写你的第一个测试。 你可以点击 创建新规范 在此界面中,如图 图 7 **.11 :

完成这些后,你可以在 图 7 **.12 中看到一个名为 <st c="12159">spec.cy.ts</st>的新文件,它位于你的项目的 <st c="12186">e2e</st>文件夹中的 <st c="12215">cypress</st>文件夹:


现在,当你返回到启动端到端测试的浏览器时,你可能会感到惊讶,如图 图 7 **.14 :

-
要解决这个问题,你只需从你的 <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 } }正如你所见 现在 ,它工作正常:

然后,你可以点击 <st c="15769">spec.cy.ts</st>文件,如果一切顺利,你将得到这个:

总结
第九章:8
使用 Cypress 编写有效的端到端组件测试
-
结构化 端到端测试 -
编写端到端 测试用例 -
使用 Cypress 自定义命令
技术要求
-
Node.js 和 npm 已安装在你的 计算机上 -
全局安装的 Angular CLI 。 -
安装在你的 计算机上的代码编辑器,例如 Visual Studio Code
端到端测试的结构化
<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> <st c="2325">describe()</st> <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>

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>
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
});
});

“Writing test cases”
“Addition context”
“Adds two positive numbers correctly”
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');
});
<st c="8112">“'adds two positive”</st> <st c="8131">“numbers correctly'”</st>
<st c="8155">cy.visit()</st>
<st c="8300">'5'</st> <st c="8347">cy.get()</st> <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> <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>

正确地添加两个负数
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>
<st c="10565">'-5'</st> <st c="10613">cy.get()</st> <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> <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>

正确地添加一个正数和一个负数
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');
});
<st c="12612">it()</st> <st c="12769">'正确地添加两个正数'</st> <st c="12788">数字'</st>
<st c="12812">cy.visit()</st>
<st c="12968">'5'</st> <st c="13015">cy.get()</st> <st c="13078">first()</st> <st c="13173">type()</st>
<st c="13246">'+'</st> <st c="13349">'+'</st><st c="13358">select()</st> <st c="13430">should()</st>
<st c="13558">'-3'</st> <st c="13606">cy.get()</st> <st c="13660">last()</st> <st c="13753">type()</st>
<st c="13867">click()</st>
<st c="13992">'2'</st><st c="14027">should()</st>

减法上下文
正确减去两个正数
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');
});
<st c="15025">it()</st> <st c="15182">'subtracts two positive</st> <st c="15206">numbers correctly'</st>
<st c="15230">cy.visit()</st>
<st c="15386">'5'</st> <st c="15433">cy.get()</st> <st c="15496">first()</st> <st c="15591">type()</st>
<st c="15664">'-'</st> <st c="15767">'-'</st><st c="15784">命令用于从下拉菜单中选择一个选项。</st> <st c="15844">The</st>
<st c="15976">'3'</st> <st c="16023">cy.get()</st> <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>

正确减去两个负数
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>
<st c="17727">'-5'</st> <st c="17775">cy.get()</st> <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> <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>

正确减去一个正数和一个负数
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>
<st c="20130">'5'</st> <st c="20177">cy.get()</st> <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> <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>

乘法上下文
正确地乘以非零数和零
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');
});
<st c="22213">it()</st> <st c="22370">'乘以非零数和</st> <st c="22402">正确地乘以零'</st>
The</st>
<st c="22579">'5'</st> cy.get()first()type()
<st c="22857">'*'</st> <st c="22961">。 select()should()
<st c="23171">'0'</st> cy.get()last()type()
click()
<st c="23605">'0'</st><st c="23641">should()</st>

正确乘以两个正数
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>
<st c="24937">'5'</st> <st c="24984">cy.get()</st><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></st>
<st c="25515">'3'</st>
<st c="25965">'15'</st>

正确乘以两个负数
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');
});
<st c="27048">'正确乘以两个负</st> <st c="27073">数'</st>
<st c="27253">'-5'</st>
<st c="27534">'*'</st> <st c="27637">'*'</st>。</st> <st c="27654">命令用于从下拉菜单中选择一个选项。</st>
<st c="27847">'-3'</st> <st c="27895">cy.get()</st> <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>

正确乘以一个正数和一个负数
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> <st c="29553"> 这里是通向我们的计算器用户界面的 URL。
<st c="29666">'5'</st> <st c="29713">cy.get()</st> <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> <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>

除法上下文
将一个正非零数除以另一个正非零数
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');
});
<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>
<st c="32062">cy.visit()</st>
<st c="32218">'5'</st> <st c="32265">cy.get()</st> <st c="32328">first()</st> <st c="32423">type()</st>
<st c="32496">'/'</st> <st c="32599">'/'</st><st c="32608">select()</st> <st c="32680">should()</st>
<st c="32808">'2'</st> <st c="32855">cy.get()</st> <st c="32909">last()</st> <st c="33002">type()</st>
<st c="33116">click()</st>
<st c="33241">'2.5'</st><st c="33278">should()</st>
<st c="33333">在我们的浏览器中,我们有</st> <st c="33362">以下结果:</st>

<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>

将一个负的非零数除以另一个负的非零数
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>
<st c="37270">'-5'</st> <st c="37318">cy.get()</st> <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> <st c="37963">last()</st> <st c="38052">
<st c="38170">click()</st>
<st c="38295">'2.5'</st><st c="38332">should()</st>

将一个正非零数除以一个负非零数
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>
<st c="39880">'5'</st> <st c="39927">cy.get()</st> <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> <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>

将一个正非零数除以零
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');
});
<st c="41986">it()</st> <st c="42143">'将一个正非零数除以零'</st> `
<st c="42193">cy.visit()</st> <st c="42236">这里,它是通向我们的计算器用户界面的 URL。
<st c="42349">'5'</st> <st c="42392"><st c="42404">函数用于从 DOM 中获取元素。</st><st c="42459">first()</st> <st c="42550">
<st c="42627">'/'</st> <st c="42730">'/'</st><st c="42739">select()</st> <st c="42807">
<st c="42939">'0'</st> <st c="42986">cy.get()</st> <st c="43036"><st c="43046">函数用于获取相应元素集合中的最后一个元素。</st><st c="43133">type()</st>
<st c="43243">
<st c="43372">'Infinity'</st><st c="43410"><st c="43422">函数再次用于做出</st>
<st c="43497">这个结果:</st>

除以一个负的非零数除以零
在这个测试用例中,我们将看到如何
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');
});
<st c="44346">it()</st> <st c="44503">'除以一个负的非零数</st> <st c="44539">除以零'</st>
<st c="44553">cy.visit()</st> <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> <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> <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>

零除以零
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>
<st c="47085">'0'</st> <st c="47132">cy.get()</st> <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>
在我们的浏览器中,我们得到以下结果:

<st c="48357">图 8.19 – “除以零”端到端测试成功</st>
在下一节中,我们将探讨 Cypress 自定义命令是什么,以及如何使用它们使代码更容易维护和阅读。
<st c="48550">使用 Cypress 自定义命令</st>
要在 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">文件中添加一些代码,该文件位于 <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>。

摘要
<st c="52071">describe</st> <st c="52084">it</st>
<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>
<st c="53067">Cypress.Commands.add()</st>
第四部分:Angular 应用程序的持续集成和持续部署
-
第九章 , 理解持续集成和持续部署(CI/CD) -
第十章 , Angular TDD 的最佳实践和模式 -
第十一章 , 通过 TDD 重构和改进 Angular 代码
第十章:9
理解持续集成和持续部署(CI/CD)
-
理解持续集成和 持续部署 -
使用 GitHub Actions 设置 CI/CD 管道以自动化构建 -
使用 GitHub Actions 设置 CI/CD 管道以自动化测试 -
使用 GitHub Actions 设置 CI/CD 管道以自动化部署流程
技术要求
-
安装在你电脑上的 Node.js 和 npm -
全局安装 Angular CLI -
安装在你电脑上的代码编辑器,例如 Visual Studio Code
理解 CI 和 CD
什么是 CI?
持续集成(CI)对开发团队的好处
-
更快的迭代和问题解决 :持续集成使团队能够更频繁地集成代码更改,加快迭代速度并促进问题解决。 小的代码更改更容易管理,从而减少了可能出现的复杂问题的复杂性。 -
提高代码质量和减少错误 :通过频繁集成和测试代码,持续集成能够在开发周期早期识别并纠正错误。 结果是代码质量更高,缺陷更少,从而改善了用户体验并 减少了停机时间。 -
提高效率和降低成本 :由持续集成驱动的自动化减少了手动任务,为开发者节省了时间。 这不仅提高了效率,还降低了与手动测试和错误管理相关的成本。 因此,工程师可以有更多时间投入到 增值活动中。 -
提高透明度和协作 :持续集成通过提供对代码质量和集成问题的持续 反馈来促进透明度。 它还通过确保代码更改定期集成和测试来促进更好的团队协作,从而在团队成员之间实现更好的协调。 -
更快的上市时间 :通过自动化构建、测试和部署流程,持续集成(CI)使团队能够更快地将新特性和更新交付给最终用户。 这种响应性使开发团队能够保持竞争力,并确保客户能够从 最新的增强功能中受益。 -
提高客户满意度 :更少的错误和缺陷最终进入生产,改善了用户体验。 持续集成还使团队能够快速响应客户反馈,使团队能够更高效地进行调整和改进 。 -
缩短平均故障恢复时间(MTTR) :持续集成使问题能够更快地被检测和解决,从而降低 MTTR。 这确保了软件的稳定性和可靠性, 最小化了停机时间。 -
提高测试可靠性 :在持续集成框架内进行持续测试,通过允许执行更精确的测试来提高测试可靠性。 这确保了软件经过彻底测试并准备好投入生产,提高了对 软件质量}的信心。 -
竞争优势 :采用 商业智能 ( BI ) 的组织具有竞争优势 ,因为它们可以更快地部署功能,从而节省资金。 这种早期反馈和自动化有助于缩短交货期、部署频率和变更失败率,进而提高 业务成果。 -
团队内部提高透明度和问责制 :CI/CD 实践提高了团队内部的透明度和问责制,使问题能够迅速识别和解决,包括施工失败和架构挫折。 这种持续反馈循环提高了整体 产品质量。
持续集成(CI)实施的关键原则
-
自动化一切 :持续集成关注自动化构建、测试和集成过程。 自动化减少了人工工作量,最小化了错误,并加速了 开发周期。 -
频繁集成 :频繁地将代码更改集成到共享仓库中,理想情况下每天几次。 这种做法可以使集成问题在开发周期的早期就被识别和解决。 -
使构建过程快速 :构建过程应该尽可能快,以确保快速反馈。 快速构建意味着问题可以更快地被发现和解决,从而促进 持续改进。 -
即时反馈 :持续集成依赖于自动化构建和测试的即时反馈。 这种反馈对于在开发过程的早期识别和解决问题至关重要。 -
从小处着手,逐步发展 :从简单的持续集成配置开始,根据需要逐步添加其他工具和实践。 这种方法鼓励灵活性和实验,使团队能够在其 特定环境中找到最佳方案。 -
定义成功指标 :明确定义持续集成过程的成功指标,例如加速代码构建或降低错误和工作率。 使用这些指标来衡量持续集成实践的有效性,并 指导改进。 -
文档 :记录持续集成过程和所有开发人员和利益相关者使用的工具。 良好的文档确保每个人都了解如何 为持续集成过程做出贡献并高效地解决 问题。 -
运维和开发之间的协作 :鼓励运维和开发紧密协作的文化。 这种协作对于从两个角度理解软件可靠性和性能至关重要。 -
可扩展性 :持续集成通过自动化代码集成和沟通,打破了增长障碍,使组织能够扩展其开发团队、代码库和 基础设施。 -
学习曲线的投资 :成功实施持续集成涉及在版本控制和自动化等领域学习新技能。 然而,这些技能很容易获得,持续集成的益处超过了 初始投资。
什么是 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)实施的关键原则
-
构建质量 :这一原则强调从一开始就将质量构建到产品中,而不是依赖于检查来实现。 它涉及创建和演变反馈循环,以便在问题被记录在版本控制系统之前尽早发现。 应使用自动化测试在问题恶化之前检测缺陷。 自动化测试应在问题恶化之前检测缺陷。 随着时间的推移,应使用自动化测试来检测缺陷。 -
小批量工作 :持续交付(CD)鼓励使用小而可管理的变更,而不是大而稀少的发布。 这种方法减少了获取反馈所需的时间,促进了问题的识别和解决,并提高了效率和动力。 目标是改变软件交付的经济性,使其能够在小批量中工作。 -
计算机执行重复性任务,人类解决问题 :这一原则强调了自动化重复性任务,如回归测试的重要性,以便人类可以专注于解决问题。 目标是创造一种平衡,其中计算机处理简单、重复的任务,而人类处理更复杂、更具创造性的任务。 -
持续改进 :持续交付(CD)推崇持续改进的理念,或称为 精益运动 **中的 kaizen **,。这关乎将改进工作视为日常工作的重要组成部分,并不断努力使事物变得更好。 这关乎不满足于现状,并始终寻找改进的机会。 -
人人有责 :在成功的组织中,每个人都对其构建的软件的质量和稳定性负责。 这一原则鼓励一种协作方法,其中开发人员、运营团队和其他利益相关者共同努力实现组织的目标,而不是优化他们自己团队的成功。 它强调了基于客户反馈和组织影响的快速反馈循环的重要性。
使用 GitHub Actions 设置 CI/CD 管道以自动化构建
步骤 1 – 创建或选择一个仓库和项目

步骤 2 – 在您的项目仓库中打开 GitHub Actions

步骤 3 – 定义您的 CI/CD 工作流程
<st c="22144">node</st>



<st c="23765">angular-tdd.yml</st>

name<st c="24004">Node.js CI</st><st c="24030">Angular</st> <st c="24038">TDD CI/CD</st>

<st c="24127">–version: [14.x, 16.x, 18.x]</st> <st c="24170">node-version: [18.x]</st>

<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
-
<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 设置 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>

<st c="32498">npm run</st> <st c="32524">ng</st>

<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"
}
}
}
<st c="35290">src</st> <st c="35264">karma.conf.js</st>
// 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
<st c="36704">npm run test –if-present</st> <st c="36734">npm run test -- --configuration=ci</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 -- --configuration=ci
- run: npm run build --if-present
使用 GitHub Actions 自动部署流程的 CI/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]"
-
<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>: 文件将上传到的远程服务器上的目标路径。
摘要
总结来说,本章涵盖了 SDLC 中 CI 和 CD 实践的基本概念,强调了它们的重要性及益处。
然后,CD 被介绍为 CI 的扩展,专注于自动化部署过程,同时确保软件始终处于可发布状态。
本章还探讨了使用 GitHub Actions 设置 CI/CD 管道的实际方面,GitHub Actions 是一个流行的流程自动化工具。
连续流程的关键概念和实践被考察,包括进行小而迭代的变更的重要性、采用主干开发、维护快速构建和测试阶段,以及将部署与生产发布解耦。
此外,本章讨论了测试在 CI/CD 流程中的作用,强调了不同类型测试的重要性,如冒烟测试、单元测试、集成测试、系统测试和验收测试。
在下一章中,我们将学习关于测试驱动开发的最佳实践和模式。
第十一章:10
Angular TDD 的最佳实践和模式
-
Angular 项目中 TDD 的最佳实践 -
任何 Angular 项目中实现 TDD 的模式探索 -
为您的 Angular 项目选择正确的 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 项目中实施 TDD 的模式
-
单元测试 :此模式侧重于测试 单独的组件或服务。 这是至关重要的,因为它确保您的应用程序的每个部分在独立于系统其他部分的情况下按预期工作。 。 -
组件测试 :Angular 应用程序是围绕组件构建的 ,使它们成为用户界面的重要组成部分。 在 Angular 项目中实施 TDD 时,从测试组件开始可以确保它们正确渲染,处理用户交互,并按预期更新 UI 。 -
服务测试 :Angular 中的服务封装 业务逻辑和数据操作功能。 为服务编写测试确保它们执行预期的功能,正确与外部资源交互,并优雅地处理 错误。 -
集成测试 :此模式涉及测试 应用程序不同部分之间的交互,例如组件和服务。 它有助于识别当应用程序的不同部分协同工作时可能出现的潜在问题。 -
端到端 (E2E) 测试 :此模式模拟用户 在实际场景中与应用程序的交互,测试从开始到结束的整个应用程序流程。 这是至关重要的,因为它确保应用程序从用户的角度来看表现如预期。
为您的 Angular 项目选择 TDD 模式
-
项目复杂性 :对于具有众多组件和服务的复杂应用程序,可能需要结合单元、集成和端到端测试模式。 这种方法确保了应用程序的每个部分在隔离状态下以及在整个应用程序的上下文中都得到了彻底的测试。 整个应用程序。 -
团队专长 :团队对 TDD 实践以及可用于 Angular 的特定测试工具和框架的熟悉程度会影响 TDD 模式的选择。 例如,Angular 提供了强大的测试工具和库,便于进行单元和集成测试。 -
项目需求 :您项目的具体需求,如性能、安全性和用户体验,也可以指导 TDD 模式的选择。 例如,端到端测试对于需要高度用户交互和真实世界测试的项目特别有用。
摘要
关键课程包括 TDD 周期及其红-绿-重构阶段,以及如
在下一章中,我们将学习如何通过 TDD(测试驱动开发)来重构和改进 Angular 代码。
第十二章:11
通过 TDD 重构和改进 Angular 代码
在本节中,我们深入探讨采用“先测试”策略的重要性,探讨重构过程中的 TDD 优势,选择要创建的最佳测试,理解
-
通过 TDD 重构 Angular 代码 -
在 Angular 应用程序中识别代码异味和改进区域 -
迭代改进 – 用于持续代码增强的红色-绿色-重构周期
技术要求
要跟随本章的示例和练习,你需要对 Angular 和 TypeScript 有基本的了解,以及以下技术要求:
-
在您的计算机上安装 Node.js 和 npm -
全局安装 Angular CLI -
在您的计算机上安装代码编辑器,例如 Visual Studio Code
本章所需代码文件可以在
使用 TDD 重构 Angular 代码
测试优先方法的力量
-
红色状态 :你首先为想要重构的代码中的特定功能编写一个测试。这个初始测试很可能会失败,表明期望的行为尚未实现。这个红色 状态作为一个 起点。 -
绿色状态 :以失败的测试为指南,你只需编写足够的代码来使测试通过。这种初始实现可能很基础,但重点是确保它准确反映了测试定义的预期行为。 现在,测试处于 绿色 状态,表示 成功实现。 -
重构 :这里是魔法发生的地方。有了通过测试的安全网,你现在可以重构代码,以提高其可读性、可维护性和效率。这可能包括以下内容:以下内容: -
将长方法分解为更小、 定义良好的函数 -
从大型组件中提取可重用组件或服务 -
应用设计模式以实现更好的 代码组织 -
简化逻辑以 增强清晰度
-
<st c="3768">calculator.component.ts</st> <st c="3809">第九章</st>
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);
});

<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;
}
}

重构中 TDD 的好处
-
增加信心 :通过测试,你可以作为一个安全网,让你在不用担心破坏现有功能的情况下尝试不同的重构技术。 这增强了你在过程中的信心。 -
改进设计 :首先考虑测试可以鼓励你编写具有良好定义功能的模块化代码。 这导致代码在长期运行中更加干净和易于维护。 -
增强可维护性
:一个全面的测试套件成为代码预期行为的活文档。 这简化了未来的修改和错误修复,因为你可以依赖测试来 捕获回归。 -
更好的代码覆盖率 :TDD 自然会鼓励你专注于用测试覆盖各种代码路径。 这导致更健壮的应用程序,隐藏的 bug 更少。
行动中的示例
-
重构长服务方法 :想象一个负责获取和处理大量数据的服务方法。 你可以编写一个专注于数据处理逻辑特定方面的测试。 最初,这个测试会失败。 然后,你会重构服务方法,将逻辑提取到单独的、经过良好测试的函数中。 这提高了代码的可读性和可维护性,同时测试确保核心功能保持完整。 -
转换神组件 :“神组件”指的是一个过于复杂的组件,通常通过在自身内部处理过多的责任或功能来违反单一职责原则。 这个术语用来强调组件变得过大且难以管理、测试和理解的负面方面。 这类组件往往注入许多服务,执行多项任务,并可能导致重大的维护挑战,随着时间的推移。 这种执行大量任务的组件可以通过创建专门针对特定功能的服务来重新设计。 因此,可以编写测试来验证重构组件和新 创建的服务的行为。
选择要编写的正确测试
-
关注痛点 :从导致代码库中出现问题(如易出错或难以理解的区域)的功能开始。 这些区域通常会导致代码难以维护、测试和理解。 -
从小处着手 :从较小、定义良好的测试开始,这些测试针对特定的功能。 这允许更快地通过 红-绿-重构周期。 -
测试集成点 :当重构与服务或其他组件交互的组件时,编写测试以验证这些交互以及组件本身。
在 Angular 应用程序中识别代码异味和改进区域
什么是代码异味?
为什么我们应该关注 Angular 中的代码异味呢?
-
降低可维护性 :随着时间的推移,有异味的代码变得难以理解和修改。 随着您的应用程序的增长和功能的添加,错综复杂的代码的复杂性可能会使更改变得繁琐并 容易出错。 -
增加调试时间 :当出现有异味的代码中的错误时,确定根本原因可能具有挑战性。 缺乏清晰的结构和组织使得寻找错误就像在 haystack 中寻找一根针一样,浪费了宝贵的 开发者时间。 -
降低团队生产力 :与有异味的代码一起工作可能会让开发者感到沮丧和缺乏动力。 解读错综复杂的逻辑的认知负担会减慢开发速度并 阻碍协作。 -
技术债务 :随着时间的推移,未处理的代码异味会积累,最终形成需要解决的技术债务。 这种债务可能成为一个重大的负担,需要专门的资源,并可能延迟新 功能开发。
-
提高代码可读性 :干净且结构良好的代码更容易被您和其他在项目上工作的开发者理解。 这减少了新团队成员的入职时间,并促进了 更好的协作。 -
增强可维护性 :重构后的代码更容易修改和适应,因为您的 应用程序需求发生变化。 这使您能够更有效地引入新功能和错误修复 。 -
减少调试时间 :清晰关注点分离的干净代码在出现错误时更容易隔离和修复。 当出现错误时。 -
提高团队生产力 :与结构良好的代码一起工作可以改善开发者的体验和满意度。 这导致更高的生产力和更积极 的开发环境。 -
最小化技术债务 :通过早期解决代码异味,您可以防止它们积累并成为未来的一大负担。 。
识别 Angular 应用程序中最常见的代码异味
-
漫长而曲折的方法 :想象一下你的服务中的一个方法,它跨越了数十行,处理各种任务。 这是一个长方法的典型例子,一个表明缺乏模块化的代码异味。 这些方法可能难以理解、测试和修改。 重构涉及将这些巨无霸分解成更小、更明确的函数,每个函数都专注于特定的任务。 这提高了代码的可读性 和可维护性。 -
神组件 :你是否遇到过责任过重的组件? 这是一个“神组件”,从数据获取到复杂的 UI 逻辑都由它处理。 这样的组件成为维护噩梦,因为一个区域的变化可能会在整个组件中产生连锁反应,导致 意外的后果。 重构可能包括 以下内容: -
创建专用服务 :将与数据访问、业务逻辑或计算相关的功能提取到单独的服务中。 这些服务可以被多个组件重用 ,促进 更好的组织。 -
拆分组件 :将神组件拆分成更小、更专注的组件,每个组件处理 UI 或功能的一个特定方面。
-
-
<st c="19648">10</st>用于分页,但其目的不明确。 重构涉及用命名变量或常量替换这些魔法数字。 例如,使用 <st c="19802">ITEMS_PER_PAGE</st>而不是 <st c="19828">10</st>,使代码更具自文档性,更容易 理解。 -
意大利面代码迷宫 :想象一下蜿蜒曲折、缺乏清晰结构和组织的代码。 这就是意大利面代码,使其在导航、理解和修改时具有挑战性。 测试驱动开发(TDD)可以成为对抗意大利面代码的有力工具。 通过首先编写测试,然后重构代码以满足这些测试,你可以引入结构并改善代码库的整体组织。
在下一节中,我们将学习迭代改进:持续代码增强的红色-绿色-重构循环。
迭代改进 – 持续代码增强的红色-绿色-重构循环
在 Angular 应用程序中重构现有代码可能是一项艰巨的任务。
想象你是一位正在处理一块大块大理石的雕塑家。
红色 – 使用失败的测试设定舞台
-
确定重构目标 :首先确定 Angular 应用程序中一个特定的区域,该区域表现出代码异味或需要改进。 这可能是服务中的长方法、处理许多任务的上帝组件,或者重复的代码片段。 -
定义预期结果 :清楚地概述重构后的代码应该做什么。 它应该处理哪些数据? 它应该如何与 UI 交互? 编写一个反映这种预期行为的测试。 记住,这个测试最初会失败,因为所需的功能尚未实现。
绿色——用最少的代码使测试通过
-
简单实现 :你最初编写的代码以使测试通过可能是基本的。 它不必是最有效或结构良好的解决方案。 优先级是让测试通过并建立重构的基线 。 -
关注功能 :确保你编写的代码满足测试中概述的特定行为。 在这个阶段,不要引入不必要的功能或 逻辑。
重构——自信地转换代码
-
模块化长方法 :将那些长而单一的方法分解成更小、更明确的函数。 这增强了代码的可读性,并使理解逻辑流程更容易。 逻辑流程。 -
提取可重用组件和服务 :如果你的组件已经变成了处理众多任务的上帝组件,考虑将功能提取到专门的服务或可重用组件中。 这促进了更好的组织和关注点的分离 。 -
消除重复 :识别并重构重复的代码片段到可重用的组件、服务或实用函数中。 这减少了代码冗余并 简化了维护。 -
应用设计模式 :考虑整合促进代码结构和组织更好的设计模式。 这可以使你的代码更易于维护,并且更容易被其他开发者理解。 其他开发者。 -
简化逻辑 :寻找机会简化复杂的逻辑,并增强代码的清晰度。 这可能包括使用更具描述性的变量名,分解复杂的条件语句,或利用 辅助函数。
在整个重构过程中,牢记绿色测试。
摘要
总结来说,通过 TDD 重构和改进 Angular 代码是一种强大的方法,可以提升 Angular 应用程序的质量、可维护性和效率。
此外,TDD(测试驱动开发)促进了独立、可测试的代码单元的开发,使得在开发早期阶段更容易识别和修复问题。
此外,TDD 通过鼓励开发者编写清晰、简洁且文档齐全的测试,促进了高质量、可维护的代码的开发。
在 Angular 的背景下,TDD 在开发服务、组件和管道方面特别有效,如提供的示例所示。
总结来说,通过 TDD 重构和改进 Angular 代码是一种有价值的实践,可以显著提升 Angular 应用程序的质量。


浙公网安备 33010602011771号