並列テストを正しく行う方法:パイプラインが失敗する理由(そしてその解決策) 

すべてのQAテスターは、自動化スイートが時間とともに肥大化していくのを見る時の、あの耐え難い感覚を知っている。最初は5分で終わる簡単なスモークテストだったものが、徐々に2時間にも及ぶ大規模な実行へと発展していく。新しい機能が追加されるたびに、AppiumモバイルスクリプトやSeleniumブラウザフローがいくつか追加され、やがて開発者はイライラし始め、リリース管理者はアップデートを求め、QA部門はひっそりと不愉快なレッテルを貼られることになる。 配送のボトルネック. 

そこで、テストフレームワークの設定を開き、同時実行の設定を見つけて、「なぜやらないのか?」と考えるのです。 

スイッチを入れて、もう一度スイートを実行する。スピードアップのはずが、不可解なエラーだらけのビルドになってしまう。 

3時間後、全く同じコミットを再度実行します。今度は成功しました。次のビルドを待ちます。すると、さらに2つのテストが失敗しました。あなたは自分が正気を失っているのではないかと不安になります。 

実際にはパイプラインの速度は上がっていません。高価で高速なランダム障害発生装置を構築しただけです。 

 問題点:パラレルスイッチの神話

現代のテスト自動化における大きな誤解は、シーケンシャルテストからパラレルテストへの移行は単なる設定変更に過ぎないというものです。そうではありません。 根本的な建築上の変化. 

テストを順番に実行すると、まるで片側一車線の道路を走る礼儀正しいドライバーのように振る舞います。一つのテストが実行され、後処理を終えると、次のテストが開始できるように道を譲ります。一度に一つのことしか起こらないため、すべてが秩序正しく保たれます。 

基盤となるテストスイートをリファクタリングせずに並列処理に切り替えると、同じドライバを信号機のない混沌とした4車線高速道路に放り込み、なぜすぐに玉突き事故が起こるのか不思議に思うことになる。 

突然、個別に実行した場合は確実に合格していたテストが、並列実行になった途端、不可解な要素タイムアウト、ドライバ接続の切断、予期しないセッション終了などの理由でランダムに失敗し始める。 Jenkinsでビルドを再実行すると、失敗したテストは合格する。しかし、代わりに他の3つのテストが失敗する。 ハイゼンバグへようこそ。 

失敗する理由:4つの原因

順番に実行すると問題なく合格するテストが、並列実行すると途端にうまくいかなくなるのはなぜでしょうか? ほとんどの場合、それは一つの原則に帰着する。 互いの領域を侵すテスト. 

