Jest で Promise + setTimeout のテストがつまる件

## 概要

Jest で `Promise` の返り値のテストを書いていたときに
`setTimeout` が絡むと非同期のテストがうまく完了しないことに気づきました。

## 例

たとえば文字列を指定回数繰り返す非同期関数のテストを書いてみます (非同期でなくてもいい処理ですが):

```typescript:repeat.test.ts
test('repeat should repeat text given times', async () => {
    await expect(repeat('go', 5))
        .resolves
        .toBe('gogogogogo');
});
```

`repeat('go', 5)` は `'go'` を 5 回繰り返した文字列を返すことをテストする感じにします。
それに沿うように実装してみます:

```typescript:repeat.ts
async function repeat(text: string, length: number): Promise<string> {
    // ウェイトをいれる
    await new Promise(
        (resolve, _) => setTimeout(resolve, 10000)
    );

    return Array
        .from({ length }, () => text)
        .join('');
}
```

無駄に `setTimeout` を使ってウェイト処理を入れてみました。
10 秒のウェイトをおくようにしています。
実際のケースではもろもろあってウェイトが必要なケースであると想定しています。

このテストの結果はタイムアウトで失敗します:

> Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.

Jest の非同期テストのデフォルトのタイムアウトが 5000 ms. だからですね。
しかもこのテストには実際に 10 秒かかってしまいます。
テストケースが増えると面倒なことにもなりそうですね。

## `useFakeTimers()` を使う

Jest にタイマーをフェイクのものに差し替えて
タイミングをコントロールできるようにします:

```typescript:repeat.test.ts
jest.useFakeTimers();

test('repeat should repeat text given times', async () => {
    const job = repeat('go', 5);
    jest.runAllTimers();
    await expect(job)
        .resolves
        .toBe('gogogogogo');
});
```

`jest.runAllTimers()` でフェイクの `setTimeout` を即時実行することで
非同期テストのタイムアウト問題が解決します。
テストの時間も短縮されました。

## `Promise` + `setTimeout`

このケースでは文字列の整形結果の取得は同期的に行われていますが、
ウェイトとは別に結果が `Promise` で非同期に取得されるとしてみます
(実際のケースでは WebAPI 経由での取得が考えられます):

```typescript:repeat.ts
async function repeat(text: string, length: number): Promise<string> {
    // 結果を WebAPI とかから取得するとする
    const result = await Promise.resolve(
        Array.from({ length }, () => text)
            .join('')
    );
    // ウェイトをいれる
    await new Promise(
        (resolve, _) => setTimeout(resolve, 10000)
    );

    return result;
}
```

この実装では再び Jest のテストタイムアウトのエラーでテストが失敗してしまいます。
たとえウェイトを 10 秒から 1 msec. に変更したとしてもです。
解決したはずのタイムアウト問題がまた起こってしまいました。

これについては Jest の Issue で報告がされています:
[useFakeTimers breaks with native promise implementation](https://github.com/facebook/jest/issues/7151)
フェイクのタイマーが `Promise` の実装を壊しているのかもという Issue です。

この問題によって結果の `Promise` が
いつまでたっても `resolve` されないことが要因になっているようですね。

## Workaround

**このケースに限って** むりやりどうにかする方法を考えてみました:

```typescript
function useImmediateTimers(global: any = window) {
    global.setTimeout = (
        handler: TimerHandler,
        timeout?: number | undefined,
        ...args: any[]
    ): number => {
        const callableHandler = (typeof handler === 'string')
            ? Function(handler)
            : handler;
        Promise
            .resolve()
            .then(() => callableHandler.apply(args));

        return -1;
    };
}
```

`setTimeout` を即時実行するように実装を置き換えてみる方法です。

```typescript:repeat.test.ts
test('repeat should repeat text given times', async () => {
    useImmediateTimers();
    await expect(repeat('go', 5))
        .resolves
        .toBe('gogogogogo');
});
```

これは (この場合) うまくいきます。

しかしながら、実際に経過時刻を見ていたりするケースであったり、
タイマーのキャンセルを行うケースであったりには対応できません。
その場合、内部実装を考えてタイマーを差し替えるというよくない感じになりそうです。
(そもそもタイマーの関連する処理を切り出して Inject できる実装にしておくのも考えられますね)
なにか他に方法があれば教えてください。