クラウドMacでSafari WebDriver回帰ゲートを構築する

クラウドMacでSafari WebDriver回帰ゲートを構築する

リモートチームがWebページの変更をマージした後、見落としやすいのはChromiumのパスではなく、Safari固有のフォーム、フォーカス、レイアウトの挙動です。リリース直前になってリモートデスクトップを開き、ページを一つずつ操作するよりも、GPUMiniのクラウドMacにSafari WebDriverの回帰ジョブを常設しておくほうが効率的です。デプロイ候補が作成されるたびに同じ主要フローを実行し、失敗時には後から検証できるページの状態を保存します。

再現可能な実行境界を先に固定する

Safariの自動化はmacOSのグラフィカルセッションに依存するため、ヘッドレスブラウザと同じ実行方式をそのまま適用することはできません。まず専用のテストユーザーを作成し、グラフィカルセッションをログイン状態に保ち、ジョブと人によるリモート操作が同時に行われないようにします。同じユーザーディレクトリではSafariセッションを一度に一つだけ実行し、履歴、ダウンロードファイル、ウィンドウ状態の相互汚染を防ぎます。

ノードを初めて準備するときは、対話型ターミナルでドライバーの有効化と診断を実行します。

sudo safaridriver --enable
safaridriver --diagnose
mkdir -p "$HOME/browser-tests/artifacts"

--enableはノードの初期準備に含まれる操作であり、毎回の自動ジョブに組み込んではいけません。通常のジョブでは診断だけを実行し、診断に失敗した場合はテストを停止します。原因を特定しにくいタイムアウト結果を大量に生成したまま処理を続けないでください。Python環境でも依存関係のバージョンを固定し、実行のたびに最新版をインストールするのではなく、特定バージョンのseleniumをプロジェクトの依存関係ファイルに記録します。

ブラウザ回帰テストの最初の基準は「起動できること」ではありません。同じコミット、同じテストデータ、同じ待機条件から、毎回同じ結論を得られることです。

ローカルフィクスチャで外部要因の変動を遮断する

ログイン、検索、チェックアウトなどのエンドツーエンドフローはテスト環境に接続できますが、コンポーネントの操作やブラウザ互換性の確認には、できる限りローカルフィクスチャを使用します。フォーム、モーダル、アップロード、キーボードナビゲーションのページをリポジトリに格納し、ループバックアドレス経由で配信すれば、DNS、証明書、外部APIの変動を排除できます。

set -euo pipefail

ROOT="$(cd "$(dirname "$0")" && pwd)"
ARTIFACTS="$ROOT/artifacts"
mkdir -p "$ARTIFACTS"

python3 -m http.server 8080 \
  --bind 127.0.0.1 \
  --directory "$ROOT/fixtures" \
  >"$ARTIFACTS/http-server.log" 2>&1 &
SERVER_PID=$!

cleanup() {
  kill "$SERVER_PID" 2>/dev/null || true
}
trap cleanup EXIT INT TERM

"$ROOT/.venv/bin/python" "$ROOT/tests/safari_smoke.py"

固定ポートを使用する前に、lsof -nP -iTCP:8080 -sTCP:LISTENで使用状況を確認します。複数のジョブが同じノードを共有している場合、プロセスを無作為に終了してはいけません。スケジューラー側で排他実行を保証するか、ジョブごとに明示的なポートを割り当て、そのポートをテスト設定に記載します。

待機条件をビジネス上の状態として定義する

最も壊れやすい実装は、クリック後に一定秒数だけスリープする方法です。マシンの負荷が少し変わるだけで、待機時間が短ければ誤検知が発生し、長ければテストスイート全体が遅くなります。代わりに、ボタンがクリック可能になる、結果一覧が表示される、ステータステキストが期待値に変わるといった、観測可能なビジネス状態を待機条件にします。

from pathlib import Path
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

artifacts = Path("artifacts")
driver = webdriver.Safari()
driver.set_page_load_timeout(20)

try:
    driver.get("http://127.0.0.1:8080/login.html")
    wait = WebDriverWait(driver, 12)

    wait.until(
        EC.visibility_of_element_located((By.ID, "email"))
    ).send_keys("tester@local.invalid")

    driver.find_element(By.ID, "continue").click()

    status = wait.until(
        EC.visibility_of_element_located((By.ID, "result"))
    )
    assert status.text == "Ready"
finally:
    driver.quit()

