Angular 测试原则

学习目标

  • 促进测试的 Angular 架构原则

  • 标准和替代测试工具

  • 使用 Karma 和 Jasmine 运行和配置单元测试和集成测试

可测试性

与其他流行的前端 JavaScript 库相比,Angular 是一个有意见的、全面的框架,涵盖了开发 JavaScript Web 应用程序的所有重要方面。Angular 提供了高级结构、低级构建块和将所有内容打包成可用应用程序的手段。

可测试的架构

没有考虑自动化测试,就无法理解 Angular 的复杂性。为什么 Angular 应用程序被结构化为组件、服务、模块等?为什么这些部分以这种方式交织在一起?为什么 Angular 应用程序的所有部分都采用相同的模式?

一个重要的原因是 可测试性。Angular 的架构保证了所有应用程序部分可以以类似的方式轻松进行测试。

良好结构化的代码

我们从经验中得知,易于测试的代码也更简单、结构更好、更易于阅读和理解。编写可测试代码的主要技术是将代码分解为更小的块,每个块“做一件事并做好”。然后松耦合这些块。

依赖注入和模拟

一种实现松耦合的主要设计模式是 依赖注入 和底层的 控制反转。应用程序部分不再自己创建依赖关系,而只是声明依赖关系。创建和提供依赖关系的繁琐任务被委托给位于顶部的 注入器

这种工作分工将应用程序部分与其依赖关系解耦:一个部分不需要知道如何设置依赖关系,更不用说依赖关系的依赖关系等等。

松耦合

依赖注入将紧耦合变成了松耦合。某个应用程序部分不再依赖于特定的类、函数、对象或其他值,而是依赖于一个可以被交换为具体实现的抽象标记。注入器接受标记并将其交换为真实的值。

原始或模拟

这对于自动化测试非常重要。在我们的测试中,我们可以决定如何处理依赖关系:

  • 我们可以提供一个 原始的、完全功能的实现。在这种情况下,我们编写的是包括直接和间接依赖关系的 集成测试

  • 或者我们提供一个没有副作用的 模拟 实现。在这种情况下,我们编写的是试图在 隔离环境 中测试应用程序部分的 单元测试

编写测试时,花费的大部分时间都用于将应用程序部分与其依赖关系解耦。本指南将教你如何设置测试环境,将应用程序部分隔离开,并重新连接等效的模拟对象。

测试工具

Angular提供了强大的测试工具。当使用命令行界面创建Angular项目时,它会自带一个完整的测试设置,用于单元测试、集成测试和端到端测试。

平衡的默认设置

Angular团队已经为你做出了决策:使用 Jasmine 作为测试框架,使用 Karma 作为测试运行器。实现代码和测试代码使用 Webpack 进行打包。应用程序部分通常在Angular的 TestBed 中进行测试。

这个设置是一种权衡,有其优势和劣势。由于这只是测试Angular应用程序的一种可能方式,你可以自己编译自己的测试工具链。

其他选择

例如,一些Angular开发者使用 Jest 而不是Jasmine和Karma。一些开发者使用 SpectatorAngular Testing Library,而不是直接使用 TestBed

这些替代方案并不是更好或更差,它们只是做出不同的权衡。本指南在单元测试和集成测试中使用Jasmine和Karma。稍后,你将学习有关Spectator的内容。

一旦你达到了特定设置的限制,你应该调查一下是否有其他替代方案可以使你的应用程序测试更容易、更快速和更可靠。

测试约定

Angular提供了一些关于测试的工具和约定。从设计上讲,它们足够灵活,以支持不同的测试方式。因此,你需要决定如何应用它们。

做出选择

这种选择的自由使得专家受益,但对初学者来说会感到困惑。在你的项目中,应该有一种首选的方式来测试特定的应用程序部分。你应该做出选择并建立项目范围的约定和模式。

将约定转化为代码

Angular附带的测试工具是低级别的工具,它们仅提供基本操作。如果直接使用这些工具,你的测试会变得混乱、重复且难以维护。

因此,你应该创建 高级别的测试工具,将你的约定转化为代码,以便编写简短、可读和可理解的测试。