テストの不安定性に関する研究では、これらの不具合を明確なカテゴリに分類しています。第37回IEEE/ACM国際自動ソフトウェアエンジニアリング会議(ASE '22)で発表された文献レビューによると、不安定なテストは一般的に、テストベースの不安定性(欠陥のあるスクリプト、不安定な識別子、非同期待機、順序依存テスト)、環境ベースの不安定性(ネットワーク条件、リソース競合、複数環境の競合)、および製品ベースの不安定性(アプリケーション自体の競合状態とメモリリーク)の3つの原因に分類されます。以下の4つの原因は、並列テストスイートにおけるこれらのカテゴリの実際的で日常的な現れです。 

データ衝突:2つのテスト、1つのレコード 

2人が同時に全く同じスプレッドシートのセルを編集している状況を想像してみてください。テストAがユーザープロファイル(user_id: 101)を更新している最中に、テストBが同時にuser_id: 101を削除した場合、どちらか一方のテストがクラッシュします。どちらのテストも記述ミスがあったわけではなく、単に同じデータに対して競合が発生しただけです。 

並行世界では、この衝突は大規模に発生します。共有ステージングデータベースに対して多数のテストを同時に実行しているチームは、遅かれ早かれこの問題に直面する可能性が非常に高いでしょう。 

ドライバーリーク:リモコンの共有 

Seleniumでブラウザを制御する場合でも、Appiumでモバイルアプリを制御する場合でも、スクリプトには専用のドライバセッションが必要です。よくある落とし穴は、静的ドライバを複数のスレッドで誤って共有してしまうことです。 

// アンチパターン:並列スレッド間でドライバを共有する
公共 静的な WebDriverドライバー;

@テスト
公共 ボイド testA() {
  ドライバー.get(「https://example.com」);
  driver.findElement(By.id("参加する"))。クリック();
}

@テスト
公共 ボイド testB() {
  driver.findElement(By.id(「ユーザー名」)).sendKeys(「管理者」);
  // おっと: testA は現在別のページにいる可能性があります
} 

まるで2人がテレビのリモコンを取り合っているようなものだ。片方がページを切り替えている間に、もう片方がクリックしている最中。まさに大混乱だ。 

劇作家でさえ、 ブラウザコンテキスト インスタンスでは、Cookieとログインセッションが同時実行テスト間で引き継がれる可能性があります。 

// アンチパターン:Playwrightにおける共有コンテキスト
定数 sharedContext = 待つ ブラウザ.newContext();
// 複数のテストで共有コンテキストを使用する。セッションが重複する。 

解決策: 使用する スレッドローカル (Java)または ワーカーごとのコンテキスト隔離 (JavaScript/劇作家) 

BDD国家汚染:キュウリの罠 

Cucumberのようなフレームワークは、平易な英語でテストを書くのに非常に優れています。しかし、自動化エンジニアはシナリオの状態をグローバル変数に格納することがよくあります。 

// アンチパターン:Cucumberにおける共有ステップ状態
公共 静的な ユーザー loginInUser;

@ギヴン(「ユーザーがログインしました」)
公共 ボイド userLoggedIn() {
  ログインユーザー = NEW ユーザー(“alice@example.com );
}

@いつ(「ユーザーが注文を行う」)
公共 ボイド placeOrder() {
  // スレッド A が loggedInUser を上書きした可能性があります
} 

並列実行時、スレッド A はサイレントに上書きします ログインユーザー スレッド B が注文ページのチェックを途中で中断し、ランダムなアサーション エラーが発生する。解決策: 依存性注入 (PicoContainer、Spring)各シナリオインスタンスは独自のステートオブジェクトを所有します。 

「残余物」の落とし穴:放置されたデータとデバイス 

並列パイプラインの障害を引き起こす最悪の原因は、クリーンアップ処理の不足です。テストが実行中にクラッシュすると、後処理がスキップされ、「孤立データ」が残されます。例えば、ロックされたショッピングカート、重複したメールアドレス、次のテストが使用する前に適切にリセットされていないデバイスなどです。 

これは、物理および仮想のテストデバイス、そしてデータにも当てはまります。共有デバイスグリッドでは、テスト後にクリーンアップを行わない(アプリの状態が残っていたり、認証情報がキャッシュされていたり、古いアプリのインストールが残っていたりする)テストは、そのデバイス上で実行される次のテストを密かに破損させる可能性があります。 Digital.ai テストにおけるAppiumのベストプラクティスガイド この問題に直接対処するため、独立したテスト方法、静的変数の回避、並列実行における適切なドライバ管理、そしてセッション間のデバイスクリーンアップを組み合わせることで、グリッドを既知の良好な状態に維持します。 

逐次処理の世界では、次のテストが別の対象を調べるため、多少の不具合があっても問題ないかもしれません。しかし、並列処理の世界では、3秒後に別のワーカー スレッドがその不具合の上に乗り出し、エラーが発生します。これは連鎖的に起こり、1つのずさんなテストが、複数の下流テストに障害を引き起こす可能性があります。

その代償:いい加減さは隠れた税金

並列テストスイートが不安定になると、その影響はダッシュボード上の警告灯が点灯するだけにとどまりません。チーム文化を根本的に損ない、リソースを浪費することになります。 

確率の複利計算 

並列パイプラインの数学を考えてみましょう。UI テストスイートが小さな 1%のフレーク率それらのテストを50回連続して実行すれば、グリーンビルドが実現する可能性はかなり高くなります。 

しかし、これらの50個のテストを8つの並列ワーカーに分散して同時に実行すると、1%の不安定率はさらに悪化します。これは基本的な確率であり、業界で測定された統計ではありません。並列化によって不安定性が改善されるのではなく悪化する理由を示すための、例示的な数学です。 

パラレルワーカー  累積合格率  誤検出率 
1(順次)  99.5%  0.5% 
2  98.0%  2.0% 
4  96.1%  3.9% 
8  92.3%  7.7% 
16  85.2%  14.8% 

 

連鎖反応:複数のワーカーが競合状態(データ衝突、ロック競合、孤立セッション)を引き起こし、コードに問題がないにもかかわらずビルドが失敗する。開発者は、幸運にも正常な組み合わせに出会うまで、ビルドを何度も繰り返す。 

アラート疲労と再実行文化 

テストスイートがランダムに失敗すると、エンジニアリングチームはアラート疲労に陥ります。ASE '22のテストの不安定性に関する文献レビューで指摘されているように、特にジュニア開発者は「不安定なテストケースを無視したり、合格するまで繰り返し実行したりする傾向があり、潜在的に危険な欠陥が報告されないままになっている」のです。同じ調査では、不安定性の根本原因を調査するには時間がかかり、不安定性が誤報だった場合はコストが完全に無駄になることも指摘されています。このような状況は、チームが深く掘り下げて調査することを阻害し、「とにかく再実行すればいい」という習慣を助長します。 

並列処理スイートを再実行するたびに、実際の計算時間とクラウドグリッドの予算が消費されます。 正確なコストは、チームの規模、スイートの長さ、インフラストラクチャの価格によって異なりますが、方向性は予測可能です。根本的な分離の問題を解決しないチームは、分離によって防げたはずの問題に対して、エンジニアリング時間とインフラストラクチャ費用の両面で繰り返し費用を支払う傾向があります。

パターン:隔離されたテストワールドの構築

並列テスト間の競合を防ぐには、テスト間での情報共有を阻止する必要があります。各テスト実行環境は、それぞれ独自のプライベートな空間内で動作する必要があります。 

一時的なインフラストラクチャ:コンテナパターン 

現代のチームは、すべての並列ワーカーを単一のステージングデータベースに向けるのではなく、コンテナ化を利用した短命な環境を使用します。Docker を使用すると、ワーカー スレッドごとに専用の隔離されたデータベース コンテナをオンザフライで起動し、テストが終了するとすぐに破棄できます。Kubernetes ベースの CI ランナーはこれをさらに拡張し、パイプライン実行ごとに使い捨てのテスト環境全体をスケジュールします。 

モバイルおよびウェブの大規模なテストには、 Digital.ai テスト Appium、Selenium、クロスブラウザテストを並列実行するための実機クラウドグリッドを提供し、セッション間にデバイスのクリーンアップを行うことで、次のテストは前回の実行で残ったアプリデータや設定を引き継ぐことなく、既知のクリーンな状態から開始できます。  

Jenkinsパイプライン内の各ワーカースレッドは、それぞれ独立した環境を受け取ります。具体的には、動的なUUIDベースのテストユーザー、プライベートなDockerデータベースコンテナ、専用のブラウザ/デバイスコンテキストなどです。 

ブラウザとデバイスのコンテキストゾーニング 

Seleniumの場合、解決策はスレッドです。safe ドライバーの割り当て: 

// パターン: ThreadLocal ドライバーの分離
プライベート 静的な ファイナル スレッドローカルドライバースレッド = NEW ThreadLocal<>();

公共 静的な WebDriver getDriver() {
  if (driverThread.get() == ヌル){
    driverThread.set(NEW ChromeDriver());
  }
  return driverThread.get();
}

@アフターメソッド
公共 ボイド 取り壊す() {
  WebDriver driver = driverThread.get();
  if (ドライバー != ヌル){
    driver.quit();
    driverThread.remove();
  }
} 

Appiumの場合、グリッドがスレッドごとにクリーンで共有されていないデバイスインスタンスを動的にプロビジョニングするようにしてください。各ワーカーは独自のインスタンスを持つ必要があります。 AppiumDriver ユニークなセッション セッションIDまた、次のセッションがデバイスを使用する前に、そのデバイスをリセットする必要があります。  

これはまさに、 Digital.ai テスト このプラットフォームは、以下の機能をサポートするように設計されています。テストスクリプトは、各セッションの最後に quit() を呼び出す責任があり、プラットフォームはセッション間に独自の自動デバイスクリーンアップサイクルでそれをサポートしているため、個々のテストがどれだけ適切にクリーンアップを行ったかに関わらず、次の並列ワーカーは常にクリーンなデバイスを取得できます。 ガイダンスと組み合わせると Digital.ai テストにおける並列実行のベストプラクティス 静的変数の使用を避け、テスト方法を独立させることで、各デバイスをテスト実行間で常に正常な状態に維持することができます。 

劇作家として、 ブラウザコンテキスト オブジェクト:各コンテキストは独立したシークレットウィンドウのように機能するため、トークンとストレージは同時実行間で決して混ざり合うことはありません。 

// パターン: テストごとに分離された劇作家のコンテキスト
定数 ブラウザ = 待つ クロム.打ち上げ();
定数 コンテキスト1 = 待つ ブラウザ.newContext();
定数 コンテキスト2 = 待つ ブラウザ.newContext();
// 各コンテキストには独自のクッキー、localStorage、sessionStorageがあります 

合成データ生成:UUIDシールド 

複雑なデータベースのリセットを行わずにデータ衝突を防ぐにはどうすればよいでしょうか? 確実な方法の一つは、実行時に生成される一意の識別子(UUID)を使用することです。これにより、並列ワーカーが同じ環境に対して同時に実行され、同じレコードにアクセスすることがなくなります。 

// パターン: UUID ベースの合成データ
@テスト
公共 ボイド testCheckout() {
  String uniqueUserId = UUID.randomUUID().toString();
  ユーザーユーザー = NEW ユーザー(uniqueUserId + @ example.com 、uniqueUserId);
 
  // スレッド A は user-a1b2c3d4@example.com
  // スレッド B は user-x9y8z7w6@example.com
  // 衝突なし
} 

合成データ生成と一時的で自己クリーンな環境を組み合わせることで、チームはテストの依存関係を完全に排除できます。テストは共有状態をめぐって競合することがなくなり、それぞれが動的に作成されたクリーンなデータセットに対して実行されます。AI 生成テストデータや自動環境クリーンアップを含む、このようなパイプラインの構築に関する詳細については、以下を参照してください。 開発者のための合成データ生成と自己クリーンアップ型テスト環境ガイド.

プレイブック:最小限のロードマップ

既存のソフトウェアスイートを並列実行用にリファクタリングするのに、大規模な書き換えは必要ありません。手順は以下の4つです。 

  1. 監査状態管理。 静的フィールドと共有ドライバをスレッドに置き換えます。safe パターン: Cucumber の依存性注入、 スレッドローカル セレン/アピウムの場合、単離 ブラウザコンテキスト Playwright のインスタンス。デバイスとブラウザのインス​​タンスがセッション間で適切にクリーンアップされていることを確認してください。 
  1. 仕事量のバランスを取る。 CIシステムから得られるテスト実行時間の履歴を利用して、負荷の高いテストを早期に分散させることで、1人の作業者に負荷が集中するのではなく、すべての作業者がほぼ同時にテストを完了できるようにします。 
  1. 隔離措置を取り、その後段階的に規模を拡大する。 脆弱なテストや処理速度が制限されるテストには、順次実行するようにタグを付けます。まずは少数の並列ワーカーで開始し、発生した競合状態を修正してから、規模を拡大します。 
  1. 検証と反復を繰り返す。 規模を拡大する際には、誤検出率とビルド時間を追跡してください。再実行率の低下は、テストスイートが単に高速化しているだけでなく、実際に健全化している良い兆候です。
  1. 今後の展望:ゲートキーパーからスピードイネーブラーへ

今後の展望:ゲートキーパーからスピードイネーブラーへ

長年にわたり、QAは最終ゲートキーパーと見なされてきた。つまり、自動化されたテストスイートがスクリプトをゆっくりと実行している間、リリースを遅らせるチームだと考えられていた。テストは 減速した エンジニアリング。 

テストスイートの基盤となるアーキテクチャを修正することで(分離されたテストデータ、スレッド、safe Selenium、Playwright、またはAppiumでのセッション管理、およびテストグリッド上の適切にクリーンアップされたデバイス)並列テストは、その認識を変えることができます。かつて何時間もかかっていたテストスイートは、実際に数分で信頼性の高いフィードバックを返すことができます。 safe 大規模に運用するため。 

テストの未来は、より多くのテストをより速く実行することだけではありません。テストを実行することこそが未来なのです。   隔離された環境、清潔な環境、そして結果に対する確信をもって。 

参考文献と参考文献 

  1. Digital.ai テスト:並列テスト – ベストプラクティス: 独立したテスト方法、静的変数の回避、並列処理とログ記録、スレッドに関する公式ガイダンスsafe Appiumのドライバ管理。 
  1. 開発者のための合成データ生成と自己クリーンアップ型テスト環境ガイド合成データを用いて、一時的で自己浄化型のテスト環境を構築する。 
  1. Ngo, K.、Nguyen, V.、Nguyen, T. (2022)「テストの不安定性に関する研究:ユニットテストからシステムテストへ」 第37回IEEE/ACM国際自動ソフトウェアエンジニアリング会議(ASE '22)。不安定なテストをテストベース、環境ベース、製品ベースの発生源に分類し、それらを検出および修復するための学術的および産業的ツールを調査する文献レビュー。 
  1. Selenium WebDriverのドキュメント: Seleniumの公式ドキュメント(ドライバセッションとブラウザ自動化に関するもの)。 
  1. Selenium: テストごとに新しいブラウザを使用: テスト分離の実践に関するSeleniumの公式ガイダンス。 
  1. 劇作家:孤立(ブラウザコンテキスト)Playwrightがテスト分離のためにBrowserContextをどのように使用するかについての公式ドキュメント。 
  1. テストピラミッドマーティン・ファウラーによる、テストの分離とスコープの粒度に関する基礎的な参考文献。 
  1. Digital.ai テスト:拡張性の高いモバイルおよびWebクロスブラウザテストクラウド: 実際のiOS/Androidデバイスとブラウザ間で並列実行を行うためのエンタープライズインフラストラクチャ。 

 

お勧めの関連ガジェット