AngularテストでwaitForAsyncとfakeAsyncを使用する方法
序章
Angular 2+は、非同期コードをテストするためのasync
およびfakeAsync
ユーティリティを提供します。 これにより、Angularユニットと統合テストの記述がはるかに簡単になります。
この記事では、サンプルテストとともにwaitForAsync
とfakeAsync
を紹介します。
前提条件
このチュートリアルを完了するには、次のものが必要です。
- Node.jsはローカルにインストールされます。これは、Node.jsのインストール方法とローカル開発環境の作成に従って実行できます。
- Angularプロジェクトのセットアップにある程度精通している。
このチュートリアルは、ノード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
ユーティリティは、Promiseをインターセプトする専用のテストゾーンでコードを実行するようにAngularに指示します。 compileComponents
を使用する場合の、Angularでのユニットテストの概要で非同期ユーティリティについて簡単に説明しました。
whenStable
ユーティリティを使用すると、すべての約束が解決されて期待を実行できるようになるまで待つことができます。
最初にapp.component.ts
を開き、Promise
からresolve
title
を使用します。
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
と交換します。
<h1>
{{ title }}
</h1>
<button (click)="setTitle()" class="set-title">
Set Title
</button>
ボタンがクリックされると、title
プロパティがpromiseを使用して設定されます。
そして、waitForAsync
とwhenStable
を使用してこの機能をテストする方法は次のとおりです。
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への応答などの便利なものを実際に待機するPromiseがあります。
この時点で、テストを実行できます。
- ng test
これにより、'should display title'
テスト結果が正常に生成されます。
fakeAsync
を使用したテスト
async
の問題は、テストで実際の待機を導入する必要があることです。これにより、テストが非常に遅くなる可能性があります。 fakeAsync
が助けになり、非同期コードを同期的にテストするのに役立ちます。
fakeAsync
を示すために、簡単な例から始めましょう。 コンポーネントテンプレートに次のような値をインクリメントするボタンがあるとします。
<h1>
{{ incrementDecrement.value }}
</h1>
<button (click)="increment()" class="increment">
Increment
</button>
次のようなコンポーネントクラスのincrement
メソッドを呼び出します。
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
これには、setTimeout
を使用して非同期化されたincrement
メソッドがあります。
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
ユーティリティを使用して統合テストを実行し、テンプレートで値が増分されることを確認します。
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'
テスト結果が正常に生成されます。
そのように通過時間を指定することは、すぐに面倒になり、どのくらいの時間が経過すべきかわからない場合に問題になる可能性があります。
flush
と呼ばれる新しいユーティリティがAngular4.2で導入され、その問題を解決します。 マクロタスクキューが空になるまでの時間の経過をシミュレートします。 マクロタスクには、setTimouts
、setIntervals
、requestAnimationFrame
などが含まれます。
したがって、flush
を使用して、次のようなテストを作成できます。
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テストガイドについては、公式ドキュメントを参照することもできます。