元々Zone
のテスト周りの新機能を書きたいですが、まだ実装完了していないので、fakeAsync
の使い方を纏めさせて頂きます。
fakeAsync
オフィシャルのドキュメントがこちらです、https://angular.io/guide/testing#async-test-with-fakeasync 、一部が私が最近更新したもので(RxJS/Jasmine.clockなど)、この記事がサンプルコードで使い方を説明したいと思います。
fakeAsync はなに?
fakeAsync
がAngularでfakeAsyncTestZoneSpec
を利用して、非同期の操作(setTimeoutなど)を同期の形でテストできるようなライブラリです。
例えば:setTimeout
をテストするため、かきのようなコードになります。
it('test setTimeout`, (done: DoneFn) => {
let a = 0;
setTimeout(() => a ++, 100);
setTimeout(() => {expect(a).toBe(1); done(); }, 100);
});
このような書き方で、テストするには時間がかかりますし、複雑なテストを書くときのexpect
も面倒になります。このケースをfakeAsync
で書きかえると、下記の様になりました。
it('test setTimeout with fakeAsync', fakeAsync(() => {
let a = 0;
setTimeout(() => a ++, 100);
tick(100);
expect(a).toBe(1);
}));
このようなやり方で、非同期のテストが同期になりました。delay
を待たずにテストが進められますし、expect
も書きやすくなりました。
fakeAsyncをサポートする非同期操作
- setTimeout
- setInterval
- Promise
- setImmediate
- requestAnimationFrame
他のFunctionが今zone.js
でどんどん対応じゅうです。
fakeAsync実際利用するときのTips
- async/await と連携して、componentをテストする.
例えば、下記の様なcomponent.spec.ts
で、
it('should show title correctly', () => {
component.title = 'hello';
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(fixture.nativeElement.querySelector(By.css('title')).textContent).toContain('hello');
});
});
これをasync/await+fakeAsync
でかきかえると、読みやすくなれます。
// Utility function
async function runChangeDetection<T>(fixture: ComponentFixture<T>) {
fixture.detectChanges();
tick();
return await fixture.whenStable();
}
it('should show title correctly', async () => {
component.title = 'hello';
await runChangeDetection<TestComponent>(fixture);
expect(fixture.nativeElement.querySelector(By.css('title')).textContent).toContain('hello');
});
- Date.nowと連携する
it('should get Date diff correctly in fakeAsync', fakeAsync(() => {
const start = Date.now();
tick(100);
const end = Date.now();
expect(end - start).toBe(100);
}));
- jasmine.clock と連携する
describe('use jasmine.clock()', () => {
beforeEach(() => { jasmine.clock().install(); });
afterEach(() => { jasmine.clock().uninstall(); });
it('should tick jasmine.clock with fakeAsync.tick', fakeAsync(() => {
// is in fakeAsync now, don't need to call fakeAsync(testFn)
let called = false;
setTimeout(() => { called = true; }, 100);
jasmine.clock().tick(100);
expect(called).toBe(true);
}));
});
さらに、jasmine.clock
を利用するとき、自動てきにfakeAsyncにはいることもできます。
まずsrc/test.ts
で、zone-testing
をimportするまえに、かきのコードを追加して、
(window as any)['__zone_symbol__fakeAsyncPatchLock'] = true;
import 'zone.js/dist/zone-testing';
そしたら、上記のケースで、fakeAsync
の呼び出しがいらなくなって、テストケースが自動てきにfakeAsyncに入りました。
describe('use jasmine.clock()', () => {
// need to config __zone_symbol__fakeAsyncPatchLock flag
// before loading zone.js/dist/zone-testing
beforeEach(() => { jasmine.clock().install(); });
afterEach(() => { jasmine.clock().uninstall(); });
it('should auto enter fakeAsync', () => {
// is in fakeAsync now, don't need to call fakeAsync(testFn)
let called = false;
setTimeout(() => { called = true; }, 100);
jasmine.clock().tick(100);
expect(called).toBe(true);
});
});
- Rxjs Schedulerとの連携
Rxjsでいろいろ時間に関するSchedulerがあって、delay
とか、interval
とか、これらもfakeAsync
と連携することができます。
まずsrc/test.ts
で、zone-testing
をimportするまえに、かきのコードを追加して、
import 'zone.js/dist/zone-patch-rxjs-fake-async';
import 'zone.js/dist/zone-testing';
そしたら、下記のrxjs schedulerのケースがfakeAsync
で実行できます。
it('should get Date diff correctly in fakeAsync with rxjs scheduler', fakeAsync(() => {
// need to add `import 'zone.js/dist/zone-patch-rxjs-fake-async'
// to patch rxjs scheduler
let result = null;
of ('hello').pipe(delay(1000)).subscribe(v => { result = v; });
expect(result).toBeNull();
tick(1000);
expect(result).toBe('hello');
const start = new Date().getTime();
let dateDiff = 0;
interval(1000).pipe(take(2)).subscribe(() => dateDiff = (new Date().getTime() - start));
tick(1000);
expect(dateDiff).toBe(1000);
tick(1000);
expect(dateDiff).toBe(2000);
}));
- Intervalのテスト
もしsetInterval
がテストコードで制御できるなら、テストが完了する前に、intervalをclear必要があります。
it('test setInterval', fakeAsync(() => {
let a = 0;
const intervalId = setInterval(() => a ++, 100);
tick(100);
expect(a).toBe(1);
tick(100);
expect(a).toBe(1);
// need to clearInterval, otherwise fakeAsync will throw error
clearInterval(intervalId);
}));
もしsetInterval
がほかの関数あるいはライブラリのなかで呼びされる場合、discardPeriodicTasks
を呼び出す必要があります。
it('test interval lib', fakeAsync(() => {
let a = 0;
funcWithIntervalInside(() => a ++, 100);
tick(100);
expect(a).toBe(1);
tick(100);
expect(a).toBe(1);
// need to discardPeriodicTasks, otherwise fakeAsync will throw error
discardPeriodicTasks();
}));
- 今Pendingの非同期操作をすべて実行したい場合
例えば、ある関数をテストするとき、この関数の中にsetTimeout
があることが分かって、でも具体的なdelay
が分からないとき、flush
を利用したら、実行することができます。
it('test', fakeASync(() => {
someFuncWithTimeout();
flush();
}));
- 今PendingのMicrotasksを実行
Microtasksといえば、基本てきにはPromise.then
になります。Macrotask
を実行したくなくて、Microtask
だけ実行したい場合、
flushMicrotasks
を利用してください。
it('test', fakeASync(() => {
let a = 0;
let b = 0;
setTimeout(() => a ++);
Promise.resolve().then(() => {
b ++;
});
flushMicrotasks();
expect(a).toBe(0);
expect(b).toBe(1);
}));
これから
今zone.js
でいろいろほかの非同期操作をfakeAsync
テストできるように改修中で、Googleの開発者から聞いて、Google内部のテストケースが大部async/await + fakeAsync
になるらしくて、これからもっとfakeAsync
の利用できるケースを広げるために頑張ります!
どうもありがとうございました!