本指南重视强大的约定,并引入了将这些约定编码为辅助函数的工具。当然,这些工具可能因人而异。你可以根据自己的需求调整这些工具,或者构建其他的测试辅助工具。

运行单元测试和集成测试

Angular命令行界面(CLI)允许您运行单元测试、集成测试和端到端测试。如果您尚未安装CLI或需要更新到最新版本,请在命令行中运行以下命令:

npm install -g @angular/cli

这将全局安装 Angular CLI,以便可以在任何地方使用 ng 命令。ng 本身并不做任何操作,只是提供了一些特定于 Angular 的命令。

例如, ng new 用于创建一个新的Angular项目目录,其中包含一个可用的应用程序脚手架。 ng serve 用于启动开发服务器, ng build 用于构建项目。

运行单元测试和集成测试的命令是:

ng test

首先,该命令会查找目录树中与 .spec.ts 模式匹配的所有文件。使用Webpack,它将它们与其依赖项一起编译成一个JavaScript bundle。该bundle的代码还会初始化Angular的测试环境 - TestBed

通常,Angular应用程序会加载并启动一个 AppModule。这个启动过程被称为引导(bootstrapping)。然后, AppModule 会导入其他模块、组件、服务等。这样,打包工具可以找到应用程序的所有部分。

但是测试bundle的工作方式不同。它不会从一个模块开始,然后遍历其依赖项。它只是导入所有以 .spec.ts 结尾的文件。

.spec.ts

每个 .spec.ts 文件代表一个测试。通常,一个 .spec.ts 文件包含至少一个Jasmine测试套件(在下一章中会详细介绍)。.spec.ts 文件位于与实现代码相同的目录中。

在我们的示例应用程序中,CounterComponent 位于 src/app/components/counter/counter.component.ts 文件中。相应的测试文件位于 src/app/components/counter/counter.component.spec.ts。这是Angular的约定,并非技术上的必要性,我们将坚持使用这种约定。

Karma

其次,ng test 命令启动Karma测试运行器。Karma会在 http://localhost:9876/ 启动一个开发服务器,用于提供Webpack编译的JavaScript bundle。

Karma然后启动一个或多个浏览器。Karma的思想是在不同的浏览器中运行相同的测试,以确保跨浏览器的互操作性。支持所有广泛使用的浏览器:Chrome、Internet Explorer、Edge、Firefox和Safari。默认情况下,Karma会启动Chrome浏览器。

测试运行器

启动的浏览器导航到 http://localhost:9876/。正如前面提到的,该网站提供测试运行器和测试bundle。测试会立即开始运行。您可以在浏览器和终端中跟踪进度并查看结果。

在运行 counter项目 的测试时,浏览器的输出如下所示:

karma success

这是终端的输出:

INFO [karma-server]: Karma v5.0.7 server started at http://0.0.0.0:9876/
INFO [launcher]: Launching browsers Chrome with concurrency unlimited
INFO [launcher]: Starting browser Chrome
WARN [karma]: No captured browser, open http://localhost:9876/
INFO [Chrome 84.0.4147.135 (Mac OS 10.15.6)]: Connected on socket yH0-wtoVtflRWMoWAAAA with id 76614320
Chrome 84.0.4147.135 (Mac OS 10.15.6): Executed 46 of 46 SUCCESS (0.394 secs / 0.329 secs)
TOTAL: 46 SUCCESS

Webpack会监视 .spec.ts 文件以及它们导入的文件的变化。当你修改实现代码(例如 counter.component.ts )或测试代码(例如 counter.component.spec.ts )时,Webpack会自动重新编译bundle并推送到打开的浏览器中。所有的测试将重新启动。

红绿循环

这个反馈循环允许你同时处理实现代码和测试代码。这对于测试驱动开发非常重要。你修改实现代码,并期望测试失败 - 测试处于“红色”状态。然后你调整测试使其再次通过 - 测试处于“绿色”状态。或者你首先编写一个失败的测试,然后调整实现直到测试通过。

测试驱动开发意味着让红绿循环指导你的开发过程。

配置Karma和Jasmine

