筆者は Optics Framework の開発に関わっています。この記事のコマンドと出力は、Optics Framework 1.11.0(macOS、Python 3.12)で実際に実行したものです。
よくある「緑なのに壊れている」問題
カートに商品を入れるテストを考えます。サインインし、商品を開き、カートに追加し、「Added to cart」が表示されることを確認する。すべてのステップが通ります。
ところが、最後の画面には「Your session expired. Please sign in again.」と出ている。前夜のビルドでトークンの更新が壊れていたのですが、テストは「Added to cart」しか見ていないので、CI は緑のままです。
エラー画面は、どこに出るか予測できません。Android の「〇〇が停止しました」ダイアログ、「応答していません」、「Something went wrong」のページ、オフラインのバナー。全ステップの後にすべてをアサートすれば、テストは倍に膨れ上がり、それでも次に追加されるメッセージは見逃します。
エラー定義ファイル
Optics Framework には、この問題のための「エラー定義」という機能があります。「これが出たら異常」というメッセージを CSV に並べておくと、実行の最後に画面を読んで、どれかが出ていないかを調べます。
プロジェクト内の任意の場所に、次のような CSV を置きます。
error_code,match_string,description,severity
ERR_CRASH,has stopped,The app crashed,critical
ERR_SESSION,Session expired,The user was signed out,high
ERR_SERVER,Something went wrong,Server error page,high
-
match_string:画面上で探す文字列 -
error_code:レポートに出る短い名前 -
description、severity:任意。レポートにそのまま書き出されます
error_code と match_string の列があれば、Optics Framework がエラー定義ファイルだと判断します。設定やキーワードの追加は不要で、optics execute のたびに使われます。
実際に試す
「Added to cart」と、セッション切れのバナーが両方出るページを用意しました。
<h1>Birch Shop</h1>
<p>Added to cart</p>
<div class="banner">Your session expired. Please sign in again.</div>
テストは「Added to cart」があることだけを確認します。
module_name,module_step,param_1,param_2
Open Cart,Launch App,file:///path/to/site/cart.html
Check Cart,Assert Presence,${added}
Element_Name,Element_ID
added,text=Added to cart
実行すると、テスト自体は通ります。
└── TC1 Add To Cart | 0.98s | PASS
├── Open Cart | 0.85s | PASS
│ └── Launch App (file:///...) | 0.84s | PASS
└── Check Cart | 0.13s | PASS
└── Assert Presence (text=Added to cart) | 0.13s | PASS
│ Total Test Cases: 1 | Passed: 1 | Failed: 0 │
一方で、execution_output/execution_logs.log には警告が出ています。
WARNING - ON-SCREEN ERROR DETECTED — code: ERR_SESSION | matched_on: match_string | match_string: 'Session expired' | severity: high | The user was signed out
そして execution_output/detected_errors_<セッションID>.json が作られます。
{
"timestamp": "2026-10-20T18:29:06.472038+05:30",
"detected": [
{
"error_code": "ERR_SESSION",
"matched_on": "match_string",
"match_string": "Session expired",
"description": "The user was signed out",
"severity": "high",
"detected_at": "end_of_run"
}
]
}
マッチは大文字・小文字を区別せず、部分一致です。Session expired で「Your session expired. Please sign in again.」も拾えています。
どのテキストを読んでいるか
チェックには OCR を使わず、ドライバーがすでに持っている UI ツリーを読みます。Web ではタグを取り除いた、画面に見えるテキストだけを対象にします。Android・iOS では text、content-desc、label、value、hint、name 属性です。
id や class 名は見ないので、上の例の class="banner" や、id="error_retry" のようなボタンで誤検知することはありません。コストは、実行の最後のページソース取得 1 回分だけです。
CI を落とす
JSON ファイルはエラーが見つかったときにしか作られません。なので、スイートの後に「ファイルがあれば失敗」というステップを 1 つ足せば、CI を赤にできます。
- name: Run the suite
run: optics execute birch-tests
- name: Fail on error screens
run: |
if compgen -G "birch-tests/execution_output/detected_errors_*.json" > /dev/null; then
cat birch-tests/execution_output/detected_errors_*.json
exit 1
fi
ジョブのログに、どのエラーコードとメッセージが出ていたかが表示されるので、スクリーンショットを開かなくても原因がわかります。古い execution_output が残っていると前回のファイルを拾ってしまうので、クリーンなチェックアウトで実行してください。
match_string の選び方
- エラーのときにしか出ない言い回しを選ぶ。「has stopped」は良い例で、「Error」は広すぎます(エラーについてのヘルプページにも当たります)。
- エラー画面のスクリーンショットから正確な文言をコピーし、短くする。マッチは単純な文字列比較なので、
*や.*はその文字そのものとして扱われます。 - アプリが意図的に表示する文言は入れない。最後の画面のどこかに出ていれば当たってしまいます。
注意点
- チェックするのは「実行が終わった時点の画面」です。Suite Teardown でアプリを閉じていると読む画面がなくなるので、閉じるステップは teardown から外してください。
- フローの途中で一瞬だけ出るメッセージは拾えません。途中の画面は、次に期待する画面をアサートして確認します。
- Python や Robot Framework から使う場合は、
Add Error Definitionsを一度呼び、好きなタイミングでCapture And Detectを呼べば、テストの途中でもチェックできます。
なぜ便利か
既存のテストを 1 行も変えずに、すべてのテストにこのチェックが付きます。1 つの CSV で Android・iOS・Web・TV の実行をカバーでき、サポート窓口が新しいエラーを見つけたら、行を 1 つ足してプルリクエストでレビューするだけです。
インストールと最初のテストは、Zenn の記事 にまとめています。