E2E の実行時間を削る — 実行 OS の分類・バンドル実行・シナリオ統合
「ぽんっ!」(旧称:でいりんぐ)の E2E は patrol で書いていて、シナリオは 42 本あります。実機・シミュレータで実際にサインインし、画面を操作して確かめる形なので、素直に全部回すと相応の時間がかかります。
E2E はコミット前に回すものとして運用しているので、実行時間はそのまま開発のテンポに効きます。ここでは、テストの守備範囲を狭めずに実行時間を抑えるために入れた 3 つの設計と、その判断基準を残します。
削減の方針: 「情報が増えない実行」をやめる
最初に決めたのは、削減の対象をどう選ぶかです。テストを減らす方向は取りません。代わりに、回しても新しい情報が得られない実行を特定して、そこだけを削ります。
この観点で見ると、時間の使い先は 3 つに分かれていました。
- 同じシナリオを 2 つの OS で回す時間
- シナリオごとに繰り返されるビルド・インストール・起動の時間
- シナリオごとに繰り返されるサインインの時間
順番に見ていきます。
1. 実行 OS を分類する
すべてのシナリオを Android と iOS の両方で回すルールにしていましたが、大半のシナリオは「データを仕込んで、Dart で書かれた UI の表示と操作を確かめる」内容です。この種の検証は、OS が変わっても通る/通らないが変わりません。
そこで判定基準を、そのシナリオの検証対象が OS 実装に依存するかの一点に置き、依存する条件を列挙しました。
| # | 両 OS で回す条件 | OS 差の中身 |
|---|---|---|
| N1 | OS ライフサイクル操作(ホームへ戻す・復帰させる) | プロセス復帰の挙動が異なる |
| N2 | OS のパーミッションダイアログ | iOS 固有のものがあり、出方も異なる |
| N3 | アプリ内課金 | ストアの課金基盤そのものが別 |
| N4 | WebView | 実装エンジンが別 |
| N5 | 広告 SDK | プラットフォームごとに SDK が別 |
| N6 | 資格情報の永続化・アカウントの実作成/実削除 | 保管先とプラグイン経路が別 |
どれにも当てはまらないシナリオは Android のみで回します。結果として、iOS で回すのは 42 本中 11 本になりました。
共通の前処理は分類根拠にしない
判断が割れたのが、ほぼ全シナリオの前処理にある UI からのサインインです。これはネイティブ依存の処理なので、厳密に言えば全シナリオが「OS 依存」に分類されてしまいます。
ただ、それを認めると分類そのものが機能しません。同じ前処理を 42 回検証しても、2 回目以降に新しい情報はないからです。そこで、共通の前処理は分類根拠にせず、代表シナリオ 1 本が各 OS で担保するというルールにしました。
「同じことを何度確かめても情報は増えない」という観点は、この分類の全体を貫く判断基準になっています。
2. 分類の正をどこに置くか
分類そのものより悩ましいのが、分類をどこに書くかです。候補は 2 つありました。
- ドキュメント(シナリオ一覧の表)
- 実行スクリプト
ドキュメントだけに置くと、実行には反映されません。スクリプトだけに置くと、シナリオを追加した人が別ファイルの更新に気づかない可能性があります。どちらも「更新されないまま放置されうる」形です。
最終的に、分類の正はテストコード自身に置きました。patrol はテストにタグを付けられるので、シナリオの定義にそのまま書きます。
patrolTest(
'おとなフロー: サインインからやること追加まで',
tags: const ['ios'], // このシナリオは iOS でも回す
($) async {
// ...
},
);
実行時は、iOS のときだけタグで絞り込みます。この形にすると、シナリオを追加する人が触るファイルの中に分類があるので、他のファイルを更新し忘れても分類が抜け落ちません。
ドキュメントの一覧表は、人が全体像を把握するために残しています。ただし 2 か所に同じ情報があるので、起動時にタグと一覧表を突き合わせ、食い違っていたらその場で止めるようにしました。
# 一覧表の「実行OS」列と、テストに付いたタグを突き合わせる
for scenario in $RUN_SCENARIOS; do
doc_says_both_os "$scenario" && doc=1 || doc=0
scenario_has_ios_tag "$scenario" && tag=1 || tag=0
[ "$doc" != "$tag" ] && drift=1 && warn "分類がズレています: $scenario"
done
[ "$drift" = 1 ] && exit 1
ドキュメントと実行の一致を「気をつける」で担保するのはうまくいきません。二重に持つと決めたなら、一致を検査する仕組みまでセットにするのが前提だと考えています。検査で落ちれば、どちらが正しいかを考えるきっかけがその場で生まれます。
3. ビルドを 1 回にまとめる
次に大きかったのがビルド時間です。シナリオごとにテストコマンドを起動していたので、ビルド・インストール・起動がシナリオの本数だけ走っていました。テスト操作そのものより、待ち時間のほうが長い状態です。
patrol は対象を複数渡すとバンドルして 1 ビルドにまとめられるので、そちらに寄せました。実測では 3 シナリオが 1 ビルド 137 秒で、個別に回すと約 6 分かかっていたところです。削減幅はシナリオ数に比例します。
合否の帰属だけは崩さない
まとめて実行すると、結果の集約はコマンド任せになります。ここで気をつけたのは、どのシナリオが通ってどれが落ちたのかを個別に確定させることです。E2E は落ちたときに原因を絞り込むためにあるので、「まとめて失敗しました」で終わる状態にはできません。
そこで、バンドル実行のログとシナリオ一覧を突き合わせ、結果を取得できなかったシナリオだけを単独で流し直す手順を入れました。
# バンドル実行のログから、シナリオごとの結果を回収する
for scenario in "${scenarios[@]}"; do
case "$(result_from_log "$bundle_log" "$scenario")" in
PASS:*) record_pass "$scenario" ;;
FAIL:*) record_fail "$scenario" ;;
*) missing+=("$scenario") ;; # 取れなかったものは後で単独実行
esac
done
# 取りこぼしは1本ずつ流し直し、合否をシナリオ本人に帰属させる
for scenario in "${missing[@]}"; do
run_single "$scenario"
done
速さのために合否の解像度を落とすのは、E2E では割に合いません。高速化の副作用で診断能力が下がっていないかを、削減とセットで確認する形にしています。
4. 前提が同じシナリオを 1 ケースにまとめる
ビルドが 1 回になっても、サインインとアプリ起動はケースごとに走ります。ここも、前提(サインインするアカウント・仕込むデータ)が同じシナリオをまとめれば削れます。
2 グループを統合しました。
- 匿名状態のおとな導線の 4 本 → 1 本(起動と匿名サインインが 4 回から 1 回に)
- おとなの設定まわりの 5 本 → 1 本(サインインが 5 回から 1 回に)
統合のコストと、その緩和
統合には明確なコストがあります。途中で落ちると、それ以降の確認まで届かないことです。5 本を 1 本にすると、2 番目で落ちたときに残り 3 つの状態が分かりません。
これを受け入れるかどうかの判断は、「落ちたときに何が分からなくなるか」と「毎回どれだけ時間を使うか」の比較になります。設定まわりの確認は互いに独立で、1 つ落ちれば原因もその場で分かることが多いので、統合する側に倒しました。
そのうえで、追える形を保つために次を守っています。
- ファイルの先頭に、統合前のシナリオとの対応表を書く
- 本文をセクションで区切り、どこまで進んだかがログで分かるようにする
- 画面の操作は上から下へ一方向に並べる(自動スクロールは下方向にしか進まないため、行き来させると要素を見つけられなくなる)
統合しないと決めたもの
通知設定のトグルは、統合の対象から外しました。オンにすると端末の通知許可状態しだいで説明ダイアログが割り込み、同じケースに入っている後続の操作まで巻き込むためです。
統合の判断基準は「前提が同じか」だけではなく、「そのシナリオが他のシナリオの実行環境を変えないか」も含まれます。割り込みを起こす操作は、独立させておいたほうが安上がりです。
5. 同時に走らせない
もう 1 つ、実行時間ではなく信頼性の話ですが、E2E は同時に 1 つしか走らせないことにして、実行スクリプトにロックを入れました。
全シナリオが共通のテストアカウントを使い、データを書き換えながら進むためです。2 つの実行が並ぶと互いのデータを書き換え合い、テストの内容とは無関係な理由で結果が変わります。
並列化は実行時間の削減として真っ先に思いつく手段ですが、共有状態を持つテストでは並列化の前に分離のコストがかかります。テストアカウントを実行単位で分ける方向もありますが、いまの規模ではロックで直列化するほうが、得られる時間に対して構成が単純に収まると判断しました。
まとめ
- 削減対象は「テストの本数」ではなく「情報が増えない実行」で選ぶ
- 実行 OS は「検証対象が OS 実装に依存するか」で分類する。共通の前処理は分類根拠にしない(代表 1 本で担保する)
- 分類の正はテストコードの隣に置く。ドキュメントにも書くなら、一致を検査する仕組みまでセットにする
- ビルドはまとめられる。ただし合否の帰属は個別に確定させ、取りこぼしは単独で流し直す
- 前提が同じシナリオは統合できる。統合しない判断基準は「他のシナリオの実行環境を変えるか」
- 共有状態を持つテストは、並列化より先に直列化の担保を入れる
E2E の高速化は、テストの守備範囲や診断能力とのトレードオフになりがちです。どちらを削っていないかを都度確認しながら進めると、削減の判断がぶれずに済みました。