Flakyなテスト

欠陥テストとは?

時々失敗するテストですが、何度もやり直せば、最終的には合格します。

テストが不安定になる原因にはどのようなものが考えられますか?

状態リーク

ラベル flaky-test::state leak

説明以前のテストからデータの状態が漏れています。実際の原因は、おそらくこのテストに欠陥があることではありません。

再現の難しさ:中程度。通常は、失敗しているファイルまで同じスペックファイルを実行することで問題が再現します。

解決方法以前のテストや、テストデータや環境が変更された箇所を修正し、各テストの後に元のテストにリセットされるようにします。

  • 1:let_it_be で作成されたデータレコードがテスト例間で共有され、意図的または非意図的にモデルが変更され、テスト 例で同期していないデータが作成された場合、状態リークが発生する可能性があります。その結果、後続のテスト例でPG::QueryCanceled: ERROR が表示されたり、再試行されたりすることがあります。状態リークと解決オプションの詳細については、GitLab テストのベストプラクティスを参照してください。
  • 例 2: マイグレーションテストでデータベースをロールバックし、テストを実行した後、一貫性のない状態でデータベースをロールアップするかもしれません。
  • 3: あるテストが、後続のテストで使用するデータを変更しました。
  • 例 4: データベースのクエリのテストは、新しいデータベースではパスしますが、そのデータベースが以前のテスト シーケンスを処理するために使われている CI/CD パイプラインでは、テストが失敗します。これは、クエリ自体を更新して、きれいでないデータベースで動作させる必要があることを意味します。
  • 例 5: 非同期リクエストの関連性のないデータベース接続がチェックインし直されたため、テストが誤って関連性のない データベース接続を使用してしまいました。この失敗はこのマージリクエストで解決されました。
  • 6:データベース接続の最大有効時間が原因でこれらの接続が切断され、その結果、これらの接続上のトランザクションに依存するテストが失敗しました。このイシューはこのマージリクエストで修正されました。

データセット固有の

ラベル flaky-test::dataset-specific

説明このテストは、データセットが特定の (通常は限定された) 状態あるいは順序にあることを想定しています。

再現の難しさ:イシューを再現するために必要なデータ量は、ローカルでは難しいかもしれません。順序の問題は、テストを何度か繰り返し実行することで再現しやすくなります。

解決方法

  • データセットが特定の状態にあると仮定しないようにテストを修正し、ID をハードコードしないようにします。
  • もしテストが順序を気にせず要素だけを気にするのであれば、アサーションを緩めます。
  • 決定論的な順序を指定することで、テストを修正します。
  • 決定論的な順序を指定してアプリのコードを修正してください。

  • 1: テーブルに 500 以上のカラムがある場合、データベースは再作成されます。マージリクエストでは成功しても、master でテストの順序が変わると失敗する可能性があります。
  • 2: 存在しない ID を持つレコードを探そうとするとエラーメッセージが返されることを保証するテスト。このテストでは、存在しないはずのハードコードされた ID を使用します (たとえば、42) 4242このテストがテストスイートの初期に実行された場合、その前に十分な数のレコードが作成されていないため合格するかもしれません 42
  • 例 3:ORDER BY を指定しないと、データベースに決定論的な順序が与えられません。
  • 4.

ランダム入力

ラベル flaky-test::random input

説明このテストではランダムな値を使います。

再現の難しさ:テストが失敗したときに使われた「ランダムな値」を使うようにローカルでテストを修正できるので簡単。

解決方法:一度問題が再現されれば、テストかアプリのどちらかをデバッグして修正するのは簡単です。

  • 例 1: データ入力がランダムであるため、散発的にしか出現しない特定のデータを扱うには、テストが十分に頑健ではありません。

信頼できない DOM セレクタ

ラベル flaky-test::unreliable dom selector

説明テストに使われた DOM セレクタは信頼できません。

再現の難しさ:中程度から困難。DOM セレクタが重複しているか、遅延の後に表示されるかなどによります。API やコントローラに遅延を追加することで、イシューを再現できる可能性があります。

解決方法この問題は、本当に問題によって異なります。リクエストの終了を待つ、ページをスクロールする、などです。

  • 1: 複数の要素にマッチする一意でない CSS セレクタや、element not found エラーをスローする前にレンダリング時間を許容しない、待機しないセレクタメソッド。
  • 2:CSS セレクタは、GraphQL リクエストが終了し、UI が更新された後にのみ表示されます。
  • 例 3: 偽陽性のテストで、Capybara はページ訪問後、ページが完全にロードされていない場合、または要素が webdriver によって検出されない場合(ビューポートの外や他の要素の後ろにレンダリングされている場合など)にすぐに true を返します。

時間依存

ラベル flaky-test::datetime-sensitive

説明このテストは特定の日時を想定しています。

再現の難易度:テストが特定の日付以降に一貫して失敗するか、あるいは特定の日付や時刻にのみ失敗するかによって異なります。

解決方法:通常、時間を凍結するのが良い解決策です。

  • 1: 時間が経過した後にテストが中断する場合。
  • 2: 月の最終日にテストが中断する場合。

