

JESTでPromiseの再帰処理をテストする方法がわかりません。

JESTを用いてテストを書いています。  
このテストではPromiseが解決されるまで、再帰処理を行うretry関数がテストの対象です。

[code]

    export function retry<T>(fn: () => Promise<T>, limit: number = 5, interval: number = 10): Promise<T> {
      return new Promise((resolve, reject) => {
        fn()
          .then(resolve)
          .catch((error) => {
            setTimeout(async () => {
              // 上限リトライ数を超えたらrejectする
              if (limit === 1) {
                reject(error);
                return;
              }
              // 上限リトライ数未満だったらコールバックの再帰処理を行う
              await retry(fn, limit - 1, interval);
            }, interval);
          });
      });
    }
    
[/code]

上記の retry関数に対して以下のテストを行います。  
①必ず resolve する Promise を渡し、1度目の実行で retry 関数が resolve されること  
②3度目の実行で resolve する Promise を渡し、3度目の実行 で retry 関数が resolve されること

これらをJESTで書くと以下のようになるのかと私は思いました。

[code]

    describe('retry', () => {
      test('resolve on the first call', async () => {
        const fn = jest.fn().mockResolvedValue('resolve!');
        await retry(fn);
        expect(fn.mock.calls.length).toBe(1);
      });
    
      test('resolve on the third call', async () => {
        const fn = jest.fn()
                   .mockRejectedValueOnce(new Error('Async error'))
                   .mockRejectedValueOnce(new Error('Async error'))
                   .mockResolvedValue('OK');
        expect(fn.mock.calls.length).toBe(3)
      });
    });
    
[/code]

結果、下記のようなエラーで失敗しました。

[code]

    Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error: 
        > 40 |   test('resolve on the third call', async () => {
             |   ^
          41 |     const fn = jest
          42 |       .fn()
          43 |       .mockRejectedValueOnce(new Error('Async error'))
    
[/code]

このエラーに関してはJESTの設定でなんとかなると思います。しかしながら、根本的な話、JESTでPromiseの再帰処理をテストする方法がこれであっているのかがわかりません。


CC BY-SA 4.0
2019-05-13T16:01:07.067
54936
2020-05-23T17:28:15.253
2020-02-15T14:39:10.093
32986
34301
question
typescript テスト promise jestjs
JESTでPromiseの再帰処理をテストする方法


1年以上も前の質問なので現在も見ていらっしゃるかはわかりませんが、回答します。

## 質問のコードの不具合箇所

まず、質問のコードが正しくエラーハンドリングされていません。

[code]

    setTimeout(async () => {
      // 上限リトライ数を超えたらrejectする
      if (limit === 1) {
        reject(error);
        return;
      }
      // 上限リトライ数未満だったらコールバックの再帰処理を行う
      await retry(fn, limit - 1, interval);
    }, interval);
    
[/code]

この実装部分ですが、`await
retry(...)`がエラーをthrowをしたときに`reject`までエラーが渡されていないので、テスト実行時にエラーを吐いて死にます。

[code]

    Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error: 
    
[/code]

のログが出力される原因もここにあります。したがって、まずは実装を正すと次のようになります。

[code]

    export function retry<T>(fn: () => Promise<T>, limit: number = 5, interval: number = 10): Promise<T> {
      return new Promise((resolve, reject) => {
        fn()
          .then(resolve)
          .catch((error) => {
            setTimeout(() => {
              // 上限リトライ数を超えたらrejectする
              if (limit === 1) {
                reject(error);
                return;
              }
              // 上限リトライ数未満だったらコールバックの再帰処理を行う
              retry(fn, limit - 1, interval).reject(resolve);
            }, interval);
          });
      });
    }
    
[/code]

## 見通しを良くする

`async/await`を利用してもう少し見通しの良い書き方をすると次のようになります。

[code]

    export const wait = async (ms: number) =>
      new Promise((resolve) => setTimeout(resolve, ms));
    
    export async function retry<T>(
      fn: () => Promise<T>,
      limit: number = 5,
      interval: number = 10
    ): Promise<T> {
      try {
        return await fn();
      } catch (error) {
        if (limit === 1) {
          throw new Error(error);
        }
        await wait(interval);
        return retry(fn, limit - 1, interval);
      }
    }
    
[/code]

書き換えたほうのテストコードを書くと次のようになります。

[code]

    describe("retry", () => {
      test("resolve on the first call ", async () => {
        const fn = jest.fn().mockResolvedValue("resolve!");
        await retry(fn);
        expect(fn.mock.calls.length).toBe(1);
      });
    
      test("retry reject test", async () => {
        let counter = 6;
        const callback = async () => {
          counter -= 1;
          if (counter > 0) {
            throw new Error(`残り ${counter}回`); // throw時点の回数を表示する
          } else {
            return "Success Retry";
          }
        };
        const result = retry(callback);
        await expect(result).rejects.toThrow("残り 1回");
      });
    
      test("retry resolve test", async () => {
        let counter = 5;
        const callback = async () => {
          counter -= 1;
          if (counter > 0) {
            throw new Error(`残り ${counter}回`);
          } else {
            return "Success Retry";
          }
        };
        const result = retry(callback);
        await expect(result).resolves.toBe("Success Retry");
      });
    });
    
[/code]

これはカバレッジ100%のコードとなってます。

### jest.config.jsの設定

[code]

    module.exports = {
      "automock": false,
      "unmockedModulePathPatterns": ["<rootDir>/node_modules/*"],
      "roots": ["<rootDir>/src"],
      "globals": {
        "ts-jest": {
          "tsConfig": "tsconfig.json",
          "diagnostics": false
        }
      },
      "moduleDirectories": ["node_modules"],
      "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "node"],
      "testMatch": ["**/__tests__/*.test.+(ts|tsx)"],
      "collectCoverage": true,
      "transform": {
        "^.+\\.(ts|tsx)$": "ts-jest"
      }
    };
    
[/code]

## 今回のテストを書くためには

今回のテストは短いコードですが次のものが含まれます。

  * 非同期処理
  * setTimeout（タイマー系）
  * リトライ処理（再送処理）

非同期処理のテストを書く場合は、テスト対象のコードが確実にtry-
catchもしくはresolve/rejectでwrapされていることを確認する必要があります。そのためには、コードの分割を行ったり、テストカバレッジを見つつ、テストされていない箇所を特定する等、抜け漏れがないようにやりましょう。もしかするとTypeScriptの型チェックやlinter系のツールでサポートされるかもしれません。

タイマー系のテストはTime
Mocksがjestに用意されているので、それを駆使しましょう。ただ、今回の場合は時間がjestのtimeoutよりも短いもの（10ms）だったので特に指定せず実行しました。  
<https://deltice.github.io/jest/docs/ja/timer-mocks.html>

リトライ処理の処理に関しては、「回数」の部分があるので、それによって処理が分岐する場合はテストコード側に状態を持ち、外側から分岐を変更できるようにすると良いでしょう。今回はたまたま非同期関数に対する処理だったので、検証する値を`await
expect(result).resolves`/`await expect(result).rejects`という形で受け取る必要がありました。

以上が今回のテストコードの戦略でした。

これをご覧になってる方にも参考になればと思います。


CC BY-SA 4.0
2020-05-23T17:22:42.163
66927
2020-05-23T17:28:15.253
2020-05-23T17:28:15.253
7997
7997
54936
answer