🤩

Jestで不安定なテストをリトライできるようにする

3 min read

はじめに

こんにちは。メディアエンジン株式会社の田中です。

突然ですが、JavaScriptコードのテストを書いていたら、不安定なテストケースが生まれてしまい、まれにCIが失敗するような現象に遭遇したことはないでしょうか?

弊社でもフロントエンドのテストフレームワークとしてJestを採用しており、まれにそのような事態に遭遇することがあります...😭

この記事では、Jestにおける不安定なテストケースへの対策について解説します。

jest.retryTimes()

Jestには、標準で失敗したテストケースをリトライするための手段が提供されています。

jest-circusパッケージjest.retryTimes()を併用することで、失敗したテストケースを再実行できます。

例)

jest.config.js
module.exports = {
  testRunner: 'jest-circus/runner',
  rootDir: 'src',
  /* ... 省略 ... */
  setupFilesAfterEnv: ['<rootDir>/test/jest.setup.js']
}

jest.setup.jsjest.retryTimesを実行します。

jest.setup.js
jest.retryTimes(5)

こうすることで、全テストケースでリトライが有効化されます。

Jest v27以降を使用されている場合は、jest-circusはデフォルトのランナーとして設定されているため、インストールや設定は不要です。

test()のラッパーを用意する方法

jest.retryTimesを利用した方法は便利ではあるものの、全てのテストケースで再実行が有効化されてしまうため、もし不安定なテストケースがあった際にそれを見逃してしまう原因にもつながります。

また、テストケースごとに細かく再実行回数を変更できないなどの問題も考えられそうです。

これらの問題を解消するために色々と考えてみた結果、Jestのtest()関数のラッパーを用意することにしました。

src/test/utils.ts
import { cleanup } from '@testing-library/vue'

interface FlakyTestOptions {
  /**
   * テストケースのタイムアウト(秒単位で指定)
   */
  timeout?: number

  /**
   * テストケースの最大リトライ回数
   */
  maxAttempts?: number
}

export function flakytest(description: string, fn: () => Promise<void>, options: FlakyTestOptions | number = {}): void {
  const { maxAttempts = 5, timeout } = typeof options === 'number' ? { timeout: options } : options

  test(
    description,
    async () => {
      for (let i = 0; i < maxAttempts; i++) {
        try {
          await fn()
          return
        } catch (error) {
          const isLastAttempt = i + 1 === maxAttempts
          if (isLastAttempt) {
            throw error
          } else {
            cleanup()
          }
        }
      }
    },
    timeout == null ? undefined : timeout * maxAttempts
  )
}

そして、不安定なテストケースは標準のtest()の代わりにこのflakytest()を使用してテストケースを定義します。

import { flakytest } from '@/test/utils'
import SomeComponent from '@/components/SomeComponent.vue'
import { fireEvent, waitFor, render } from '@testing-library/vue'

flakytest("SomeComponent", async () => {
  const screen = render(SomeComponent)
  const button = await screen.findByText('...')
  await fireEvent.click(button)
    ...
}, { maxAttempts: 20, timeout: 10000 })

こうすることで、

  • テストケースごとに再実行回数を指定できる
  • どのテストケースが不安定なのかがすぐにわかる

などが実現できます。

そもそも論

ここで紹介した方法はあくまで一時しのぎであり、理想的な解決策ではないと思います...

可能であれば、きちんと時間を取ってテストケースが失敗する原因を探り、それを修正するのが理想的だと思います。

ただし、不安定なテストケースの原因を探るのはそれなりに時間がかかったりするので、状況に応じてどう対処するかを適宜判断するのがよいのではないかと思います。

おわりに

以上、Jestで不安定なテストケースへの対策方法について解説しました。

少しでも参考になりましたら幸いです。

最後に、弊社ではエンジニアやデザイナーなどの職種で積極的に採用中です!

Wantedlyでも情報発信していますので、もし興味がありましたらぜひご覧いただければと思います!

Discussion

ログインするとコメントできます