原文:Testing and faking Angular dependencies
依赖注入是 Angular 的一个关键特性。这种灵活的方法使我们的可声明和基于类的服务更容易隔离测试。
可摇树依赖项移除了间接层 即Angular 模块,但我们如何测试它们的可摇树 provider?我们将测试依赖于特定平台 API 的注入令牌的值工厂。
某些组件具有特定于浏览器的功能。我们将一起测试通知用户我们将终止 Internet Explorer 11 支持的横幅。一个合适的测试套件可以给我们足够的信心,我们甚至不必在 Internet Explorer 11 中测试横幅。
我们必须小心不要对复杂的集成场景过于自信。我们应该始终确保在尽可能接近生产的环境中执行 QA(质量保证)测试。这意味着在真实 Internet Explorer 11 浏览器中运行应用程序。
Angular 测试实用程序使我们能够伪造依赖项以进行测试。我们将使用 Angular CLI 的测试框架 Jasmine 探索在 Angular 测试环境中配置和解决依赖关系的不同选项。
通过示例,我们将探索组件 fixtures、组件初始化、自定义 expectations、模拟事件。我们甚至会为非常精简但明确的测试用例创建自定义测试工具。
Faking dependency injection tokens used in token providers
看个例子。
我们创建了一个依赖注入令牌,该令牌评估为指示当前浏览器是否为 Internet Explorer 11 的标志。
// user-agent.token.ts
import { InjectionToken } from '@angular/core';
export const userAgentToken: InjectionToken<string> =
new InjectionToken('User agent string', {
factory: (): string => navigator.userAgent,
providedIn: 'root',
});
// is-internet-explorer-11.token.ts
import { inject, InjectionToken } from '@angular/core';
import { userAgentToken } from './user-agent.token';
export const isInternetExplorer11Token: InjectionToken<boolean> =
new InjectionToken('Internet Explorer 11 flag', {
factory: (): boolean =>
/Trident\/7\.0.+rv:11\.0/.test(inject(userAgentToken)),
providedIn: 'root',
});
为了单独测试 Internet Explorer 11 标志提供程序,我们可以用一个假值替换 userAgentToken。
我们注意到用户代理字符串提供程序从特定于平台的 Navigator API 中提取相关信息。 为了学习,假设我们将需要来自同一个全局导航器对象的其他信息。 根据我们使用的测试运行器,Navigator API 甚至可能在测试环境中不可用。
为了能够创建虚假的导航器配置,我们为导航器 API 创建了一个依赖注入令牌。 我们可以在开发和测试期间使用这些虚假配置来模拟用户上下文。
// user-agent.token.ts
import { inject, InjectionToken } from '@angular/core';
import { navigatorToken } from './navigator.token';
export const userAgentToken: InjectionToken<string> =
new InjectionToken('User agent string', {
factory: (): string => inject(navigatorToken).userAgent,
providedIn: 'root',
});
// navigator.token.ts
import { InjectionToken } from '@angular/core';
export const navigatorToken: InjectionToken<Navigator> =
new InjectionToken('Navigator API', {
factory: (): Navigator => navigator,
providedIn: 'root',
});
对于我们的第一个测试,我们将为 Navigator API 令牌提供一个假值,该令牌在工厂提供程序中用作用户代理字符串令牌的依赖项。
为了出于测试目的替换令牌提供程序,我们在 Angular 测试模块中添加了一个覆盖提供程序,类似于 Angular 模块自己的提供程序如何覆盖导入的 Angular 模块的提供程序。
// navigator-api.spec.ts
import { inject, TestBed } from '@angular/core/testing';
import { navigatorToken } from './navigator.token';
import { userAgentToken } from './user-agent.token';
describe('Navigator API', () => {
describe('User agent string', () => {
describe('Provider', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: navigatorToken,
useValue: {
userAgent: 'Fake browser',
},
},
],
});
});
it(
'extracts the user agent string from the Navigator API token',
inject([userAgentToken], (userAgent: string) => {
expect(userAgent).toBe('Fake browser');
}));
});
});
});
请注意,虽然我们正在测试的是 user agent 令牌及其提供者,但我们正在用假值替换 navigator 令牌依赖项。
Resolving dependencies using the inject function
Angular 测试实用程序为我们提供了不止一种解决依赖关系的方法。 在这个测试中,我们使用@angular/core/testing 包中的 inject 函数(*不是@angular/core 中的那个)。
注入函数允许我们通过在我们作为参数传递的数组中列出它们的标记来解决多个依赖项。 每个依赖注入令牌都被解析并作为参数提供给测试用例函数。
Gotchas when using the Angular testing function inject
当我们使用没有声明的 Angular 测试模块时,即使在同一个测试用例中,我们通常也可以多次覆盖 provider. 我们将在本文后面研究一个例子。
值得注意的是,在使用 Angular 测试功能 inject 时,情况并非如此。 它在执行测试用例函数体之前解决依赖关系。
我们可以使用静态方法 TestBed.configureTestingModule 和 TestBed.overrideProvider 替换 beforeAll 和 beforeEach 钩子中的令牌提供者。 但是当我们使用注入测试功能来解决依赖关系时,我们不能在测试用例之间改变提供者或在测试用例期间替换它。
在没有 declarables 的测试中解决 Angular 依赖关系的一种更灵活的方法是使用静态方法 TestBed.get。 我们只需从测试用例函数或测试生命周期钩子的任何地方传递我们想要解析的依赖注入令牌。
让我们看另一个原生浏览器 API 示例,我们使用依赖注入令牌对其进行抽象,以进行开发和测试。
Location 依赖于 Document:
// location.token.ts
import { DOCUMENT } from '@angular/common';
import { inject, InjectionToken } from '@angular/core';
export const locationToken: InjectionToken<Location> =
new InjectionToken('Location API', {
factory: (): Location => inject(DOCUMENT).location,
providedIn: 'root',
});
// location-api.spec.ts
import { DOCUMENT } from '@angular/common';
import { TestBed } from '@angular/core/testing';
import { locationToken } from './location.token';
describe('Location API', () => {
describe('Provider', () => {
it('extracts the location from the DOCUMENT token', () => {
TestBed.configureTestingModule({
providers: [
{
provide: DOCUMENT,
useValue: {
location: {
href: 'Fake URL',
},
},
},
],
});
const location: Location = TestBed.get(locationToken);
expect(location.href).toBe('Fake URL');
});
});
});
我们通过使用静态 TestBed.get 方法使 Angular 依赖注入系统解析 Location API。 正如 StackBlitz 测试项目中所证明的那样,文档令牌被成功伪造并用于使用其真实的工厂提供程序来解析被测令牌。
Gotchas when resolving dependencies using TestBed
在之前的测试中,我们通过在 Angular 测试模块中为 DOCUMENT 令牌提供文档来将文档替换为假对象。 如果我们没有这样做,Angular 就会提供全局文档对象。
此外,如果我们想测试不同的文档配置,如果我们没有为文档令牌创建 test provider,我们将无法这样做。
在我们使用 TestBed.configureTestingModule 添加测试提供程序的情况下,我们可以使用静态方法 TestBed.overrideProvider 在各种测试用例中将其替换为不同的假值。 在测试 Internet Explorer 11 检测和 Internet Explorer 11 横幅组件时,我们将使用此技术创建测试工具。
请注意,这是唯一可能的,因为我们不使用 declarable。 一旦我们调用 TestBed.createComponent,Angular 测试平台的依赖就被锁定了。
Testing value factories with dependencies
在本文的第一部分中,我们介绍了一个在其提供程序中带有值工厂的令牌。 值工厂评估用户代理字符串是否代表 Internet Explorer 11 浏览器。
为了测试值工厂中的浏览器检测,我们从真实浏览器中收集了一些用户代理字符串并将它们放在一个枚举中。
// fake-user-agent.ts
export enum FakeUserAgent {
Chrome = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36',
InternetExplorer10 = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729)',
InternetExplorer11 = 'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; rv:11.0) like Gecko',
Firefox = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0',
}
在 Internet Explorer 11 检测测试套件中,我们将几乎孤立地测试 isInternetExplorer11Token。 但真正的业务逻辑价值在于它的工厂提供者,它依赖于用户代理令牌。
用户代理令牌从 Navigator API 令牌中提取其值,但 Navigator API 测试套件已涵盖该依赖项。 我们将选择用户代理令牌作为依赖链中合适的位置来开始伪造依赖。
// internet-explorer-11-detection.spec.ts
import { TestBed } from '@angular/core/testing';
import { isInternetExplorer11Token } from './is-internet-explorer-11.token';
import { FakeUserAgent } from './fake-user-agent';
import { userAgentToken } from './user-agent.token';
describe('Internet Explorer 11 detection', () => {
function setup({ userAgent }: { userAgent: string }) {
TestBed.overrideProvider(userAgentToken, { useValue: userAgent });
return {
isInternetExplorer11: TestBed.get(isInternetExplorer11Token),
};
}
const nonInternetExplorerUserAgents: ReadonlyArray<string> =
Object.entries(FakeUserAgent)
.filter(([browser]) =>
!browser.toLowerCase().includes('internetexplorer'))
.map(([_browser, userAgent]) => userAgent);
it('accepts an Internet Explorer 11 user agent', () => {
const { isInternetExplorer11 } = setup({
userAgent: FakeUserAgent.InternetExplorer11,
});
expect(isInternetExplorer11).toBe(true);
});
it('rejects an Internet Explorer 10 user agent', () => {
const { isInternetExplorer11 } = setup({
userAgent: FakeUserAgent.InternetExplorer10,
});
expect(isInternetExplorer11).toBe(false);
});
it('rejects other user agents', () => {
nonInternetExplorerUserAgents.forEach(userAgent => {
const { isInternetExplorer11 } = setup({ userAgent });
expect(isInternetExplorer11).toBe(
false,
`Expected to reject user agent: "${userAgent}"`);
});
});
});
在指定测试用例之前,我们创建了一个测试设置函数,并从我们的假用户代理字符串中减少了一组非 Internet Explorer 用户代理字符串。
测试设置函数采用用户代理并使用它来伪造用户代理令牌提供者。然后我们返回一个具有属性 isInternetExplorer11 的对象,该对象具有通过 TestBed.get 方法从 isInternetExplorer11Token 评估的值。
让我们先测试一下快乐路径。我们传递 Internet Explorer 11 用户代理字符串,并期望被测令牌通过 Angular 的依赖注入系统评估为 true。正如 StackBlitz 测试项目中所见,浏览器检测按预期工作。
当用户使用 Internet Explorer 10 浏览时会发生什么?我们的测试套件表明 Internet Explorer 11 在这种情况下不会导致误报。
换句话说,当依赖令牌中提供 Internet Explorer 10 用户代理字符串时,被测令牌评估为 false。如果这不是预期用途,我们需要更改检测逻辑。现在我们已经进行了测试,很容易证明该更改何时会成功。
最后的测试在 FakeUserAgent 枚举定义的非 Internet Explorer 浏览器上执行浏览器检测。测试用例遍历用户代理字符串,伪造用户代理提供程序,评估 isInternetExplorer11Token 并期望其值为 false。如果不是这种情况,测试运行程序会显示有用的错误消息。
Faking dependencies in component tests
现在我们对 Internet Explorer 11 浏览器检测感到满意,创建和显示弃用横幅很简单。
<!-- internet-explorer-11-banner.component.html -->
<aside *ngIf="isBannerVisible">
Sorry, we will not continue to support Internet Explorer 11.<br />
Please upgrade to Microsoft Edge.<br />
<button (click)="onDismiss()">
Dismiss
</button>
</aside>
// internet-explorer-11-banner.component.ts
import { Component, Inject } from '@angular/core';
import { isInternetExplorer11Token } from './is-internet-explorer-11.token';
@Component({
selector: 'internet-explorer-11-banner',
templateUrl: './internet-explorer-11-banner.component.html',
})
export class InternetExplorer11BannerComponent {
private isDismissed = false;
get isBannerVisible() {
return this.isInternetExplorer11 && !this.isDismissed;
}
constructor(
@Inject(isInternetExplorer11Token) private isInternetExplorer11: boolean,
) {}
onDismiss() {
this.isDismissed = true;
}
}
解除状态只是作为本地 UI 状态存储在私有组件属性中,该属性由计算属性 isBannerVisible 使用。
横幅组件有一个依赖项——isInternetExplorer11Token,它被评估为一个布尔值。 由于 Inject 装饰器,这个布尔值是通过横幅组件构造函数注入的。
Summary
在本文中,我们演示了如何在 Angular 项目中测试和伪造 tree-shakable 依赖项。 我们还测试了依赖于平台特定 API 的价值工厂。
在此过程中,我们调查了使用注入测试功能解决依赖项时的问题。 使用 TestBed,我们解决了依赖注入令牌并探索了这种方法的陷阱。
我们以多种方式测试了 Internet Explorer 11 弃用横幅,以至于几乎不需要在实际浏览器中对其进行测试。 我们在它的组件测试套件中伪造了它的依赖项,但正如我们所讨论的,我们应该始终在复杂的集成场景的真实浏览器目标中测试它。