22
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Angular fakeAsyncTest 使い方の纏め

Last updated at Posted at 2018-12-16

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

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

22
14
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?