不安定なインフラ

ラベル flaky-test::unstable infrastructure

説明インフラストラクチャのイシューによりテストが失敗することがあります。

再現の難しさ:難しい。CIインフラの問題を再現するのは本当に難しい。ローカルでコンテナを使えば可能かもしれません。

解決方法専用のイシューでインフラ部門と話を始めるのが通常良いアイデアです。

  • 1:ランナーは現在、高負荷状態にあります。
  • 2:ランナーはネットワークの問題を抱えており、ジョブを早期に失敗させています。

隔離されたテスト

master に欠陥のあるテストがある場合:

  1. 失敗::flaky-test “イシューを関連するグループラベルで作成します。
  2. 最初の失敗の後、テストを隔離します。そのテストがタイムリーに修正できない場合、開発者全員の生産性に影響があるので、隔離すべきです。

RSpec

高速検疫

マージリクエストを開いたりパイプラインを待ったりせずにテストをすばやく隔離するには、高速隔離プロセスに従います。

長期隔離

テストが高速検疫されると、長期検疫プロセスに進むことができます。これは、マージリクエストを開くことで実行できます。

まず、テスト ファイルがfeature_category メタデータを持っていることを確認し、テスト ファイルの正しい帰属を確認します。

次に、quarantine: '<issue url>' メタデータを、以前に作成した~"failure::flaky-test "イシューのURLで使用することができます。

it 'succeeds', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345' do
  expect(response).to have_gitlab_http_status(:ok)
end

これはCIでスキップされることを意味します。デフォルトでは、隔離されたテストはローカルで実行されます。

--tag ~quarantine で実行することで、ローカル開発でもテストをスキップすることができます:

bin/rspec --tag ~quarantine

長期隔離MRが本番環境に到達したら、先に作成した高速隔離MRを元に戻してください。

Jest

Jest specの場合、jest/no-disabled-tests ESLintルールを無効にし、イシューのURLを含めるために、eslint-disable-next-line コメントとともに.skip メソッドを使用することができます。以下に例を示します:

// quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/56789
// eslint-disable-next-line jest/no-disabled-tests
it.skip('should throw an error', () => {
  expect(response).toThrowError(expected_error)
});

これは、--runInBand Jestコマンドラインオプションでテストスーツが実行されない限り、スキップされることを意味します:

jest --runInBand

隔離されたスペックを含むファイルのリストは、コマンドで見つけることができます:

yarn jest:quarantine

どちらのテスト・フレームワークでも、イシューに~"quarantined test" ラベルを追加してください。

テストが隔離されると、3 つの選択肢があります:

  • テストを修正する(つまり、その薄っぺらさを取り除く)。
  • テストのレベルを下げます。
  • テストを完全に削除します(たとえば、すでに下位のテストがある、別の同じレベルのテストと重複している、テストが多すぎるなど)。

自動再試行と欠陥テストの検出

私たちの CI では、RSpec::Retry を使って失敗したサンプルを自動的に数回リトライしています(正確なリトライ回数はspec/spec_helper.rb を参照)。

また、カスタムのRspecFlaky::Listenerも使っています。このレポーターはmaster ブランチのmaintenance スケジュールパイプラインのupdate-tests-metadata ジョブで実行され、失敗したサンプルをrspec/flaky/report-suite.jsonに保存します。レポートファイルはretrieve-tests-metadata ジョブによってすべてのパイプラインで取得されます。

これはもともとhttps://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13021.

リトライを内部で有効にしたい場合は、RETRIES 環境変数を使用します。インスタンスンスRETRIES=1 bin/rspec ... は、失敗したサンプルを1回再試行します。

ローカルでレポーターを生成するには、FLAKY_RSPEC_GENERATE_REPORT 環境変数を使用します。例えば、FLAKY_RSPEC_GENERATE_REPORT=1 bin/rspec ...

rspec/flaky/report-suite.json レポートの使用法

rspec/flaky/report-suite.json レポートは、内部ダッシュボードで監視するために、1 日 1 回Snowflake にインポートされます。

GitLabで過去に発生した問題

順序依存の欠陥テスト

これらの Flaky テストは、他のテストとの実行順序によって失敗することがあります。たとえば

このような失敗を引き起こすテストを特定するために、scripts/rspec_bisect_flaky を使用することができます:

  1. まず、失敗したテストの前に実行されたテストのリストを取得します。CI ジョブの出力ログのKnapsack node specs: からリストを検索できます。
  2. specのリストをファイルとして保存し、実行します:

    cat knapsack_specs.txt | xargs scripts/rspec_bisect_flaky
    

順序依存のイシューがある場合、上記のスクリプトは最小限の再現を印刷します。

時間依存の不安定なテスト

配列順序の期待

フィーチャーテスト

カピバラ期待タイムアウト

仕様のハングアップ

specがハングする場合、Railsのバグが原因かもしれません:

提案

テストファイルの分割

コンテキストを絞り込んで問題のあるテストを特定するために、大きな RSpec ファイルを複数のファイルに分割することが役立ちます。

リソース


テストのドキュメントに戻る