block includes include ../_util-fns - var _JavaScript = 'JavaScript'; //- Double underscore means don't escape var, use !{__var}. - var __chaining_op = '; or ,'; - var __new_op = 'new'; - var __objectAsMap = 'object'; :marked This chapter offers tips and techniques for testing Angular applications. Along the way you will learn some general testing principles and techniques but the focus is on testing applications written with Angular 本章提供了一些测试Angular应用的提示和技巧。虽然这里讲述了一些常规测试理念和技巧,但是其重点是测试用Angular编写的应用。 #top :marked # Table of Contents # 目录 1. [Introduction to Angular Testing](#testing-intro) 1. [Angular测试入门](#testing-intro)
1. [Setup](#setup) 1. [搭建测试环境](#setup) - [setup files](#setup-files): `karma.conf`, `karma-test-shim`, `systemjs.config` - [文件配置](#setup-files): `karma.conf`, `karma-test-shim`, `systemjs.config` - [npm packages](#npm-packages) - [npm 包](#npm-packages) 1. [The first karma test](#1st-karma-test) 1. [第一个Karma测试](#1st-karma-test)
1. [Introduction to the Angular testing utilities](#atu-intro) 1. [Angular测试工具](#atu-intro)
1. [The sample application and its tests](#sample-app) 1. [例子应用及其测试](#sample-app)
1. [A simple component test](#simple-component-test) 1. [一个简单的组件测试](#simple-component-test) - [_configureTestingModule_](#configure-testing-module) - [_configureTestingModule_](#configure-testing-module) - [_createComponent_](#create-component) - [_createComponent_](#create-component) - [_ComponentFixture_, _DebugElement_, _query(By.css)_](#component-fixture) - [_ComponentFixture_, _DebugElement_, _query(By.css)_](#component-fixture) - [_detectChanges_](#detect-changes) - [_detectChanges_](#detect-changes) - [_autoDetectChanges_](#auto-detect-changes) - [_autoDetectChanges_](#auto-detect-changes) 1. [Test a component with a service dependency](#component-with-dependency) 1. [测试拥有服务依赖的组件](#component-with-dependency) - [test doubles](#service-test-doubles) - [测试复制品](#service-test-doubles) - [get the injected service](#get-injected-service) - [获取注入的服务](#get-injected-service) - [_TestBed.get_](#testbed-get) - [_TestBed.get_](#testbed-get) 1. [Test a component with an async service](#component-with-async-service) 1. [测试拥有异步服务的组件](#component-with-async-service) - [spies](#service-spy) - [spies](#service-spy) - [_async_](#async) - [_async_](#async) - [_whenStable_](#when-stable) - [_whenStable_](#when-stable) - [_fakeAsync_](#async) - [_fakeAsync_](#async) - [_tick_](#tick) - [_tick_](#tick) - [_jasmine.done_](#jasmine-done) - [_jasmine.done_](#jasmine-done) 1. [Test a component with an external template](#component-with-external-template) 1. [测试拥有外部模板的组件](#component-with-external-template) - [_async_](#async-in-before-each) in `beforeEach` - [_async_](#async-in-before-each) in `beforeEach` - [_compileComponents_](#compile-components) - [_compileComponents_](#compile-components) 1. [Test a component with inputs and outputs](#component-with-inputs-output) 1. [测试拥有导入(inputs)和导出(outputs)的组件](#component-with-inputs-output) - [_triggerEventHandler_](#trigger-event-handler) - [_triggerEventHandler_](#trigger-event-handler) 1. [Test a component inside a test host component](#component-inside-test-host) 1. [在宿主组件中测试组件](#component-inside-test-host)
1. [Test a routed component](#routed-component) 1. [测试路由组件](#routed-component) - [_inject_](#inject) - [_inject_](#inject) 1. [Test a routed component with parameters](#routed-component-w-param) 1. [测试一个带有参数的被路由的组件](#routed-component-w-param) - [_Observable_ test double](#stub-observable) - [**可观察**测试复制品](#stub-observable) 1. [Use a _page_ object to simplify setup](#page-object) 1. [使用**page**对象来简化配置](#page-object)
1. [Setup with module imports](#import-module) 1. [用模块的导入进行配置](#import-module)
1. [Override component providers](#component-override) 1. [替换组件提供商](#component-override)
1. [Test a _RouterOutlet_ component](#router-outlet-component) 1. [测试一个**RouterOutlet**组件](#router-outlet-component) - [stubbing unneeded components](#stub-component) - [伪造不需要的组件](#stub-component) - [Stubbing the _RouterLink_](#router-link-stub) - [伪造_RouterLink_](#router-link-stub) - [_By.directive_ and injected directives](#by-directive) - [_By.directive_和注入的指令](#by-directive) 1. ["Shallow" component tests with *NO\_ERRORS\_SCHEMA*](#shallow-component-test) 1. [使用*NO\_ERRORS\_SCHEMA*来“浅化”组件测试](#shallow-component-test)
1. [Test an attribute directive](#attribute-directive) 1. [测试属性指令](#attribute-directive)
1. [Isolated unit tests](#isolated-unit-tests "Unit testing without the Angular testing utilities") 1. [隔离的单元测试](#isolated-unit-tests "Unit testing without the Angular testing utilities") - [服务](#isolated-service-tests) - [管道](#isolated-pipe-tests) - [组件](#isolated-component-tests) 1. [Angular testing utility APIs](#atu-apis) 1. [Angular测试工具APIs](#atu-apis) - [Stand-alone functions](#atu-apis): `async`, `fakeAsync`, etc. - [独立函数](#atu-apis): `async`, `fakeAsync`, etc. - [_TestBed_](#testbed-class-summary) - [_TestBed_](#testbed-class-summary) - [_ComponentFixture_](#component-fixture-class-summary) - [_ComponentFixture_](#component-fixture-class-summary) - [_DebugElement_](#debug-element-details) - [_DebugElement_](#debug-element-details) 1. [FAQ](#faq "Frequently asked questions") 1. [常见问题](#faq "Frequently asked questions") :marked It’s a big agenda. Fortunately, you can learn a little bit at a time and put each lesson to use. 以上主题繁多。幸运的是,你可以慢慢地阅读并立刻应用每一个主题。 # 在线例子 The chapter sample code is available as live examples for inspection, experiment, and download. 本章所有例子代码都在下面的在线例子中,以供参考、实验和下载。 * 例子应用 * 第一个测试规格 * 完整的应用测试规格 * 示范规格包 a(href="#top").to-top Back to top a(href="#top").to-top 回到顶部 .l-hr #testing-intro :marked # Introduction to Angular Testing # Angular测试入门 You write tests to explore and confirm the behavior of the application. 编写测试来探索和确认应用的行为。 1. They **guard** against changes that break existing code (“regressions”). 1. 测试**守护**由于代码变化而打破已有代码(“回归”)的情况。 1. They **clarify** what the code does both when used as intended and when faced with deviant conditions. 1. 不管代码被正确使用还是错误使用,测试起到**澄清**代码的作用。 1. They **reveal** mistakes in design and implementation. Tests shine a harsh light on the code from many angles. When a part of the application seems hard to test, the root cause is often a design flaw, something to cure now rather than later when it becomes expensive to fix. 1. 测试**暴露**设计和实现可能出现的错误。测试从很多角度为代码亮出警报灯。当应用程序很难被测试时, 根本原因一般都是设计缺陷,这种缺陷最好立刻将其修正,不要等到它变得很难被修复的时候才行动。 This chapter assumes that you know something about testing. Don't worry if you don't. There are plenty of books and online resources to get up to speed. 本章假设你熟悉测试。但是如果你不熟悉也没有关系。有很多书本和在线资源可以帮助你。 ## Tools and Technologies ## 工具与技术 You can write and run Angular tests with a variety of tools and technologies. This chapter describes specific choices that are known to work well. 你可以用多种工具和技术来编写和运行Angular测试。本章介绍了一些大家已经知道能良好工作的选择。 table(width="100%") col(width="20%") col(width="80%") tr th 技术 th 目的 tr(style=top) td(style="vertical-align: top") Jasmine td :marked The [Jasmine test framework](http://jasmine.github.io/2.4/introduction.html). provides everything needed to write basic tests. It ships with an HTML test runner that executes tests in the browser. [Jasmine测试框架](http://jasmine.github.io/2.4/introduction.html)提供了所有编写基本测试的工具。 它自带一个HTML测试运行器,用来在浏览器中执行测试。 tr(style=top) td(style="vertical-align: top") Angular测试工具 td :marked The Angular testing utilities create a test environment for the Angular application code under test. Use them to condition and control parts of the application as they interact _within_ the Angular environment. Angular测试工具为被测试的Angular应用代码创建一个测试环境。在应用代码与Angular环境互动时,使用Angular测试工具来限制和控制应用的部分代码。 tr(style=top) td(style="vertical-align: top") Karma td :marked The [karma test runner](https://karma-runner.github.io/1.0/index.html) is ideal for writing and running unit tests while developing the application. It can be an integral part of the project's development and continuous integration processes. This chapter describes how to setup and run tests with karma. [karma测试运行器](https://karma-runner.github.io/1.0/index.html)是在开发应用的过程中 编写和运行单元测试的理想工具。 它能成为项目开发和连续一体化进程的一个不可分割的一部分。本章讲述了如何用Karma设置和运行测试。 tr(style=top) td(style="vertical-align: top") Protractor td :marked Use protractor to write and run _end-to-end_ (e2e) tests. End-to-end tests explore the application _as users experience it_. In e2e testing, one process runs the real application and a second process runs protractor tests that simulate user behavior and assert that the application responds in the browser as expected. 使用`Protractor`来编写和运行_端对端(e2e)_测试。端对端测试**像用户体验应用程序那样**探索它。 在端对端测试中,一个进程运行真正的应用,另一个进程运行Protractor测试,模拟用户行为,判断应用在浏览器中的反应是否正确。 .l-hr #setup :marked # Setup # 搭建测试环境 Many think writing tests is fun. Few enjoy setting up the test environment. To get to the fun as quickly as possible, the deep details of setup appear later in the chapter (_forthcoming_). A bare minimum of discussion plus the downloadable source code must suffice for now. 很多人认为编写测试很有趣。很少有人享受搭建测试环境的过程。 为了尽快开始有趣的部分,我们在本章后面一点再详细讲述搭建测试环境的细节。 在这里,我们先进行最简单的讨论,再加上源代码供大家下载。 There are two fast paths to getting started. 有两种快速方法立刻开始。 1. Start a new project following the instructions in the [QuickStart github repository](https://github.com/angular/quickstart/blob/master/README.md). 1. 根据[快速起步的github库](https://github.com/angular/quickstart/blob/master/README.md)中的说明创建一个新的项目. 1. Start a new project with the [Angular CLI](https://github.com/angular/angular-cli/blob/master/README.md). 1. 使用[Angular CLI](https://github.com/angular/angular-cli/blob/master/README.md)创建一个新的项目。 Both approaches install **npm packages, files, and scripts** pre-configured for applications built in their respective modalities. Their artifacts and procedures differ slightly but their essentials are the same and there are no differences in the test code. 以上两种方法都安装在各自的模式下为应用预先配置的**npm包、文件和脚本**。它们的文件和规程有一点不同,但是它们的核心部分是一样的,并且在测试代码方面没有任何区别。 In this chapter, the application and its tests are based on the QuickStart repo. 在本章中,应用及其测试是基于《快速起步》库的。 .alert.is-helpful :marked If your application was based on the QuickStart repository, you can skip the rest of this section and get on with your first test. The QuickStart repo provides all necessary setup. 如果你的应用是基于《快速起步》库的,可以跳过本小节下面的内容,直接开始第一个测试。 《快速起步》库一同了所有必须的配置。 #setup-files :marked ### Setup files ### 文件配置 Here's brief description of the setup files. 下面是文件配置的简介: table(width="100%") col(width="20%") col(width="80%") tr th 文件 th 描述 tr td(style="vertical-align: top") karma.conf.js td :marked The karma configuration file that specifies which plug-ins to use, which application and test files to load, which browser(s) to use, and how to report test results. Karma配置文件。它指定使用哪些插件、加载哪个应用和测试文件、使用哪些浏览器和如何报告测试结果。 It loads three other setup files: 它加载三种配置文件: * `systemjs.config.js` * `systemjs.config.extras.js` * `karma-test-shim.js` tr td(style="vertical-align: top") karma-test-shim.js td :marked This shim prepares karma specifically for the Angular test environment and launches karma itself. It loads the `systemjs.config.js` file as part of that process. 本垫片为Angular测试环境特别准备Karma,并运行Karma。在这个过程中,它加载`systemjs.config.js`文件。 tr td(style="vertical-align: top") systemjs.config.js td :marked [SystemJS](https://github.com/systemjs/systemjs/blob/master/README.md) loads the application and test files. This script tells SystemJS where to find those files and how to load them. It's the same version of `systemjs.config.js` used by QuickStart-based applications. [SystemJS](https://github.com/systemjs/systemjs/blob/master/README.md) 加载应用和测试文件。本脚本告诉SystemJS去哪儿寻找这些文件,以及如何加载它们。 `systemjs.config.js`的版本和基于《快速起步》的应用使用的一样。 tr td(style="vertical-align: top") systemjs.config.extras.js td :marked An optional file that supplements the SystemJS configuration in `systemjs.config.js` with configuration for the specific needs of the application itself. 一个可有可无的文件,用来配置应用自己特殊的需求,补充`systemjs.config.js`里面的SystemJS配置。 A stock `systemjs.config.js` can't anticipate those needs. You fill the gaps here. 原装`systemjs.config.js`无法预测这些需求。你可以在这里填补空白。 The sample version for this chapter adds the **model barrel** to the SystemJs `packages` configuration. 本章例子添加了**模型封装桶**到SystemJS的`packages`配置。 tr td(colspan="2") +makeExample('testing/ts/systemjs.config.extras.js', '', 'systemjs.config.extras.js')(format='.') :marked ### npm packages ### npm包 The sample tests are written to run in Jasmine and karma. The two "fast path" setups added the appropriate Jasmine and karma npm packages to the `devDependencies` section of the `package.json`. They were installed when you ran `npm install`. 例子中的测试是按照能在`Jasmine`和`Karma`中运行的规格编写的。以上两种“快速途径”配置测试环境,都在`package.json`的`devDependencies`中添加了合适的`Jasmine`和`karma`的`npm`包。 你运行`npm install`时就会安装它们。 .l-hr #1st-karma-test :marked # The first karma test # 第一个`karma`测试 Start with a simple test to make sure the setup works properly. 编写一个简单的测试,来确认以上的配置是否工作正常。 Create a new file called `1st.spec.ts` in the application root folder, `app/` 在应用的根目录`app/`创建一个新文件,名叫`1st.spec.ts`。 .alert.is-important :marked Tests written in Jasmine are called _specs_ . **The filename extension must be `.spec.ts`**, the convention adhered to by `karma.conf.js` and other tooling. 用Jasmine编写的测试都被叫做**specs**。**文件名后缀必须是`.spec.ts`**,这是`karma.conf.js`和其他工具所坚持和遵守的规约。 :marked **Put spec files somewhere within the `app/` folder.** The `karma.conf.js` tells karma to look for spec files there, for reasons explained [below](#spec-file-location). **将测试配置放到`app/`文件夹下的任何位置。** `karma.conf.js`告诉`Karma`在这个文件夹中寻找测试配置文件,原因[如下](#spec-file-location)。 Add the following code to `app/1st.spec.ts`. 添加下面的代码到`app/1st.spec.ts`。 +makeExample('testing/ts/app/1st.spec.ts', '', 'app/1st.spec.ts')(format='.') :marked ## Run karma ## 运行`Karma` Compile and run it in karma from the command line with this command: 使用下面的命令从命令行中编译并在`Karma`中运行上面的测试配置文件。 code-example(format="." language="bash"). npm test :marked The command compiles the application and test code and starts karma. Both processes watch pertinent files, write messages to the console, and re-run when they detect changes. 该命令编译应用及其测试代码,并启动Karma。 两个进程都监视相关文件,往控制台输入信息和检测到变化时自动重新运行。 .l-sub-section :marked The QuickStart development path defined the `test` command in the `scripts` section of npm's `package.json`. The Angular CLI has different commands to do the same thing. Adjust accordingly. 《快速开始》在npm的`package.json`中的`scripts`里定义了`test`命令。 Angular CLI使用不同的命令来做同样的事情。对不同的环境采取不同的方案。 :marked After a few moments, karma opens a browser and starts writing to the console. 等一小段时间后,Karma便打开浏览器并开始向控制台输出。 figure.image-display img(src='/resources/images/devguide/testing/karma-browser.png' style="width:400px;" alt="Karma browser") :marked Hide (don't close!) the browser and focus on the console output which should look something like this. 隐藏(不要关闭)浏览器,查看控制台的输出,应该看起来像这样: code-example(format="." language="bash"). > npm test ... [0] 1:37:03 PM - Compilation complete. Watching for file changes. ... [1] Chrome 51.0.2704: Executed 0 of 0 SUCCESS Chrome 51.0.2704: Executed 1 of 1 SUCCESS SUCCESS (0.005 secs / 0.005 secs) :marked Both the compiler and karma continue to run. The compiler output is preceeded by `[0]`; the karma output by `[1]`. 编译器和`Karma`都会持续运行。编译器的输入信息前面有`[0]`,`Karma`的输出前面有`[1]`。 Change the expectation from `true` to `false`. 将期望从`true`变换为`false`。 The _compiler_ watcher detects the change and recompiles. **编译器**监视器检测到这个变化并重新编译。 code-example(format="." language="bash"). [0] 1:49:21 PM - File change detected. Starting incremental compilation... [0] 1:49:25 PM - Compilation complete. Watching for file changes. :marked The _karma_ watcher detects the change to the compilation output and re-runs the test. **`Karma`**监视器检测到编译器输出的变化,并重新运行测试。 code-example(format="." language="bash"). [1] Chrome 51.0.2704 1st tests true is true FAILED [1] Expected false to equal true. [1] Chrome 51.0.2704: Executed 1 of 1 (1 FAILED) (0.005 secs / 0.005 secs) :marked It failed of course. 正如所料,测试结果是**失败**。 Restore the expectation from `false` back to `true`. Both processes detect the change, re-run, and karma reports complete success. 将期望从`false`恢复为`true`。两个进程都检测到这个变化,自动重新运行,`Karma`报告测试成功。 .alert.is-helpful :marked The console log can be quite long. Keep your eye on the last line. It says `SUCCESS` when all is well. 控制台的日志可能会非常长。注意最后一样。当一切正常时,它会显示`SUCCESS`。 If it says `FAILED`, scroll up to look for the error or, if that's too painful, pipe the console output to a file and inspect with your favorite editor. 如果它显示`FAILED`,往上翻查看错误。如果觉得这样太麻烦,你可以将控制台的输出内容导出到一个文件中,使用你最喜欢的编辑器查看。 code-example(format="." language="json"). npm test > spec-output.txt :marked ## Test debugging ## 调试测试 Debug specs in the browser in the same way you debug an application. 在浏览器中,像调试应用一样调试测试配置。 - Reveal the karma browser window (hidden earlier). - 显示`Karma`的浏览器窗口(之前被隐藏了)。 - Click the "DEBUG" button; it opens a new browser tab and re-runs the tests - 点击“DEBUG”按钮;它打开一个新的浏览器标签页并重新开始运行测试 - Open the browser's “Developer Tools” (F12 or Ctrl-Shift-I). - 打开浏览器的“Developer Tools”(F12或者Ctrl-Shift-I)。 - Pick the "sources" section - 选择“sources”页 - Open the `1st.spec.ts` test file (Ctrl-P, then start typing the name of the file). - 打开`1st.spec.ts`测试文件(Ctrl-P, 然后输入文件名字)。 - Set a breakpoint in the test - 在测试中设置一个断点。 - Refresh the browser … and it stops at the breakpoint. - 刷新浏览器...然后它就会停在断点上。 figure.image-display img(src='/resources/images/devguide/testing/karma-1st-spec-debug.png' style="width:700px;" alt="Karma debugging") a(href="#top").to-top Back to top a(href="#top").to-top 回到顶部 .l-hr #atu-intro :marked # Introduction to the Angular Testing Utilities # Angular测试工具入门 Many tests explore how applications classes interact with Angular and the DOM while under Angular's control. 许多测试探索应用的类在被`Angular`控制时,是如何与`Angular`和`DOM`互动的。 Such tests are easy to write with the help of the Angular testing utilities which include the `TestBed` class and some helper functions. Angular测试工具包含了`TestBed`类和一些助手函数方法,在它们的帮助下,很容易编写上面那样的测试。 Tests written with these utilities are the main focus of this chapter. But they are not the only tests you should write. 利用**这些工具**编写的测试是本章的主要焦点。但是它们不是你能写的唯一测试类型。 ### Isolated unit tests ### 孤立的单元测试 [Isolated unit tests](#isolated-unit-tests "Unit testing without the Angular testing utilities") examine an instance of a class all by itself without any dependence on Angular or any injected values. The tester creates a test instance of the class with new, supplying test doubles for the constructor parameters as needed, and then probes the test instance API surface. [孤立的单元测试](#isolated-unit-tests "不使用Angular测试工具进行单元测试")独立自己检测类的实例,不依靠Angular或者任何其他注入值。 测试器使用`new`创建一个类的测试实例,在需要时提供用于测试的构造函数参数复制品,并测试被测试实例的API。 You can and should write isolated unit tests for pipes and services. 你可以,也应该为服务和管道编写孤立的单元测试。 +makeExample('testing/ts/app/shared/title-case.pipe.spec.ts', 'mini-excerpt', 'app/shared/title-case.pipe.spec.ts (excerpt)') :marked Components can be tested in isolation as well. However, isolated unit tests don't reveal how these classes interact with Angular. In particular, they can't reveal how a component class interacts with its own template or with other components. Such tests require the Angular testing utilities. 孤立的测试不会展示类是如何与Angular互动的。也就是说,它们不会展示组件的类是如何与自己的模板或者其他组件互动的。 这样的测试需要Angular测试工具。 ### Testing with the Angular Testing Utilities ### 利用**Angular测试工具**进行测试 The Angular testing utilities include the `TestBed` class and several helper functions from `@angular/core/testing`. **Angular测试工具**包含了`TestBed`类和在`@angular/core/testing`中一些助手函数方法。 The `TestBed` creates an Angular testing module — an `@NgModule` class — that you configure to produce the module environment for the class you want to test. You tell the `TestBed` to create an instance of the _component-under-test_ and probe that instance with tests. `TestBed`创建一个Angular测试模块 - 一个`@NgModule`类 - 通过配置它,你为想要测试的类创造模块环境。 通过`TestBed`创建一个被测试的组件的实例,并使用测试来测探这个实例。 Before each spec, the `TestBed` resets itself to a base state. The base state includes a default testing module configuration consisting of the declarables (components, directives, and pipes) and providers (some of them mocked) that almost everyone needs. 在每个测试之前,`TestBed`将自己重设为初始状态。 这个初始状态包含了一个默认的、几乎所有情况都需要的测试模块配置,包括可声明类(组件、指令和管道)和提供商(其中一些是伪造的)。 .l-sub-section :marked The testing shims mentioned [earlier](#setup) initialize the testing module configuration to something like the `BrowserModule` from `@angular/platform-browser`. [之前](#setup)提到的测试垫片初始化测试模块配置到一个模块,这个模块和`@angular/platform-browser`中的`BrowserModule`类似。 :marked This default configuration is merely a _foundation_ for testing an app. You call `TestBed.configureTestingModule` with an object that defines additional imports, declarations, providers and schemas to fit your application tests. Optional `override...` methods can fine-tune aspects of the configuration. 默认配置只是测试一个应用的**基础**。 使用一个定义额外inports、declarations、providers和schemas的对象来调用`TestBed.configureTestingModule`,以适合你的应用程序的测试。 可选的`override...`方法可以微调配置的各个方面。 After configuring the `TestBed`, tell it to create an instance of the _component-under-test_ and the test fixture that you'll need to inspect and control the component's immediate environment. `TestBed`配置完成以后,用它创建一个**被测试的组件**的实例和测试fixture,用来检查和控制组件周围的环境。 +makeExample('testing/ts/app/banner.component.spec.ts', 'simple-example-before-each', 'app/banner.component.spec.ts (simplified)')(format='.') :marked Angular tests can interact with the HTML in the test DOM, simulate user activity, tell Angular to perform specific task (such as change detection), and see the effects of these actions both in the _component-under-test_ and in the test DOM. Angular测试可以在测试DOM中与HTML互动,模拟用户行为,告诉Angular执行特定任务(比如变换检测),并在被测试的组件和测试DOM中查看这些行为的效果。 +makeExample('testing/ts/app/banner.component.spec.ts', 'simple-example-it', 'app/banner.component.spec.ts (simplified)')(format='.') :marked A comprehensive review of the Angular testing utilities appears [later in the chapter](#atu-apis). Let's dive right into Angular testing, starting with with the components of a sample application. 完整的关于Angular测试工具的回顾将会在[本章后面](#atu-apis)出现。 让我们深入到Angular测试,以一个例子应用的组件开始。 a(href="#top").to-top Back to top a(href="#top").to-top 回到顶部 .l-hr #sample-app :marked # The sample application and its tests # 例子应用和它的测试 This chapter tests a cut-down version of the _Tour of Heroes_ [tutorial app](../tutorial). 本章测试一个**简化**版本的**英雄指南**[教程应用程序](../tutorial)。 The following live example shows how it works and provides the complete source code. 下面的在线例子展示了它如何工作,并提供了完整的源代码。

:marked The following live example runs all the tests of this application inside the browser, using the Jasmine Test Runner instead of karma. 下面的在线例子在浏览器中运行该应用的所有测试,使用的是`Jasmine`测试运行器,而非`Karma`。 It includes the tests discussed in this chapter and additional tests for you to explore. This live example contains both application and test code. Give it some time to load and warm up. 它包含了本章讨论的测试和其他测试。本在线例子包含了整个应用和测试代码。给它一些时间来加载。 a(href="#top").to-top Back to top a(href="#top").to-top 回到顶部 .l-hr #simple-component-test :marked # Test a component # 测试一个组件 :marked The top of the screen displays application title, presented by the `BannerComponent` in `app/banner.component.ts`. `app/banner.component.ts`中的`BannerComponent`在屏幕顶部显示应用的标题。 +makeExample('testing/ts/app/banner.component.ts', '', 'app/banner.component.ts')(format='.') :marked `BannerComponent` has an inline template and an interpolation binding. The component is probably too simple to be worth testing in real life but it's perfect for a first encounter with the `TestBed`. `BannerComponent`有内联模板和插值表达式绑定。 在真实应用场景中,这个组件可能过于简单,不值得测试。但是对于第一次接触`TestBed`,它非常合适。 The corresponding `app/banner-component.spec.ts` sits in the same folder as the component, for reasons explained [here](#q-spec-file-location); 对应的`app/banner-component.spec.ts`处于与组件相同的目录,原因在[这里](#q-spec-file-location)有所解释。 Start with ES6 import statements to get access to symbols referenced in the spec. 首先,使用ES6导入声明获取在测试配置中引用的符号。 +makeExample('testing/ts/app/banner.component.spec.ts', 'imports', 'app/banner.component.spec.ts (imports)')(format='.') #configure-testing-module :marked Here's the setup for the tests followed by observations about the `beforeEach`: 下面是测试的配置和`beforeEach`的细节: +makeExample('testing/ts/app/banner.component.spec.ts', 'setup', 'app/banner.component.spec.ts (setup)')(format='.') :marked `TestBed.configureTestingModule` takes an `@NgModule`-like metadata object. This one simply declares the component to test, `BannerComponent`. `TestBed.configureTestingModule`接受一个像`@NgModule`元素据的对象。 这里的对象仅仅声明了要测试的组件`BannerComponent`。 It lacks `imports` because (a) it extends the default testing module configuration which already has what `BannerComponent` needs and (b) `BannerComponent` doesn't interact with any other components. 它没有`imports`,因为(a)它拓展默认测试模块配置,已经有了所有`BannerComponent`需要的,(b)`BannerComponent`不和其他任何组件互动。 #create-component :marked ### _createComponent_ ### _createComponent_ `TestBed.createComponent` creates an instance of `BannerComponent` to test and returns a [fixture](#component-fixture). `TestBed.createComponent`创建一个`BannerComponent`组件的实例用来测试和返回一个[fixture](#component-fixture)。 `TestBed.createComponent` closes the current `TestBed` instance to further configuration. You cannot call any more `TestBed` configuration methods, not `configureTestModule` nor any of the `override...` methods. The `TestBed` throws an error if you try. `TestBed.createComponent`关闭当前`TestBed`实例,让它不能再被配置。 你不能再调用`TestBed`的配置方法,也不能调用`configureTestingModule`或者任何`override...`方法,否则`TestBed`会抛出一个错误。 .alert.is-important :marked Do not configure the `TestBed` after calling `createComponent`. 不要再调用`createComponent`之后试图配置`TestBed`。 #component-fixture :marked ### _ComponentFixture_, _DebugElement_, and _query(By.css)_ The `createComponent` method returns a **`ComponentFixture`**, a handle on the test environment surrounding the created component. The fixture provides access to the component instance itself and to the **`DebugElement`** which is a handle on the component's DOM element. `createComponent`方法返回一个**`ComponentFixture`**,用来控制和访问已创建的组件所在的测试环境。 这个fixture提供了对组件实例自身的访问,同时还提供了用来访问组件的DOM元素的**`DebugElement`**对象。 The `title` property value was interpolated into the DOM within `

` tags. Use the fixture's `DebugElement` to `query` for the `

` element by CSS selector. `title`属性被插值到DOM的`

`标签中。 用CSS选择器从fixture的`DebugElement`中`query``

`元素。 The **`query`** method takes a predicate function and searches the fixture's entire DOM tree for the _first_ element that satisfies the predicate. The result is a _different_ `DebugElement`, one associated with the matching DOM element. **`query`**方法接受一个predicate函数,并搜索fixture的整个DOM树,试图寻找**第一个**满足predicate函数的元素。 .l-sub-section :marked The `queryAll` method returns an array of _all_ `DebugElements` that satisfy the predicate. `queryAll`方法返回一个数组,包含所有`DebugElement`中满足predicate的元素。 A _predicate_ is a function that returns a boolean. A query predicate receives a `DebugElement` and returns `true` if the element meets the selection criteria. 一个**predicate**是一个返回布尔值的函数。 一个查询predicate接受一个`DebugElement`,如果元素符合选择条件便返回`true`。 :marked The **`By`** class is an Angular testing utility that produces useful predicates. Its `By.css` static method produces a standard CSS selector predicate that filters the same way as a jQuery selector. **`By`**类是一个Angular测试工具,它生成有用的predicate。 它的`By.css`静态方法产生一个标准CSS选择器 predicate,与JQuery选择器相同的方式过滤。 Finally, the setup assigns the DOM element from the `DebugElement` **`nativeElement`** property to `el`. The tests will assert that `el` contains the expected title text. 最后,这个配置指定`DebugElement`中的**`nativeElement`DOM元素到属性`el`。 测试将判断`el`是否包含期待的标题文本。 ### The tests ### 测试 Jasmine runs the `beforeEach` function before each of these tests 再每个测试之前,Jasmin都一次运行`beforeEach`函数: +makeExample('testing/ts/app/banner.component.spec.ts', 'tests', 'app/banner.component.spec.ts (tests)')(format='.') :marked These tests ask the `DebugElement` for the native HTML element to satisfy their expectations. 这些测试向`DebugElement`获取原生HTML元素,来满足自己的期望。 #detect-changes :marked ### _detectChanges_: Angular change detection within a test ### **detectChanges**:在测试中的Angular变化检测 Each test tells Angular when to perform change detection by calling `fixture.detectChanges()`. The first test does so immediately, triggering data binding and propagation of the `title` property to the DOM element. 每个测试都通过调用`fixture.detectChanges()`来通知Angular执行变化检测。第一个测试立刻这么做,触发数据绑定和并将`title`属性发送到DOM元素中。 The second test changes the component's `title` property _and only then_ calls `fixture.detectChanges()`; the new value appears in the DOM element. 第二个测试在更改组件的`title`属性**之后**才调用`fixture.detectChanges()`。新值出现在DOM元素中。 In production, change detection kicks in automatically when Angular creates a component or the user enters a keystroke or an asynchronous activity (e.g., AJAX) completes. 在产品阶段,当Angular创建一个组件、用户输入或者异步动作(比如AJAX)完成时,自动触发变化检测。 The `TestBed.createComponent` does _not_ trigger change detection. The fixture does not automatically push the component's `title` property value into the data bound element, a fact demonstrated in the following test: `TestBed.createComponent`**不会**触发变化检测。该工具不会自动将组件的`title`属性值推送到数据绑定的元素,下面的测试展示了这个事实: +makeExample('testing/ts/app/banner.component.spec.ts', 'test-w-o-detect-changes', 'app/banner.component.spec.ts (no detectChanges)')(format='.') :marked This behavior (or lack of it) is intentional. It gives the tester an opportunity to inspect or change the state of the component _before Angular initiates data binding or calls lifecycle hooks_. 此行为(或者缺乏的行为)是有意的。**在Angular初始化数据绑定或者调用生命周期钩子**之前,它给测试者一个机会查看或者改变组件的状态。 #auto-detect-changes :marked ### Automatic change detection ### 自动变化检测 Some testers prefer that the Angular test environment run change detection automatically. That's possible by configuring the `TestBed` with the _AutoDetect_ provider: 一些测试者偏向让Angular测试环境自动运行变化检测。这是可能的,只要配置`TestBed`的**AutoDetect**提供商即可: +makeExample('testing/ts/app/banner.component.spec.ts', 'auto-detect', 'app/banner.component.spec.ts (AutoDetect)')(format='.') :marked Here are three tests that illustrate how _AutoDetect_ works. 下面是展示**自动检测**如何工作的三个测试: +makeExample('testing/ts/app/banner.component.spec.ts', 'auto-detect-tests', 'app/banner.component.spec.ts (AutoDetect Tests)')(format='.') :marked The first test shows the benefit of automatic change detection. 第一个测试展示了自动检测的好处。 The second and third test reveal an important limitation. The Angular testing environment does _not_ know that the test changed the component's `title`. _AutoDetect_ responds to _asynchronous activities_ such as promise resolution, timers, and DOM events. But a direct, synchronous update of the component property is invisible to _AutoDetect_. The test must call `fixture.detectChanges()` manually to trigger another cycle of change detection. 第二和第三个测试显示了一个重要的局限性。 Angular测试环境**不会**知道测试改变了组件的`title`属性。 **自动检测**只对**异步行为**比如承诺的解析,计时器和DOM时间做出反应。 但是一个直接的,同步的组件属性值的变化时不会触发**自动检测**的。 测试必须手动调用`fixture.detectChange()`,来触发新一轮的变化检测周期。 .alert.is-helpful :marked Rather than wonder when the test fixture will or won't perform change detection, the samples in this chapter _always call_ `detectChanges()` _explicitly_. There is no harm in calling `detectChanges()` more often than is strictly necessary. 与其怀疑测试工具会不会执行变化检测,本章中的例子**总是显式**调用`detectChanges()`。 频繁调用`detectChanges()`不理会是否真的需要是没有什么坏处的。 a(href="#top").to-top Back to top a(href="#top").to-top 回到顶部 .l-hr #component-with-dependency :marked # Test a component with a dependency # 测试有一个依赖的组件 Components often have service dependencies. The `WelcomeComponent` displays a welcome message to the logged in user. It knows who the user is based on a property of the injected `UserService`: 组件经常有服务依赖。`WelcomeComponent`为登陆的用户显示一条欢迎信息。它从注入的`UserService`的一个属性知道用户的身份: +makeExample('testing/ts/app/welcome.component.ts', '', 'app/welcome.component.ts')(format='.') :marked The `WelcomeComponent` has decision logic that interacts with the service, logic that makes this component worth testing. Here's the testing module configuration for the spec file, `app/welcome.component.spec.ts`: `WelcomeComponent`有与服务进行交互的决策逻辑,这样的逻辑让这个组件值得测试。下面是测试配置文件的测试模块配置,`app/welcome.component.spec.ts`: +makeExample('testing/ts/app/welcome.component.spec.ts', 'config-test-module', 'app/welcome.component.spec.ts')(format='.') :marked This time, in addition to declaring the _component-under-test_, the configuration adds a `UserService` provider to the `providers` list. But not the real `UserService`. 这次,在测试配置里不但声明了被测试的组件,而且在`providers`数组中添加了`UserService`依赖。但不是真实的`UserService`。 #get-injected-service :marked ## Provide service test doubles ## 提供服务复制品 A _component-under-test_ doesn't have to be injected with real services. In fact, it is usually better if they are test doubles (stubs, fakes, spies, or mocks). The purpose of the spec is to test the component, not the service, and real services can be trouble. 被测试的组件不一定要注入真正的服务。实际上,服务的复制品(stubs, fakes, spies或者mocks)通常会更加合适。 测试配置的主要目的是测试组件,而不是服务。真实的服务可能会有问题。 Injecting the real `UserService` could be a nightmare. The real service might try to ask the user for login credentials and try to reach an authentication server. These behaviors could be hard to intercept. It is far easier and safer to create and register a test double in place of the real `UserService`. 注入真实的`UserService`有可能很麻烦。真实的服务可能询问用户登录凭据,也可能试图连接一个认证服务器。 可能很难拦截这些行为。所以在真实的`UserService`的位置创建和注册一个`UserService`复制品来的更加容易和安全。 This particular test suite supplies a minimal `UserService` stub that satisfies the needs of the `WelcomeComponent` and its tests: 这个测试套件提供了一个最小化的`UserService` stub,用来满足`WelcomeComponent`和它的测试的需求: +makeExample('testing/ts/app/welcome.component.spec.ts', 'user-service-stub')(format='.') #get-injected-service :marked ## Get injected services ## 获取注入的服务 The tests need access to the (stub) `UserService` injected into the `WelcomeComponent`. 测试需要访问被注入到`WelcomeComponent`中的的`UserService`(stub)。 Angular has a hierarchical injection system. There can be injectors at multiple levels, from the root injector created by the `TestBed` down through the component tree. Angular有一个层次化的注入系统。 可以有很多层注入器,从根`TestBed`创建的注入器下来贯穿整个组件数。 The safest way to get the injected service, the way that **_always works_**, is to **get it from the injector of the _component-under-test_**. The component injector is a property of the fixture's `DebugElement`. 最安全的获取注入的服务和**总是有效**的方法,是**从被测试的组件的注入器获取**。 组件注入器是fixture的`DebugElement`的一个属性。 +makeExample('testing/ts/app/welcome.component.spec.ts', 'injected-service', 'WelcomeComponent\'s injector')(format='.') #testbed-get :marked ### _TestBed.get_ ### _TestBed.get_ You _may_ also be able to get the service from the root injector via `TestBed.get`. This is easier to remember and less verbose. But it only works when Angular injects the component with the service instance in the test's root injector. Fortunately, in this test suite, the _only_ provider of `UserService` is the root testing module, so it is safe to call `TestBed.get` as follows: 你**可以**通过`TestBed.get`方法来从根注入器中获取服务。 它更容易被记住,也更加简介。 但是它只有在Angular使用测试的根注入器的那个服务实例来注入到组件才有效。 幸运的是,在这个测试套件中,**唯一**的`UserService`提供商就是根测试模块,所以像下面这样调用`TestBed.get`很安全: +makeExample('testing/ts/app/welcome.component.spec.ts', 'inject-from-testbed', 'TestBed injector')(format='.') .l-sub-section :marked The [`inject`](#inject) utility function is another way to get one or more services from the test root injector. [`inject`](#inject)辅助函数方法是另外一种从测试的根注入器注入一个或多个服务到测试的方法。 See the section "[_Override Component Providers_](#component-override)" for a use case in which `inject` and `TestBed.get` do not work and you must get the service from the component's injector. 到“[**替换组件提供商**](#component-override)”查看`inject`和`TestBed.get`无效、必须从组件的注入器获取服务的用例。 :marked ### Always get the service from an injector ### 总是从一个注入器获取服务 Surprisingly, you dare not reference the `userServiceStub` object that was provided to the testing module in the body of your test. **It does not work!** The `userService` instance injected into the component is a completely _different_ object, a clone of the provided `userServiceStub`. 出人意料的是,你不敢引用测试代码里提供给测试模块`userServiceStub`对象。**它是行不通的!**。 被注入组件的`userService`实例是一个彻底**不一样**的对象,是提供的`userServiceStub`的克隆。 +makeExample('testing/ts/app/welcome.component.spec.ts', 'stub-not-injected')(format='.') #welcome-spec-setup :marked ### Final setup and tests ### 最后的设置和测试 Here's the complete `beforeEach` using `TestBed.get`: 这里是使用`TestBed.get`的完整`beforeEach`: +makeExample('testing/ts/app/welcome.component.spec.ts', 'setup', 'app/welcome.component.spec.ts')(format='.') :marked And here are some tests: 下面是一些测试: +makeExample('testing/ts/app/welcome.component.spec.ts', 'tests', 'app/welcome.component.spec.ts')(format='.') :marked The first is a sanity test; it confirms that the stubbed `UserService` is called and working. 第一个测试是合法测试,它确认这个伪造的`UserService`是否被调用和工作正常。 .l-sub-section :marked The second parameter to the Jasmine `it` (e.g., `'expected name'`) is an optional addendum. If the expectation fails, Jasmine displays this addendum after the expectation failure message. It can help clarify what went wrong and which expectation failed in a spec with multiple expectations. Jasmine的`it`方法的第二个参数(比如`'expected name'`)是一个可选附加参数。 如果这个期待失败了,Jasmine在期待失败信息后面显示这个附加参数。 在一个拥有多个期待的测试配置中,它可以帮助澄清发生了什么错误,哪个期待失败了。 :marked The remaining tests confirm the logic of the component when the service returns different values. The second test validates the effect of changing the user name. The third test checks that the component displays the proper message when there is no logged-in user. 接下来的测试确认当服务返回不同的值时组件的逻辑是否工作正常。 第二个测试验证变换用户名字的效果。 第三个测试检查如果用户没有登录,组件是否显示正确消息。 a(href="#top").to-top Back to top a(href="#top").to-top 回到顶部 .l-hr #component-with-async-service :marked # Test a component with an async service # 测试一个有异步服务的组件 Many services return values asynchronously. Most data services make an HTTP request to a remote server and the response is necessarily asynchronous. 许多服务异步返回值。大部分数据服务向远程服务器发起HTTP请求,响应必然是异步的。 The "About" view in this sample displays Mark Twain quotes. The `TwainComponent` handles the display, delegating the server request to the `TwainService`. Both are in the `app/shared` folder because the author intends to display Twain quotes on other pages someday. Here is the `TwainComponent`. 本例的`About`视图显示马克吐温的名言。 `TwainComponent`组件处理显示、委派`TwainService`向服务器发起请求。 两者都在`app/shared`目录里,因为作者计划将来在其他页面也显示马克吐温的名言。 下面是`TwainComponent`: +makeExample('testing/ts/app/shared/twain.component.ts', 'component', 'app/shared/twain.component.ts')(format='.') :marked The `TwainService` implementation is irrelevant at this point. It is sufficient to see within `ngOnInit` that `twainService.getQuote` returns a promise which means it is asynchronous. `TwainService`的实现细节现在并不重要。 `ngOnInit`的`twainService.getQuote`返回一个承诺,所以显然它是异步的。 In general, tests should not make calls to remote servers. They should emulate such calls. The setup in this `app/shared/twain.component.spec.ts` shows one way to do that: 一般来讲,测试不应该向远程服务器发请求。 它们应该仿真这样的请求。`app/shared/twain.component.spec.ts`里的配置是其中一种伪造方法: +makeExample('testing/ts/app/shared/twain.component.spec.ts', 'setup', 'app/shared/twain.component.spec.ts (setup)')(format='.') #service-spy :marked ### Spying on the real service ### 刺探(Spy)真实服务 This setup is similar to the [`welcome.component.spec` setup](#welcome-spec-setup). But instead of creating a stubbed service object, it injects the _real_ service (see the testing module `providers`) and replaces the critical `getQuote` method with a Jasmine spy. 本配置与[`welcome.component.spec` setup](#welcome-spec-setup)类似。 但是与其伪造一个服务对象,它注入了真实的服务(参见测试模块的`providers`),并用一个Jasmine的`spy`替换关键的`getQuote`方法。 +makeExample('testing/ts/app/shared/twain.component.spec.ts', 'spy')(format='.') :marked The spy is designed such that any call to `getQuote` receives an immediately resolved promise with a test quote. The spy bypasses the actual `getQuote` method and therefore will not contact the server. 这个Spy的设计是,所有调用`getQuote`的方法都会收到立刻解析的承诺,得到一个预设的名言。Spy拦截了实际`getQuote`方法,所有它不会联系服务。 .l-sub-section :marked Faking a service instance and spying on the real service are _both_ great options. Pick the one that seems easiest for the current test suite. Don't be afraid to change your mind. 伪造一个服务实例和刺探真实服务都是好方法。挑选一个对当前测试套件最简单的方法。你可以随时改变主意。 :marked Here are the tests with commentary to follow: 下面是接下来带有注解的测试: +makeExample('testing/ts/app/shared/twain.component.spec.ts', 'tests', 'app/shared/twain.component.spec.ts (tests)') :marked ### Synchronous tests ### 同步测试 The first two tests are synchronous. Neither test can prove that a value from the service will be displayed. 前两个测试是同步的。它们都不能证明服务返回的值将会被显示。 Thanks to the spy, the second test verifies that `getQuote` is called. But the quote itself has not arrived, despite the fact that the spy returns a resolved promise. 在Spy的帮助下,第二个测试验证了`getQuote`被调用了。 虽然Spy返回了一个解析了的承诺,但是确没有收到名言。 This test must wait at least one full turn of the JavaScript engine, a least one "tick", before the value becomes available. By that time, the test runner has moved on to the next test in the suite. 这个测试必须等待JavaScript引擎一整个回合,至少一个"tick"后,返回值才会有效。这个时候,测试运行器已经移到了测试套件下一个测试。 The test must become an "async test" ... like the third test 测试必须变成一个“异步测试”...就像第三个测试那样。 #async :marked ## The _async_ function in _it_ ## **it**里的**async**函数方法 Notice the `async` in the third test. 注意第三个测试的`async`方法。 +makeExample('testing/ts/app/shared/twain.component.spec.ts', 'async-test', 'app/shared/twain.component.spec.ts (async test)')(format='.') :marked The `async` function is one of the Angular testing utilities. `async`函数是**Angular TestBed**的一部分。 It simplifyies coding of asynchronous tests by arranging for the tester's code to run in a special _async test zone_. 通过将测试代码放到一个特殊的**异步测试区域**来运行,`async`函数简化了异步测试的代码。 The `async` function _takes_ a parameterless function and _returns_ a parameterless function which becomes the argument to the Jasmine `it` call. `async`函数**接受**一个无参数的函数方法,**返回**一个无参数的函数方法,变成Jasmine的`it`函数的参数。 The body of the `async` argument looks much like the body of a normal `it` argument. There is nothing obviously asynchronous about it. For example, it doesn't return a promise and there is no `done` function to call as there is in standard Jasmine asynchronous tests. `async`函数的参数看起来和一个普通的`it`参数主体一样。 没有任何地方显示异步特征。 比如,它不返回承诺,并且没有`done`方法可调用,因为他是一个标准的Jasmine异步测试。 Some functions called within a test (such as `fixture.whenStable`) continue to reveal their asynchronous behavior. Consider also the [_fakeAsync_](#fake-async) alternative which affords a more linear coding experience. 在一个测试(比如`fixture.whenStable`)里面调用函数时,会继续体现它们的异步行为。 考虑使用[_fakeAsync_](#fake-async),以获得一个更加直观的代码经验。 #when-stable :marked ## _whenStable_ ## **whenStable** The test must wait for the `getQuote` promise to resolve. 测试必须等待`getQuote`承诺的解析。 The `getQuote` promise promise resolves in the next turn of the JavaScript engine, thanks to the spy. But a different test implementation of `getQuote` could take longer. An integration test might call the _real_ `getQuote`, resulting in an XHR request that took many seconds to respond. 在spy的帮助下,`getQuote`承诺在JavaScript引擎的下一个回合中被解析。但是不同的`getQuote`测试的实施可能需要更长时间。 集成的测试有可能调用**真实**的`getQuote`,导致一个XHR请求,需要好几秒来响应。 This test has no direct access to the promise returned by the call to `testService.getQuote` which is private and inaccessible inside `TwainComponent`. 调用`testService.getQuote`返回的承诺是是私有的,不能被`TwainComponent`访问,这个测试也对其无访问权。 Fortunately, the `getQuote` promise is accessible to the _async test zone_ which intercepts all promises issued within the _async_ method call. 幸运的是,**异步测试区域**可以访问`getQuote`承诺,因为它拦截所有调用**异步**方法所发出的承诺。 The `ComponentFixture.whenStable` method returns its own promise which resolves when the `getQuote` promise completes. In fact, the _whenStable_ promise resolves when _all pending asynchronous activities_ complete ... the definition of "stable". `ComponentFixture.whenStable`方法返回它自己的承诺,它在`getQuote`承诺完成时被解析。实际上,当**所有待处理异步行为**完成时即为“stable”,在“stable”后**whenStable**承诺被解析。 Then the testing continues. The test kicks off another round of change detection (`fixture.detectChanges`) which tells Angular to update the DOM with the quote. The `getQuote` helper method extracts the display element text and the expectation confirms that the text matches the test quote. 然后测试继续运行。 测试开始另一轮的变化检测(`fixture.detectChanges`),通知Angular使用名言来更新DOM。 `getQuote`助手方法提取出显示元素文本,然后expect语句确认这个文本与预备的名言相符。 #fakeAsync #fake-async :marked ## The _fakeAsync_ function ## **fakeAsync**函数方法 The fourth test verifies the same component behavior in a different way. 第四个测试用不同的方法验证同样的组件行为。 +makeExample('testing/ts/app/shared/twain.component.spec.ts', 'fake-async-test', 'app/shared/twain.component.spec.ts (fakeAsync test)')(format='.') :marked Notice that `fakeAsync` replaces `async` as the `it` argument. The `fakeAsync` function is another of the Angular testing utilities. 注意,在`it`的参数中,`async`被`faceAsync`替换。 `fakeAsync`是另一个Angular测试工具。 Like [async](#async-fn-in-it), it _takes_ a parameterless function and _returns_ a parameterless function which becomes the argument to the Jasmine `it` call. 和[async](#async-fn-in-it)一样,它也**接受**一个无参数函数并**返回**一个无参数函数,变成Jasmine的`it`函数的参数。 The `fakeAsync` function enables a linear coding style by running the test body in a special _fakeAsync test zone_. The principle advantage of `fakeAsync` over `async` is that the test appears to be synchronous. There are no promises at all. No `then(...)` chains to disrupt the visible flow of control. There are limitations. For example, you cannot make an XHR call from within a `fakeAsync`. #tick #tick-first-look :marked ## The _tick_ function Compare the third and fourth tests. Notice that `fixture.whenStable` is gone, replaced by `tick()`. The `tick` function is one of the Angular testing utilities and a companion to `fakeAsync`. It can only be called within a `fakeAsync` body. Calling `tick()` simulates the passage of time until all pending asynchronous activities complete, including the resolution of the `getQuote` promise in this test case. It returns nothing. There is no promise to wait for. Proceed with the same test code as formerly appeared within the `whenStable.then()` callback. Even this simple example is easier to read than the third test. To more fully appreciate the improvement, imagine a succession of asynchronous operations, chained in a long sequence of promise callbacks. #jasmine-done :marked ## _jasmine.done_ While `fakeAsync` and even `async` function greatly simplify Angular asynchronous testing, you can still fallback to the traditional Jasmine asynchronous testing technique. You can still pass `it` a function that takes a [`done` callback](http://jasmine.github.io/2.0/introduction.html#section-Asynchronous_Support). Now you are responsible for chaining promises, handling errors, and calling `done` at the appropriate moment. Here is a `done` version of the previous two tests: +makeExample('testing/ts/app/shared/twain.component.spec.ts', 'done-test', 'app/shared/twain.component.spec.ts (done test)')(format='.') :marked Although we have no direct access to the `getQuote` promise inside `TwainComponent`, the spy does and that makes it possible to wait for `getQuote` to finish. The `jasmine.done` technique, while discouraged, may become necessary when neither `async` nor `fakeAsync` can tolerate a particular asynchronous activity. That's rare but it happens. a(href="#top").to-top Back to top .l-hr #component-with-external-template :marked # Test a component with an external template The `TestBed.createComponent` is a synchronous method. It assumes that everything it could need is already in memory. That has been true so far. Each tested component's `@Component` metadata has a `template` property specifying an _inline templates_. Neither component had a `styleUrls` property. Everything necessary to compile them was in memory at test runtime. The `DashboardHeroComponent` is different. It has an external template and external css file, specified in `templateUrl` and `styleUrls` properties. +makeExample('testing/ts/app/dashboard/dashboard-hero.component.ts', 'component', 'app/dashboard/dashboard-hero.component.ts (component)')(format='.') :marked The compiler must read these files from a file system before it can create a component instance. The `TestBed.compileComponents` method asynchronously compiles all the components configured in its current testing module. After it completes, external templates and css files, have been "inlined" and `TestBed.createComponent` can do its job synchronously. .l-sub-section :marked WebPack developers need not call `compileComponents` because it inlines templates and css as part of the automated build process that precedes running the test. :marked The `app/dashboard/dashboard-hero.component.spec.ts` demonstrates the pre-compilation process: +makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'compile-components', 'app/dashboard/dashboard-hero.component.spec.ts (compileComponents)')(format='.') #async-in-before-each :marked ## The _async_ function in _beforeEach_ Notice the `async` call in the `beforeEach`. The `async` function arranges for the tester's code to run in a special _async test zone_ that hides the mechanics of asynchronous execution, just as it does when passed to an [_it_ test)(#async). #compile-components :marked ## _compileComponents_ In this example, `Testbed.compileComponents` compiles one component, the `DashboardComponent`. It's the only declared component in this testing module. Tests later in this chapter have more declared components and some of them import application modules that declare yet more components. Some or all of these components could have external templates and css files. `TestBed.compileComponents` compiles them all asynchonously at one time. The `compileComponents` method returns a promise so you can perform additional tasks _after_ it finishes. ### _compileComponents_ closes configuration After `compileComponents` runs, the current `TestBed` instance is closed to further configuration. You cannot call any more `TestBed` configuration methods, not `configureTestModule` nor any of the `override...` methods. The `TestBed` throws an error if you try. .alert.is-important :marked Do not configure the `TestBed` after calling `compileComponents`. Make `compileComponents` the last step before calling `TestBed.createInstance` to instantiate the _component-under-test_. :marked The `DashboardHeroComponent` spec follows the asynchonous `beforeEach` with a _synchronous_ `beforeEach` that completes the setup steps and runs tests ... as described in the next section. .l-hr #component-with-inputs-outputs :marked # Test a component with inputs and outputs A component with inputs and outputs typically appears inside the view template of a host component. The host uses a property binding to set the input property and uses an event binding to listen to events raised by the output property. The testing goal is to verify that such bindings work as expected. The tests should set input values and listen for output events. The `DashboardHeroComponent` is tiny example of a component in this role. It displays an individual heroe provided by the `DashboardComponent`. Clicking that hero tells the the `DashboardComponent` that the user has selected the hero. The `DashboardHeroComponent` is embedded in the `DashboardComponent` template like this: +makeExample('testing/ts/app/dashboard/dashboard.component.html', 'dashboard-hero', 'app/dashboard/dashboard.component.html (excerpt)')(format='.') :marked The `DashboardHeroComponent` appears in an `*ngFor` repeater which sets each component's `hero` input property to the iteration value and listens for the components `selected` event. Here's the component's definition again: +makeExample('testing/ts/app/dashboard/dashboard-hero.component.ts', 'component', 'app/dashboard/dashboard-hero.component.ts (component)')(format='.') :marked While testing a component this simple has little intrinsic value, it's worth knowing how. Three approaches come to mind: 1. Test it as used by `DashboardComponent` 1. Test it as a stand-alone component 1. Test it as used by a substitute for `DashboardComponent` A quick look at the `DashboardComponent` constructor discourages the first approach: +makeExample('testing/ts/app/dashboard/dashboard.component.ts', 'ctor', 'app/dashboard/dashboard.component.ts (constructor)')(format='.') :marked The `DashboardComponent` depends upon the Angular router and the `HeroService`. You'd probably have to replace them both with test doubles and that looks like a lot of work. The router seems particularly challenging. .l-sub-section :marked The [discussion below](#routed-component) covers testing components that requre the router. :marked The immediate goal is to test the `DashboardHeroComponent`, not the `DashboardComponent`, and there's no need to work hard unnecessarily. Let's try the second and third options. ## Test _DashboardHeroComponent_ stand-alone Here's the spec file setup. +makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'setup', 'app/dashboard/dashboard-hero.component.spec.ts (setup)')(format='.') :marked The async `beforeEach` was discussed [above](#component-with-external-template). Having compiled the components asynchronously with `compileComponents`, the rest of the setup proceeds _synchronously_ in a _second_ `beforeEach`, using the basic techniques described [earlier](#simple-component-test). Note how the setup code assigns a test hero (`expectedHero`) to the component's `hero` property, emulating the way the `DashboardComponent` would set it via the property binding in its repeater. The first test follows: +makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'name-test', 'app/dashboard/dashboard-hero.component.spec.ts (name test)')(format='.') :marked It verifies that the hero name is propagated through to template with a binding. There's a twist. The template passes the hero name through the Angular `UpperCasePipe` so the test must match the element value with the uppercased name: +makeExample('testing/ts/app/dashboard/dashboard-hero.component.html')(format='.') :marked .alert.is-helpful :marked This small test demonstrates how Angular tests can verify a component's visual representation — something not possible with [isolated unit tests](#isolated-component-tests) — at low cost and without resorting to much slower and more complicated end-to-end tests. :marked The second test verifies click behavior. Clicking the hero should raise a `selected` event that the host component (`DashboardComponent` presumably) can hear: +makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'click-test', 'app/dashboard/dashboard-hero.component.spec.ts (click test)')(format='.') :marked The component exposes an `EventEmitter` property. The test subscribes to it just as the host component would do. The `heroEl` is a `DebugElement` that represents the hero `
`. The test calls `triggerEventHandler` with the "click" event name. The "click" event binding responds by calling `DashboardHeroComponent.click()`. If the component behaves as expected, `click()` tells the component's `selected` property to emit the `hero` object, the test detects that value through its subscription to `selected`, and the test should pass. #trigger-event-handler :marked ### _triggerEventHandler_ The Angular `DebugElement.triggerEventHandler` can raise _any data-bound event_ by its _event name_. The second parameter is the event object passed to the handler. In this example, the test triggers a "click" event with a null event object. +makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'trigger-event-handler')(format='.') :marked The test assumes (correctly in this case) that the runtime event handler — the component's `click()` method — doesn't care about the event object. Other handlers will be less forgiving. For example, the `RouterLink` directive expects an object with a `button` property indicating the mouse button that was pressed. The directive throws an error if the event object doesn't do this correctly. #click-helper :marked Clicking a button, an anchor, or an arbitrary HTML element is a common test task. Make that easy by encapsulating the _click-triggering_ process in a helper such as the `click` function below: +makeExample('testing/ts/testing/index.ts', 'click-event', 'testing/index.ts (click helper)')(format='.') :marked The first parameter is the _element-to-click_. You can pass a custom event object as the second parameter if you wish. The default is a (partial) left-button mouse event object accepted by many handlers including the `RouterLink` directive. .callout.is-critical header click() is not an Angular testing utility :marked The `click()` helper function is **not** one of the Angular testing utilities. It's a function defined in _this chapter's sample code_ and used by all of the sample tests. If you like it, add it to your own collection of helpers. :marked Here's the previous test, rewritten using this click helper. +makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'click-test-2', 'app/dashboard/dashboard-hero.component.spec.ts (click test revised)')(format='.') .l-hr #component-inside-test-host :marked # Test a component inside a test host component In the previous approach the tests themselves played the role of the host `DashboardComponent`. A nagging suspicion remains. Will the `DashboardHeroComponent` work properly when properly data-bound to a host component? Testing with the actual `DashboardComponent` host is doable but seems more trouble than its worth. It's easier to emulate the `DashboardComponent` host with a _test host_ like this one: +makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'test-host', 'app/dashboard/dashboard-hero.component.spec.ts (test host)')(format='.') :marked The test host binds to `DashboardHeroComponent` as the `DashboardComponent` would but without the distraction of the `Router`, the `HeroService` or even the `*ngFor` repeater. The test host sets the component's `hero` input property with its test hero. It binds the component's `selected` event with its `onSelected` handler that records the emitted hero in its `selectedHero` property. Later the tests check that property to verify that the `DashboardHeroComponent.selected` event really did emit the right hero. The setup for the test-host tests is similar to the setup for the stand-alone tests: +makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'test-host-setup', 'app/dashboard/dashboard-hero.component.spec.ts (test host setup)')(format='.') :marked This testing module configuration shows two important differences: 1. It _declares_ both the `DashboardHeroComponent` and the `TestHostComponent`. 1. It _creates_ the `TestHostComponent` instead of the `DashboardHeroComponent`. The `fixture` returned by `createComponent` holds an instance of `TestHostComponent` instead of an instance of `DashboardHeroComponent`. Of course creating the `TestHostComponent` has the side-effect of creating a `DashboardHeroComponent` because the latter appears within the template of the former. The query for the hero element (`heroEl`) still finds it in the test DOM albeit at greater depth in the element tree than before. The tests themselves are almost identical to the stand-alone version +makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'test-host-tests', 'app/dashboard/dashboard-hero.component.spec.ts (test-host)')(format='.') :marked Only the selected event test differs. It confirms that the selected `DashboardHeroComponent` hero really does find its way up through the event binding to the host component. a(href="#top").to-top Back to top .l-hr #routed-component :marked # Test a routed component Testing the actual `DashboardComponent` seemed daunting because it injects the `Router`. +makeExample('testing/ts/app/dashboard/dashboard.component.ts', 'ctor', 'app/dashboard/dashboard.component.ts (constructor)')(format='.') :marked It also injects the `HeroService` but faking that is a [familiar story](#component-with-async-servic). The `Router` has a complicated API and is entwined with other services and application pre-conditions. Fortunately, the `DashboardComponent` isn't doing much with the `Router` +makeExample('testing/ts/app/dashboard/dashboard.component.ts', 'goto-detail', 'app/dashboard/dashboard.component.ts (goToDetail)')(format='.') :marked This is often the case. As a rule you test the component, not the router, and care only if the component navigates with the right address under the given conditions. Stubbing the router with a test implementation is an easy option. This should do the trick: +makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'router-stub', 'app/dashboard/dashboard.component.spec.ts (Router Stub)')(format='.') :marked Now we setup the testing module with the test stubs for the `Router` and `HeroService` and create a test instance of the `DashbaordComponent` for subsequent testing. +makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'compile-and-create-body', 'app/dashboard/dashboard.component.spec.ts (compile and create)')(format='.') :marked The following test clicks the displayed hero and confirms (with the help of a spy) that `Router.navigateByUrl` is called with the expected url. +makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'navigate-test', 'app/dashboard/dashboard.component.spec.ts (navigate test)')(format='.') #inject :marked ## The _inject_ function Notice the `inject` function in the second `it` argument. +makeExample('testing/ts/app/dashboard/dashboard.component.spec.ts', 'inject')(format='.') :marked The `inject` function is one of the Angular testing utilities. It injects services into the test function where you can alter, spy on, and manipulate them. The `inject` function has two parameters 1. an array of Angular dependency injection tokens 1. a test function whose parameters correspond exactly to each item in the injection token array .callout.is-important header inject uses the TestBed Injector :marked The `inject` function uses the current `TestBed` injector and can only return services provided at that level. It does not return services from component providers. :marked This example injects the `Router` from the current `TestBed` injector. That's fine for this test because the `Router` is (and must be) provided by the application root injector. If you need a service provided by the component's _own_ injector, call `fixture.debugElement.injector.get` instead: +makeExample('testing/ts/app/welcome.component.spec.ts', 'injected-service', 'Component\'s injector')(format='.') .alert.is-important :marked Use the component's own injector to get the service actually injected into the component. :marked The `inject` function closes the current `TestBed` instance to further configuration. You cannot call any more `TestBed` configuration methods, not `configureTestModule` nor any of the `override...` methods. The `TestBed` throws an error if you try. .alert.is-important :marked Do not configure the `TestBed` after calling `inject`. a(href="#top").to-top Back to top .l-hr #routed-component-w-param :marked # Test a routed component with parameters Clicking a _Dashboard_ hero triggers navigation to `heroes/:id` where `:id` is a route parameter whose value is the `id` of the hero to edit. That URL matches a route to the `HeroDetailComponent`. The router pushes the `:id` token value into the `ActivatedRoute.params` _Observable_ property, Angular injects the `ActivatedRoute` into the `HeroDetailComponent`, and the component extracts the `id` so it can fetch the corresponding hero via the `HeroDetailService`. Here's the `HeroDetailComponent` constructor: +makeExample('testing/ts/app/hero/hero-detail.component.ts', 'ctor', 'app/hero/hero-detail.component.ts (constructor)')(format='.') :marked `HeroDetailComponent` listens for changes to the `ActivatedRoute.params` in its `ngOnInit` method. +makeExample('testing/ts/app/hero/hero-detail.component.ts', 'ng-on-init', 'app/hero/hero-detail.component.ts (ngOnInit)')(format='.') .l-sub-section :marked The expression after `route.params` chains an _Observable_ operator that _plucks_ the `id` from the `params` and then chains a `forEach` operator to subscribes to `id`-changing events. The `id` changes every time the user navigates to a different hero. The `forEach` passes the new `id` value to the component's `getHero` method (not shown) which fetches a hero and sets the component's `hero` property. If the`id` parameter is missing, the `pluck` operator fails and the `catch` treats failure as a request to edit a new hero. The [Router](router.html#route-parameters) chapter covers `ActivatedRoute.params` in more detail. :marked A test can explore how the `HeroDetailComponent` responds to different `id` parameter values by manipulating the `ActivatedRoute` injected into the component's constructor. By now you know how to stub the `Router` and a data service. Stubbing the `ActivatedRoute` would follow the same pattern except for a complication: the `ActivatedRoute.params` is an _Observable_. #stub-observable :marked ### _Observable_ test double The `hero-detail.component.spec.ts` relies on an `ActivatedRouteStub` to set `ActivatedRoute.params` values for each test. This is a cross-application, re-usable _test helper class_. We recommend locating such helpers in a `testing` folder sibling to the `app` folder. This sample keeps `ActivatedRouteStub` in `testing/router-stubs.ts`: +makeExample('testing/ts/testing/router-stubs.ts', 'activated-route-stub', 'testing/router-stubs.ts (ActivatedRouteStub)')(format='.') :marked Notable features of this stub: * The stub implements only two of the `ActivatedRoute` capabilities: `params` and `snapshot.params`. * _BehaviorSubject_ drives the stub's `params` _Observable_ and returns the same value to every `params` subscriber until it's given a new value. * The `HeroDetailComponent` chain its expressions to this stub `params` _Observable_ which is now under the tester's control. * Setting the `testParams` property causes the `subject` to push the assigned value into `params`. That triggers the `HeroDetailComponent` _params_ subscription, described above, in the same way that navigation does. * Setting the `testParams` property also updates the stub's internal value for the `snapshot` property to return. .l-sub-section(style="margin-left:30px") :marked The [_snapshot_](router.html#snapshot "Router Chapter: snapshot") is another popular way for components to consume route parameters. .callout.is-helpful :marked The router stubs in this chapter are meant to inspire you. Create your own stubs to fit your testing needs. #observable-tests :marked ### _Observable_ tests Here's a test demonstrating the component's behavior when the observed `id` refers to an existing hero: +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'route-good-id', 'app/hero/hero-detail.component.spec.ts (existing id)')(format='.') .l-sub-section :marked The `createComponent` method and `page` object are discussed [in the next section](#page-object). Rely on your intuition for now. :marked When the `id` cannot be found, the component should re-route to the `HeroListComponent`. The test suite setup provided the same `RouterStub` [described above](#routed-component) which spies on the router without actually navigating. This test supplies a "bad" id and expects the component to try to navigate. +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'route-bad-id', 'app/hero/hero-detail.component.spec.ts (bad id)')(format='.') :marked :marked While this app doesn't have a route to the `HeroDetailComponent` that omits the `id` parameter, it might add such a route someday. The component should do something reasonable when there is no `id`. In this implementation, the component should create and display a new hero. New heroes have `id=0` and a blank `name`. This test confirms that the component behaves as expected: +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'route-no-id', 'app/hero/hero-detail.component.spec.ts (no id)')(format='.') :marked .callout.is-helpful :marked Inspect and download _all_ of the chapter's application test code with this live example. .l-hr #page-object :marked # Use a _page_ object to simplify setup The `HeroDetailComponent` is a simple view with a title, two hero fields, and two buttons. figure.image-display img(src='/resources/images/devguide/testing/hero-detail.component.png' alt="HeroDetailComponent in action") :marked But there's already plenty of template complexity. +makeExample('testing/ts/app/hero/hero-detail.component.html', '', 'app/hero/hero-detail.component.html')(format='.') :marked To fully exercise the component, the test needs ... * to wait until a `hero` arrives before `*ngIf` allows any element in DOM * element references for the title name span and name input-box to inspect their values * two button references to click * spies on services and component methods Even a small form such as this one can produce a mess of tortured conditional setup and CSS element selection. Tame the madness with a `Page` class that simplifies access to component properties and encapsulates the logic that sets them. Here's the `Page` class for the `hero-detail.component.spec.ts` +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'page', 'app/hero/hero-detail.component.spec.ts (Page)')(format='.') :marked Now the important hooks for component manipulation and inspection are neatly organized and accessible from an instance of `Page`. A `createComponent` method creates a `page` and fills in the blanks once the `hero` arrives. +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'create-component', 'app/hero/hero-detail.component.spec.ts (createComponent)')(format='.') :marked The [observable tests](#observable-tests) in the previous section demonstrate how `createComponent` and `page` keep the tests short and _on message_. There are no distractions: no waiting for promises to resolve and no searching the DOM for element values to compare. Here are a few more `HeroDetailComponent` tests to drive the point home. +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'selected-tests', 'app/hero/hero-detail.component.spec.ts (selected tests)')(format='.') a(href="#top").to-top Back to top .l-hr #import-module :marked # Setup with module imports Earlier component tests configured the testing module with a few `declarations` like this: +makeExample('testing/ts/app/dashboard/dashboard-hero.component.spec.ts', 'compile-components', 'app/dashboard/dashboard-hero.component.spec.ts (config)')(format='.') :marked The `DashboardComponent` is simple. It needs no help. But more complex components often depend on other components, directives, pipes, and providers and these must be added to the testing module too. Fortunately, the `TestBed.configureTestingModule` parameter parallels the metadata passed to the `@NgModule` decorator which means you can also specify `providers` and `imports. The `HeroDetailComponent` requires a lot of help despite its small size and simple construction. In addition to the support it receives from the default testing module `CommonModule`, it needs: * `NgModel` and friends in the `FormsModule` enable two-way data binding * The `TitleCasePipe` from the `shared` folder * Router services (which these tests are stubbing) * Hero data access services (also stubbed) One approach is to configure the testing module from the individual pieces as in this example: +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'setup-forms-module', 'app/hero/hero-detail.component.spec.ts (FormsModule setup)')(format='.') :marked Because many app components need the `FormsModule` and the `TitleCasePipe`, the developer created a `SharedModule` to combine these and other frequently requested parts. The test configuration can use the `SharedModule` too as seen in this alternative setup: +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'setup-shared-module', 'app/hero/hero-detail.component.spec.ts (SharedModule setup)')(format='.') :marked It's a bit tighter and smaller, with fewer import statements (not shown). #feature-module-import :marked ### Import the feature module The `HeroDetailComponent` is part of the `HeroModule` [Feature Module](ngmodule.html#feature-modules) that aggregates more of the interdependent pieces including the `SharedModule`. Try a test configuration that imports the `HeroModule` like this one: +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'setup-hero-module', 'app/hero/hero-detail.component.spec.ts (HeroModule setup)')(format='.') :marked That's _really_ crisp. Only the _test doubles_ in the `providers` remain. Even the `HeroDetailComponent` declaration is gone. .l-sub-section :marked In fact, if you try to declare it, Angular throws an error because `HeroDetailComponent` is declared in both the `HeroModule` and the `DynamicTestModule` (the testing module). .alert.is-helpful :marked Importing the component's feature module is often the easiest way to configure the tests, especially when the feature module is small and mostly self-contained ... as feature modules should be. :marked a(href="#top").to-top Back to top .l-hr #component-override :marked # Override component providers The `HeroDetailComponent` provides its own `HeroDetailService`. +makeExample('testing/ts/app/hero/hero-detail.component.ts', 'prototype', 'app/hero/hero-detail.component.ts (prototype)')(format='.') :marked It's not possible to stub the component's `HeroDetailService` in the `providers` of the `TestBed.configureTestingModule`. Those are providers for the _testing module_, not the component. They prepare the dependency injector at the _fixture level_. Angular creates the component with its _own_ injector which is a _child_ of the fixture injector. It registers the component's providers (the `HeroDetailService` in this case) with the child injector. A test cannot get to child injector services from the fixture injector. And `TestBed.configureTestingModule` can't configure them either. Angular has been creating new instances of the real `HeroDetailService` all along! .l-sub-section :marked These tests could fail or timeout if the `HeroDetailService` made its own XHR calls to a remote server. There might not be a remote server to call. Fortunately, the `HeroDetailService` delegates responsibility for remote data access to an injected `HeroService`. +makeExample('testing/ts/app/hero/hero-detail.service.ts', 'prototype', 'app/hero/hero-detail.service.ts (prototype)')(format='.') :marked The [previous test configuration](#feature-module-import) replaces the real `HeroService` with a `FakeHeroService` that intercepts server requests and fakes their responses. :marked What if you aren't so lucky. What if faking the `HeroService` is hard? What if `HeroDetailService` makes its own server requests? The `TestBed.overrideComponent` method can replace the component's `providers` with easy-to-manage _test doubles_ as seen in the following setup variation: +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'setup-override', 'app/hero/hero-detail.component.spec.ts (Override setup)')(format='.') :marked Notice that `TestBed.configureTestingModule` no longer provides a (fake) `HeroService` because it's [not needed](#stub-hero-detail-service). #override-component-method :marked ### The _overrideComponent_ method Focus on the `overrideComponent` method. +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'override-component-method', 'app/hero/hero-detail.component.spec.ts (overrideComponent)')(format='.') :marked It takes two arguments: the component type to override (`HeroDetailComponent`) and an override metadata object. The [overide metadata object](#metadata-override-object) is a generic defined as follows: code-example(format="." language="javascript"). type MetadataOverride = { add?: T; remove?: T; set?: T; }; :marked A metadata override object can either add-and-remove elements in metadata properties or completely reset those properties. This example resets the component's `providers` metadata. The type parameter, `T`, is the kind of metadata you'd pass to the `@Component` decorator: code-example(format="." language="javascript"). selector?: string; template?: string; templateUrl?: string; providers?: any[]; ... #stub-hero-detail-service :marked ### _StubHeroDetailService_ This example completely replaces the component's `providers` with an array containing the `StubHeroDetailService`. The `StubHeroDetailService` is dead simple. It doesn't need a `HeroService` (fake or otherwise). +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'stub-hds', 'app/hero/hero-detail.component.spec.ts (StubHeroDetailService)')(format='.') :marked ### The override tests Now the tests can control the component's hero directly by manipulating the stub's `testHero`. +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'override-tests', 'app/hero/hero-detail.component.spec.ts (override tests)')(format='.') :marked ### More overrides The `TestBed.overrideComponent` method can be called multiple times for the same or different components. The `TestBed` offers similar `overrideDirective`, `overrideModule`, and `overridePipe` methods for digging into and replacing parts of these other classes. Explore the options and combinations on your own. a(href="#top").to-top Back to top .l-hr #router-outlet-component :marked # Test a _RouterOutlet_ component The `AppComponent` displays routed components in a ``. It also displays a navigation bar with anchors and their `RouterLink` directives. #app-component-html +makeExample('testing/ts/app/app.component.html', '', 'app/app.component.html')(format='.') :marked The component class does nothing. +makeExample('testing/ts/app/app.component.ts', '', 'app/app.component.ts')(format='.') :marked Unit tests can confirm that the anchors are wired properly without engaging the router. See why this is worth doing [below](#why-stubbed-routerlink-tests). #stub-component :marked ### Stubbing unneeded components The test setup should look familiar +makeExample('testing/ts/app/app.component.spec.ts', 'setup-stubs', 'app/app.component.spec.ts (Stub Setup)')(format='.') :marked The `AppComponent` is the declared test subject The setup extends the default testing module with one real component (`BannerComponent`) and several stubs. * `BannerComponent` is simple and harmless to use as is. * The real `WelcomeComponent` has an injected service. `WelcomeStubComponent` is a placeholder with no service to worry about. * The real `RouterOutlet` is complex and errors easily. The `RouterOutletStubComponent` (in `testing/router-stubs.ts`) is safely inert. The component stubs are essential. Without them, the Angular compiler doesn't recognize the `` and `` tags and throws an error. #router-link-stub :marked ### Stubbing the _RouterLink_ The `RouterLinkStubDirective` contributes substantively to the test +makeExample('testing/ts/testing/router-stubs.ts', 'router-link', 'testing/router-stubs.ts (RouterLinkStubDirective)')(format='.') :marked The `host` metadata property wires the click event of the host element (the ``) to the directive's `onClick` method. The URL bound to the `[routerLink]` attribute flows to the directive's `linkParams` property. Clicking the anchor should trigger the `onClick` method which sets the telltale `navigatedTo` property. Tests can inspect that property to confirm the expected _click-to-navigation_ behavior. #by-directive #inject-directive :marked ### _By.directive_ and injected directives A little more setup triggers the initial data binding and gets references to the navigation links: +makeExample('testing/ts/app/app.component.spec.ts', 'test-setup', 'app/app.component.spec.ts (test setup)')(format='.') :marked Two points of special interest: 1. You can locate elements _by directive_, using `By.directive`, not just by css selectors. 1. You can use the component's dependency injector to get an attached directive because Angular always adds attached directives to the component's injector. #app-component-tests :marked Here are some tests that leverage this setup: +makeExample('testing/ts/app/app.component.spec.ts', 'tests', 'app/app.component.spec.ts (selected tests)')(format='.') .l-sub-section :marked The "click" test _in this example_ is worthless. It works hard to appear useful when in fact it tests the `RouterLinkStubDirective` rather than the _component_. This is a common failing of directive stubs. It has a legitimate purpose in this chapter. It demonstrates how to find a `RouterLink` element, click it, and inspect a result, without engaging the full router machinery. This is a skill you may need to test a more sophisticated component, one that changes the display, re-calculates parameters, or re-arranges navigation options when the user clicks the link. #why-stubbed-routerlink-tests :marked ### What good are these tests? Stubbed `RouterLink` tests can confirm that a component with links and an outlet is setup properly, that the component has the links it should have, and that they are all pointing in the expected direction. These tests do not concern whether the app will succeed in navigating to the target component when the user clicks a link. Stubbing the RouterLink and RouterOutlet is the best option for such limited testing goals. Relying on the real router would make them brittle. They could fail for reasons unrelated to the component. For example, a navigation guard could prevent an unauthorized user from visiting the `HeroListComponent`. That's not the fault of the `AppComponent` and no change to that component could cure the failed test. A _different_ battery of tests can explore whether the application navigates as expected in the presence of conditions that influence guards such as whether the user is authenticated and authorized. A future chapter update will explain how to write such tests with the `RouterTestingModule`. a(href="#top").to-top Back to top .l-hr #shallow-component-test :marked # "Shallow component tests" with *NO\_ERRORS\_SCHEMA* The [previous setup](#stub-component) declared the `BannerComponent` and stubbed two other components for _no reason other than to avoid a compiler error_. Without them, the Angular compiler doesn't recognize the ``, `` and `` tags in the [_app.component.html_](#app-component-html) template and throws an error. Add `NO_ERRORS_SCHEMA` to the testing module's `schemas` metadata to tell the compiler to ignore unrecognized elements and attributes. You no longer have to declare irrelevant components and directives. These tests are ***shallow*** because they only "go deep" into the components you want to test. Here is a setup (with `import` statements) that demonstrates the improved simplicity of _shallow_ tests, relative to the stubbing setup. +makeTabs('testing/ts/app/app.component.spec.ts, testing/ts/app/app.component.spec.ts', 'setup-schemas, setup-stubs-w-imports', 'app/app.component.spec.ts (NO_ERRORS_SCHEMA), app/app.component.spec.ts (Stubs)')(format='.') :marked The _only_ declarations are the _component-under-test_ (`AppComponent`) and the `RouterLinkStubDirective` that contributes actively to the tests. The [tests in this example](#app-component-tests) are unchanged. .alert.is-important :marked _Shallow component tests_ with `NO_ERRORS_SCHEMA` greatly simplify unit testing of complex templates. However, the compiler no longer alerts you to mistakes such as misspelled or misused components and directives. a(href="#top").to-top Back to top .l-hr #attribute-directive :marked # Test an attribute directive An _attribute directive_ modifies the behavior of an element, component or another directive. Its name reflects the way the directive is applied: as an attribute on a host element. The sample application's `HighlightDirective` sets the background color of an element based on either a data bound color or a default color (lightgray). It also sets a custom property of the element (`customProperty`) to `true` for no reason other than to show that it can. +makeExample('testing/ts/app/shared/highlight.directive.ts', '', 'app/shared/highlight.directive.ts')(format='.') :marked It's used throughout the application, perhaps most simply in the `AboutComponent`: +makeExample('testing/ts/app/about.component.ts', '', 'app/about.component.ts')(format='.') :marked Testing the specific use of the `HighlightDirective` within the `AboutComponent` requires only the techniques explored above (in particular the ["Shallow test"](#shallow-component-test) approach). +makeExample('testing/ts/app/about.component.spec.ts', 'tests', 'app/about.component.spec.ts')(format='.') :marked However, testing a single use case is unlikely to explore the full range of a directive's capabilities. Finding and testing all components that use the directive is tedious, brittle, and almost as unlikely to afford full coverage. [Isolated unit tests](#isolated-unit-tests) might be helpful. But attribute directives like this one tend to manipulate the DOM. Isolated unit tests don't and therefore don't inspire confidence in the directive's efficacy. A better solution is to create an artificial test component that demonstrates all ways to apply the directive. +makeExample('testing/ts/app/shared/highlight.directive.spec.ts', 'test-component', 'app/shared/highlight.directive.spec.ts (TestComponent)')(format='.') figure.image-display img(src='/resources/images/devguide/testing/highlight-directive-spec.png' width="200px" alt="HighlightDirective spec in action") .l-sub-section :marked The `` case binds the `HighlightDirective` to the name of a color value in the input box. The initial value is the word "cyan" which should be the background color of the input box. :marked Here are some tests of this component: +makeExample('testing/ts/app/shared/highlight.directive.spec.ts', 'selected-tests', 'app/shared/highlight.directive.spec.ts (selected tests)') :marked A few techniques are noteworthy: * The `By.directive` predicate is a great way to get the elements that have this directive _when their element types are unknown_. * The `:not` pseudo-class in `By.css('h2:not([highlight])')` helps find `

` elements that _do not_ have the directive. `By.css('*:not([highlight])')` finds _any_ element that does not have the directive. * `DebugElement.styles` affords access to element styles even in the absence of a real browser, thanks to the `DebugElement` abstraction. But feel free to exploit the `nativeElement` when that seems easier or more clear than the abstraction. * Angular adds a directive to the injector of the element to which it is applied. The test for the default color uses the injector of the 2nd `

` to get its `HighlightDirective` instance and its `defaultColor`. * `DebugElement.properties` affords access to the artificial custom property that is set by the directive. a(href="#top").to-top Back to top .l-hr #isolated-unit-tests :marked # Isolated Unit Tests Testing applications with the help of the Angular testing utilities is the main focus of this chapter. However, it's often more productive to explore the inner logic of application classes with _isolated_ unit tests that don't depend upon Angular. Such tests are often smaller and easier to read, write and maintain. They don't * import from the Angular test libraries * configure a module * prepare dependency injection `providers` * call `inject` or `async` or `fakeAsync` They do * exhibit standard, Angular-agnostic testing techniques * create instances directly with `new` * substitute test doubles (stubs, spys, and mocks) for the real dependencies. .callout.is-important header Write both kinds of tests :marked Good developers write both kinds of tests for the same application part, often in the same spec file. Write simple _isolated_ unit tests to validate the part in isolation. Write _Angular_ tests to validate the part as it interacts with Angular, updates the DOM, and collaborates with the rest of the application. #isolated-service-tests :marked ## Services Services are good candidates for isolated unit testing. Here are some synchronous and asynchronous unit tests of the `FancyService` written without assistance from Angular testing utilities. +makeExample('testing/ts/app/bag/bag.no-testbed.spec.ts', 'FancyService', 'app/bag/bag.no-testbed.spec.ts') :marked A rough line count suggests that these isolated unit tests are about 25% smaller than equivalent Angular tests. That's telling but not decisive. The benefit comes from reduced setup and code complexity. Compare these equivalent tests of `FancyService.getTimeoutValue`. +makeTabs( `testing/ts/app/bag/bag.no-testbed.spec.ts, testing/ts/app/bag/bag.spec.ts`, 'getTimeoutValue, getTimeoutValue', `app/bag/bag.no-testbed.spec.ts (Isolated), app/bag/bag.spec.ts (with Angular testing utilities)`) :marked They have about the same line-count. But the Angular-dependent version has more moving parts, including a couple of utility functions (`async` and `inject`). Both approaches work and it's not much of an issue if you're using the Angular testing utilities nearby for other reasons. On the other hand, why burden simple service tests with added complexity? Pick the approach that suits you. ### Services with dependencies Services often depend on other services that Angular injects into the constructor. You can test these services _without_ the testbed. In many cases, it's easier to create and _inject_ dependencies by hand. The `DependentService` is a simple example +makeExample('testing/ts/app/bag/bag.ts', 'DependentService', 'app/bag/bag.ts')(format='.') :marked It delegates it's only method, `getValue`, to the injected `FancyService`. Here are several ways to test it. +makeExample('testing/ts/app/bag/bag.no-testbed.spec.ts', 'DependentService', 'app/bag/bag.no-testbed.spec.ts') :marked The first test creates a `FancyService` with `new` and passes it to the `DependentService` constructor. It's rarely that simple. The injected service can be difficult to create or control. You can mock the dependency, or use a dummy value, or stub the pertinent service method with a substitute method that is easy to control. These _isolated_ unit testing techniques are great for exploring the inner logic of a service or its simple integration with a component class. Use the Angular testing utilities when writing tests that validate how a service interacts with components _within the Angular runtime environment_. #isolated-pipe-tests :marked ## Pipes Pipes are easy to test without the Angular testing utilities. A pipe class has one method, `transform`, that turns an input to an output. The `transform` implementation rarely interacts with the DOM. Most pipes have no dependence on Angular other than the `@Pipe` metadata and an interface. Consider a `TitleCasePipe` that capitalizes the first letter of each word. Here's a naive implementation implemented with a regular expression. +makeExample('testing/ts/app/shared/title-case.pipe.ts', '', 'app/shared/title-case.pipe.ts')(format='.') :marked Anything that uses a regular expression is worth testing thoroughly. Use simple Jasmine to explore the expected cases and the edge cases. +makeExample('testing/ts/app/shared/title-case.pipe.spec.ts', 'excerpt', 'app/shared/title-case.pipe.spec.ts') :marked ### Write Angular tests too These are tests of the pipe _in isolation_. They can't tell if the `TitleCasePipe` is working properly as applied in the application components. Consider adding component tests such as this one: +makeExample('testing/ts/app/hero/hero-detail.component.spec.ts', 'title-case-pipe', 'app/hero/hero-detail.component.spec.ts (pipe test)') #isolated-component-tests :marked ## Components Component tests typically examine how a component class interacts with its own template or with collaborating components. The Angular testing utilities are specifically designed to facilitate such tests. Consider this `ButtonComp` component. +makeExample('testing/ts/app/bag/bag.ts', 'ButtonComp', 'app/bag/bag.ts (ButtonComp)')(format='.') :marked The following Angular test demonstrates that clicking a button in the template leads to an update of the on-screen message. +makeExample('testing/ts/app/bag/bag.spec.ts', 'ButtonComp', 'app/bag/bag.spec.ts (ButtonComp)')(format='.') :marked The assertions verify the data binding flow from one HTML control (the `