Jest で非同期な関数のテストを書く - のこのこかずのこ

のこのこかずのこ

10年エンジニアやってるけどいまだになんもわからん

Jest で非同期な関数のテストを書く

最近は、こちらの本でフロントエンドのテストについて勉強しています。テストって書いた方がいいんだろうな〜と思いながらもとっつきにくいというか何をしたらいいか分からなかったので、体系的に学べるのは大変助かるしとても勉強になります!

その中から、Jestで非同期のテストをするときの書き方パターンを、本の内容+自分で調べたことのまとめ。 Jestはドキュメントも丁寧。

非同期コードのテスト · Jest

テストしたい関数

// 必ず成功するPromiseを返す関数
export function alwaysResolve(duration: number) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(duration);
    }, duration);
  });
}

// 必ず失敗するPromiseを返す関数
export function alwaysReject(duration: number) {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(duration);
    }, duration);
  });
}

// 普通に使う場合
alwaysResolve(3000).then((n)=>{console.log(n)});
alwaysReject(4000).catch((n)=>{console.log(n)});

非同期処理のテストコード

パターン1: 関数を実行した時に生成されたPromiseインスタンスを、テスト関数の戻り値としてreturnし、Promiseが解決するまでテストの判定を待つ。

then / catchを使うので感覚的にわかりやすい。が、ちょっと冗長なコード。

describe("Promiseをreturnして解決を待つ", () => {
  test("alwaysResolveの実行結果", () => {
    return alwaysResolve(50).then((duration) => {
      expect(duration).toBe(50);
    });
  });
  test("alwaysRejectの実行結果", () => {
    return alwaysReject(50).catch((duration) => {
      expect(duration).toBe(50);
    });
  });
});

パターン2 : resolves rejects マッチャーを使用したアサーションをreturnする。

ちょっとシンプルに書ける。が、うっかりreturnを書き忘れちゃうと、失敗すべきテストも常にパスするバグが生まれるので注意が必要。

describe("マッチャーを使う", () => {
  test("alwaysResolveの実行結果", () => {
    return expect(alwaysResolve(50)).resolves.toBe(50);
  });
  test("alwaysRejectの実行結果", () => {
    return expect(alwaysReject(50)).rejects.toBe(50);
  });
});

※ 上記2つのパターンでは、JestがPromiseの完了を待つために return が必要ってところが注意ポイント。

パターン3 : async/await と、resolves rejects マッチャーを使用する。

async/await を使えば、自動的にPromiseを返すため、returnを書き忘れてバグる心配はなし。まぁ、同じようにawaitを書き忘れた場合も常にテストがパスしちゃいますが…(書き忘れるな)

describe("async/awaitとマッチャーを使う", () => {
  test("alwaysResolveの実行結果", async () => {
    await expect(alwaysResolve(50)).resolves.toBe(50);
  });
  test("alwaysRejectの実行結果", async () => {
    await expect(alwaysReject(50)).rejects.toBe(50);
  });
});

パターン4 : async/await でPromiseが解決するのを待ってから、アサーションに展開する。

resolveのテストは一番シンプルに書けるけど、rejectのテストはtry...catchを使う必要がある。さらに、テスト対象の関数が意図せず成功してしまった場合に、catch節を通らなくて、テストをパスしてしまう危険がある!そのため、想定した数のアサーションが呼ばれたことを確認するために expect.assertions を必ず記述する必要がある。

describe("async/awaitで解決を待つ", () => {
  test("alwaysResolveの実行結果", async () => {
    expect(await alwaysResolve(50)).toBe(50);
  });
  test("alwaysRejectの実行結果", async () => {
    expect.assertions(1); // これが大事!!!
    try {
      await alwaysReject(50);
    } catch (err) {
      expect(err).toBe(50);
    }
  });
});

どの書き方も、一長一短な感じがあるので、私はこれで行くぜ!と決めてその書き方をマスターするのがいいのかもしれないな〜と思いました。