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
)42
。42
このテストがテストスイートの初期に実行された場合、その前に十分な数のレコードが作成されていないため合格するかもしれません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
説明このテストは特定の日時を想定しています。
再現の難易度:テストが特定の日付以降に一貫して失敗するか、あるいは特定の日付や時刻にのみ失敗するかによって異なります。
解決方法:通常、時間を凍結するのが良い解決策です。
例
不安定なインフラ
ラベル flaky-test::unstable infrastructure
説明インフラストラクチャのイシューによりテストが失敗することがあります。
再現の難しさ:難しい。CIインフラの問題を再現するのは本当に難しい。ローカルでコンテナを使えば可能かもしれません。
解決方法専用のイシューでインフラ部門と話を始めるのが通常良いアイデアです。
例
隔離されたテスト
master
に欠陥のあるテストがある場合:
- 失敗::flaky-test “イシューを関連するグループラベルで作成します。
- 最初の失敗の後、テストを隔離します。そのテストがタイムリーに修正できない場合、開発者全員の生産性に影響があるので、隔離すべきです。
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で過去に発生した問題
-
rspec-retry
は、いくつかの API 仕様が失敗したときに私たちを苦しめます:https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/9825 -
PG::UniqueViolation
による散発的なRSpecの失敗:https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/9846 - ffaker は、テストが処理できないようなおかしなデータを生成します (テストは予測可能であるべきなので、それはよくありません!):
-
spec/mailers/notify_spec.rb
をもっと堅牢に:https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10015 -
spec/requests/api/commits_spec.rb
における過渡故障:https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/9944 - 工場出荷時のデータをシーケンスに置き換えます:https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10184
- spec/finders/issues_finder_spec.rbの一時的な失敗:https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10404
-
順序依存の欠陥テスト
これらの Flaky テストは、他のテストとの実行順序によって失敗することがあります。たとえば
このような失敗を引き起こすテストを特定するために、scripts/rspec_bisect_flaky
を使用することができます:
- まず、失敗したテストの前に実行されたテストのリストを取得します。CI ジョブの出力ログの
Knapsack node specs:
からリストを検索できます。 -
specのリストをファイルとして保存し、実行します:
cat knapsack_specs.txt | xargs scripts/rspec_bisect_flaky
順序依存のイシューがある場合、上記のスクリプトは最小限の再現を印刷します。
時間依存の不安定なテスト
- https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10046
- https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10306
配列順序の期待
フィーチャーテスト
- 演習を開始する前に、テストに必要なすべてのデータを作成してください:https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12059
- ビス https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12604
- ビス https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12664
- ページのコンテンツに対してではなく、基礎となるデータベースの状態に対してアサートします:https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10934
- JSテストでは、Capybaraがクリックを送信したタイミングで要素が移動すると、Capybaraが誤クリックする可能性があります。
- イベントハンドラが設定される前にJSイベントをトリガすること。
- Markdown 画像の
src
属性でアサートする場合、画像が遅延ロードされるのを待ちます。 - フラッシュ告知バナーに対するアサートは避けてください。
Capybaraのビューポートサイズに関する問題
- spec/features/issues/filtered_search/filter_issues_spec.rbの一時的な失敗:https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10411
Capybara JSドライバ関連のイシュー。
- AJAXリクエストが発生していないとき、AJAXを待たないでください:https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10454
- ビス https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12626
カピバラ期待タイムアウト
仕様のハングアップ
specがハングする場合、Railsのバグが原因かもしれません:
- https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81112
- https://gitlab.com/gitlab-org/gitlab/-/issues/337039
提案
テストファイルの分割
コンテキストを絞り込んで問題のあるテストを特定するために、大きな RSpec ファイルを複数のファイルに分割することが役立ちます。
リソース
- 不安定なテスト:本当に再実行しますか?
- 脆弱なテストに対処し、排除する方法
- Railsのテストスイートでflakyに対処するためのヒント
- フレーキー」テスト: 短編
- インサイトを使用して、欠陥のあるテスト、遅いテスト、失敗したテストを発見する方法