Karma和Jasmine的配置文件位于项目根目录下的 karma.conf.js 文件中。自Angular 15开始,Angular CLI不会默认创建这个文件。如果它不存在,你可以使用以下终端命令创建它:

ng generate config karma

有许多配置选项和插件可用,所以我们只会介绍其中几个。

启动器

如前所述,标准配置在Chrome浏览器中运行测试。要在其他浏览器中运行测试,我们需要安装不同的 启动器

每个启动器都需要在 plugins 数组中加载:

plugins: [
  require('karma-jasmine'),
  require('karma-chrome-launcher'),
  require('karma-jasmine-html-reporter'),
  require('karma-coverage'),
  require('@angular-devkit/build-angular/plugins/karma')
],

已经有一个启动器,karma-chrome-launcher。这是一个npm包。

要安装其他启动器,我们首先需要安装相应的npm包。让我们安装Firefox启动器。运行以下终端命令:

npm install --save-dev karma-firefox-launcher

然后在 karma.conf.js 文件中引入该包:

plugins: [
  require('karma-jasmine'),
  require('karma-chrome-launcher'),
  require('karma-firefox-launcher'),
  require('karma-jasmine-html-reporter'),
  require('karma-coverage'),
  require('@angular-devkit/build-angular/plugins/karma'),
],

为了在 Firefox 中运行测试,我们需要将 Firefox 添加到浏览器列表中:browsers: ['Chrome'] 变为 browsers: ['Chrome', 'Firefox']

现在,Karma 将启动两个浏览器并并行运行测试。

报告器

Karma 的另一个重要概念是 报告器。它们用于格式化和输出测试结果。在默认配置中,有三个报告器处于活动状态:

  1. 内置的 进度 报告器在 shell 上输出文本。在测试运行时,它输出测试进度的信息,例如:

    Chrome 84.0.4147.135 (Mac OS 10.15.6): Executed 9 of 46 SUCCESS (0.278 secs / 0.219 secs)

    最后的输出为:

    Chrome 84.0.4147.135 (Mac OS 10.15.6): Executed 46 of 46 SUCCESS (0.394 secs / 0.329 secs)<br> TOTAL: 46 SUCCESS

  2. 标准的 HTML 报告器 kjhtml(npm 包: karma-jasmine-html-reporter)在浏览器中呈现结果。+

    karma jasmine html reporter
  3. 代码覆盖率报告器(npm 包: karma-coverage)会生成测试覆盖率报告。详见 测量代码覆盖率

通过编辑 reporters 数组,您可以添加报告器或替换现有的报告器:

reporters: ['progress', 'kjhtml'],

例如,要添加一个创建 JUnit XML 报告的报告器,请先安装 npm 包:

npm install --save-dev karma-junit-reporter

然后,将其作为插件进行引用:

plugins: [
  require('karma-jasmine'),
  require('karma-chrome-launcher'),
  require('karma-jasmine-html-reporter'),
  require('karma-coverage'),
  require('karma-junit-reporter'),
  require('@angular-devkit/build-angular/plugins/karma'),
],

最后,在添加这个 reporter:

reporters: ['progress', 'kjhtml', 'junit'],

运行 ng test 后,你会在项目目录中找到一个 XML 报告文件。

Jasmine 配置

Jasmine 适配器的配置位于 client 对象内的 jasmine 属性中:

client: {
  jasmine: {
    // 在这里可以添加 Jasmine 的配置选项
    // 可能的选项列表在 https://jasmine.github.io/api/edge/Configuration.html 中列出
    // 例如,你可以通过 `random: false` 来禁用随机执行
    // 或者通过 `seed: 4321` 来设置一个特定的种子值
  },
  clearContext: false // leave Jasmine Spec Runner output visible in browser
},

本指南建议激活一个有用的 Jasmine 配置选项:failSpecWithNoExpectations。如果测试中没有至少一个期望(expectation),则会导致测试失败(有关期望的更多内容稍后会详细介绍)。在几乎所有情况下,没有期望的规范往往源于测试代码的错误。

client: {
  jasmine: {
    failSpecWithNoExpectations: true,
  },
  clearContext: false // leave Jasmine Spec Runner output visible in browser
},