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

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