Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
14
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
14
Help us understand the problem. What are the problem?