如何在Angular测试中使用waitForAsync和fakeAsync
介绍
Angular 2+ 提供 async 和 fakeAsync 实用程序来测试异步代码。 这应该使您的 Angular 单元和集成测试更容易编写。
在本文中,您将通过示例测试向您介绍waitForAsync和fakeAsync。
先决条件
要完成本教程,您需要:
- Node.js 安装在本地,您可以按照【X57X】如何安装Node.js 并创建本地开发环境【X126X】进行。
- 熟悉 设置 Angular 项目 。
本教程已使用 Node v16.4.0、npm v7.19.0 和 @angular/core v12.1.1 进行了验证。
设置项目
首先,使用 @angular/cli 创建一个新项目:
ng new angular-async-fakeasync-example
然后,导航到新创建的项目目录:
cd angular-async-fakeasync-example
这将创建一个包含 app.component.html、app.compontent.ts 和 app.component.spec.ts 文件的新 Angular 项目。
使用 waitForAsync 进行测试
waitForAsync 实用程序告诉 Angular 在拦截 Promise 的专用测试区域中运行代码。 在使用 compileComponents 时,我们在 Angular 中的单元测试 简介中简要介绍了异步实用程序。
whenStable 实用程序允许我们等到所有的 Promise 都解决后才能运行我们的期望。
首先打开 app.component.ts 并使用 Promise 到 resolve title:
src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title!: string;
setTitle() {
new Promise(resolve => {
resolve('Async Title!');
}).then((val: any) => {
this.title = val;
});
}
}
然后打开 app.component.html 并用 h1 和 button 替换它:
src/app/app.component.html
<h1>
{{ title }}
</h1>
<button (click)="setTitle()" class="set-title">
Set Title
</button>
单击按钮时,title 属性使用承诺设置。
以下是我们如何使用 waitForAsync 和 whenStable 测试此功能的方法:
src/app/app.component.spec.ts
import { TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
});
it('should display title', waitForAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.debugElement
.query(By.css('.set-title'))
.triggerEventHandler('click', null);
fixture.whenStable().then(() => {
fixture.detectChanges();
const value = fixture.debugElement
.query(By.css('h1'))
.nativeElement
.innerText;
expect(value).toEqual('Async Title!');
});
}));
});
注意: 在一个真实的应用程序中,您将拥有实际等待有用的东西的承诺,例如对后端 API 的请求的响应。
此时,您可以运行测试:
ng test
这将产生一个成功的 'should display title' 测试结果。
使用 fakeAsync 进行测试
async 的问题是我们仍然必须在测试中引入真正的等待,这会使我们的测试变得非常慢。 fakeAsync 来拯救并帮助以同步方式测试异步代码。
为了演示fakeAsync,让我们从一个简单的例子开始。 假设我们的组件模板有一个按钮,可以像这样增加一个值:
src/app/app.component.html
<h1>
{{ incrementDecrement.value }}
</h1>
<button (click)="increment()" class="increment">
Increment
</button>
它调用组件类中的 increment 方法,如下所示:
src/app/app.component.ts
import { Component } from '@angular/core';
import { IncrementDecrementService } from './increment-decrement.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(public incrementDecrement: IncrementDecrementService) { }
increment() {
this.incrementDecrement.increment();
}
}
这个方法本身调用了 incrementDecrement 服务中的一个方法:
ng generate service increment-decrement
它有一个 increment 方法,它通过使用 setTimeout 实现异步:
src/app/increment-decrement.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class IncrementDecrementService {
value = 0;
message!: string;
increment() {
setTimeout(() => {
if (this.value < 15) {
this.value += 1;
this.message = '';
} else {
this.message = 'Maximum reached!';
}
}, 5000); // wait 5 seconds to increment the value
}
}
显然,在现实世界的应用程序中,这种异步性可以通过多种不同的方式引入。
现在让我们使用 fakeAsync 和 tick 实用程序来运行集成测试,并确保模板中的值递增:
src/app/app.component.spec.ts
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AppComponent
]
}).compileComponents();
});
it('should increment in template after 5 seconds', fakeAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.debugElement
.query(By.css('button.increment'))
.triggerEventHandler('click', null);
tick(2000);
fixture.detectChanges();
const value1 = fixture.debugElement.query(By.css('h1')).nativeElement.innerText;
expect(value1).toEqual('0'); // value should still be 0 after 2 seconds
tick(3000);
fixture.detectChanges();
const value2 = fixture.debugElement.query(By.css('h1')).nativeElement.innerText;
expect(value2).toEqual('1'); // 3 seconds later, our value should now be 1
}));
});
请注意 tick 实用程序如何在 fakeAsync 块内用于模拟时间的流逝。 传入 tick 的参数是通过的毫秒数,这些在测试中是累积的。
笔记: Tick can also be used with no argument, in which case it waits until all the microtasks are done (when promises are resolved for example).
此时,您可以运行测试:
ng test
这将产生一个成功的 'should increment in template after 5 seconds' 测试结果。
像这样指定经过时间会很快变得很麻烦,并且当您不知道应该经过多少时间时可能会成为问题。
Angular 4.2 中引入了一个名为 flush 的新实用程序,并有助于解决该问题。 它模拟时间的流逝,直到宏任务队列为空。 宏任务包括 setTimouts、setIntervals 和 requestAnimationFrame 等。
因此,使用 flush,我们可以编写这样的测试,例如:
src/app/app.component.spec.ts
import { TestBed, fakeAsync, flush } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AppComponent
]
}).compileComponents();
});
it('should increment in template', fakeAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.debugElement
.query(By.css('button.increment'))
.triggerEventHandler('click', null);
flush();
fixture.detectChanges();
const value = fixture.debugElement.query(By.css('h1')).nativeElement.innerText;
expect(value).toEqual('1');
}));
});
此时,您可以运行测试:
ng test
这将产生一个成功的 'should increment in template' 测试结果。
结论
在本文中,通过示例测试向您介绍了 waitForAsync 和 fakeAsync。
您也可以参考 官方文档 以获得深入的 Angular 测试指南。