Angular fakeAsyncTest 使い方の纏め


元々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の利用できるケースを広げるために頑張ります!

どうもありがとうございました!