ロケーターには、安定したidまたはテスト専用属性を優先して使用し、レイアウト変更の影響を受ける階層セレクターには依存しないようにします。アサーションでは、要素の存在だけでなく結果そのものも検証する必要があります。送信ボタンが表示されていても送信成功を意味するわけではなく、一覧コンテナが存在していてもデータのレンダリングが完了しているとは限りません。

失敗時の状態を毎回再現できるようにする

ブラウザテストで最もコストが高いのは、「CIにはタイムアウトと表示されているが、現場の状態が何も残っていない」ケースです。各テストケースが失敗した時点で、スクリーンショット、ページソース、現在のURL、テストケース名、時刻情報を直ちに保存します。スイート全体の終了後にだけスクリーンショットを撮っても、多くの場合、すでに遷移または閉じられたページしか記録できません。

失敗の症状 最初に保存するもの 優先して確認する項目
要素が一向に表示されない スクリーンショット、ページソース オーバーレイ、レスポンシブレイアウト、ロケーター
クリックしても反応がない 現在のURL、ボタンの状態 無効状態、フォーカス、イベントバインディング
ページ読み込みがタイムアウトする URL、サービスログ ローカルサービス、リダイレクト、リソースのブロック
ドライバーがセッションを作成できない ドライバー診断 グラフィカルセッション、残存プロセス、権限

スクリーンショットのファイル名には、テストケースの識別子と一意の実行番号を含めます。ただし、トークン、メールアドレス、プロジェクトのシークレットを含めてはいけません。ページソースにもテストデータが含まれる可能性があるため、アーカイブ前に同様にマスキングし、明確な保存期限を設定します。

例外発生時に直ちに保存する

テストフレームワークの失敗フックから、共通の証跡保存関数を呼び出します。保存処理自体が失敗する場合もあるため、スクリーンショットとページソースの書き込みエラーは個別に捕捉し、アーカイブ処理の例外が元のアサーションを上書きしないようにします。

むやみに再試行せず、残存セッションを回復する

ジョブが中断されると、Safariまたはドライバープロセスがセッションを占有したまま残ることがあります。その状態で次の実行をすぐに再試行すると、セッション作成の失敗が連続して発生しがちです。まず、テストユーザーが所有する実行中のジョブが残っていないかを確認し、ほかの正常なテストが実行されていないことを確かめたうえで、残存セッションを終了します。共有ノードでは、広範なプロセス名を指定した一括終了を行ってはいけません。

再試行が適しているのは、既知の一時的なセッション作成エラーだけであり、回数も最大1回に制限します。アサーションの失敗、ページ構造の変更、ビジネス状態の不一致を自動的に再試行してはいけません。実際の回帰が偶発的な問題に見せかけられてしまうためです。1回の再試行で回復した場合は、ジョブを完全に正常と扱うのではなく、不安定なイベントとして個別に記録します。

最後に、ゲートを2段階に分けます。コミットごとにローカルフィクスチャを使ったスモークテストを実行し、デプロイ候補では少数の主要なエンドツーエンドフローを追加で実行します。リリース前には、ドライバー診断が成功していること、テストユーザーが排他的に使用されていること、フィクスチャサービスにアクセスできること、明示的な待機に固定スリープが含まれていないこと、失敗時の証跡を読み取れること、クリーンアップ関数が必ず実行されることを確認します。これにより得られるのは「たまに成功する」ブラウザスクリプトの寄せ集めではなく、変化を長期的に追跡できるSafari回帰テストのベースラインです。

よくある質問

グラフィカルログインセッションなしでSafari WebDriverを実行できますか?

安定した実行方式としては推奨できません。専用テストユーザーのmacOSグラフィカルセッションを維持し、ジョブ開始前にドライバー診断を行います。

1台のクラウドMacで複数のSafariセッションを並列実行できますか?

まずはユーザー環境ごとに1セッションを直列実行します。処理量を増やす場合は、同じブラウザプロファイルを共有せず独立ノードへ分散します。

Safariテスト失敗時に残すべき証跡は何ですか?

失敗画面、ページソース、現在のURL、テスト名、ドライバー診断結果を保存します。必要なアプリケーションログは機密値を除去して添付します。

専用物理ノード

開発やビルド作業に使うクラウドMacを選ぶ

2種類のM4構成、4種類のレンタル期間、販売中の4つのノードを比較し、用途に合わせてデプロイします。

構成を選んで注文する