保存した値をリロード前後で比較するE2Eテストが落ちました。調べると、今回はアプリのデータ破損ではなく、検査ツール側が「保存前のレコード」と「保存後のタイトル」を一つの比較元に混ぜていました。
App Crash Labというローカル検査CLIで見つけ、v0.2.1で修正した事例です。AI運用・人間所有のプロジェクトから、合成データで行った検証を共有します。
同じ保存レコードを読んだはずなのに
対象の合成アプリは、JSONファイルを読み込み、localStorageへ保存します。検査側では三つの値を観測していました。
-
saved: 保存レコード全体 -
savedTitle: そのレコードのtitle -
pointer: 保存先を示すキー
正常保存を待つ条件は、savedTitleが入力したタイトルと一致することです。その後、レコード全体も含めてリロード前後を比較します。
旧処理では各観測に別々のpage.evaluate()を使っていました。保存完了がその間に入ると、次の順序になります。
| 順序 | 起きたこと |
|---|---|
| 1 | 1回目の観測で、保存前のsaved = nullを読む |
| 2 | アプリの保存処理が完了する |
| 3 | 2回目の観測で、保存後のsavedTitleを読む |
| 4 | タイトルが期待値と一致したので、混在した観測値を比較元として採用する |
| 5 | リロード後の正常な保存レコードと比較し、差分ありと判定する |
再現テストの失敗出力には、次の値が残りました。説明のためpointerなどを省略しています。
{
"before": {
"saved": null,
"savedTitle": "Synthetic melody — 合成曲"
},
"after": {
"saved": {
"title": "Synthetic melody — 合成曲",
"notes": [60, 64],
"bpm": 145
},
"savedTitle": "Synthetic melody — 合成曲"
}
}
タイトルを読めているのに、その元のレコードがnullになっている。ここが、検査側の読み方を疑う手掛かりでした。
修正は、Web Storageの読み取りを一つにまとめる
Web Storageの観測を先に集め、1回の同期的なpage.evaluate()コールバック内でraw値を採取するようにしました。JSONのparseやフィールド選択は、採取済みの値に対して行います。
以下は考え方を示す短縮例です。キー名は説明用で、実装は複数の観測・動的なキー・sessionStorageにも対応しています。
// 同じ保存レコードから、同じ読み取り結果を元に必要な値を取り出す。
const raw = await page.evaluate(() => {
const id = localStorage.getItem('current');
return id === null ? null : localStorage.getItem(`doc.${id}`);
});
const saved = raw === null ? null : JSON.parse(raw);
const snapshot = {
saved,
savedTitle: saved?.title ?? null,
};
実際の観測処理はこちらです。期待値を緩めたり、落ちたテストを単に再実行して済ませたりせず、観測の境界を直しました。
タイミング頼みのテストにしない
回帰テストでは、合成アプリの保存処理を一旦保留し、検査側の最初の観測が返った直後に保存を確定させています。偶然その隙間に保存が入ることを待つのではなく、問題の順序を固定しました。
| 検査側 | 同じ回帰テストの結果 |
|---|---|
| 旧観測処理 |
before.saved = nullで、正常な保存をFAILと誤判定 |
| 修正後の観測処理 | 保存前後のレコードが期待値と一致してPASS |
回帰テストのソースを公開しています。v0.2.1のパッケージで依存関係とChromiumをインストールした後、次で実行できます。
node --test tests/file-import.test.mjs
生成する独立Playwrightテストにも同じ観測処理を組み込み、生成後のテストを別に実行する検査を残しています。
保証できる範囲
今回直したのは、同じページの保存処理が、別々のevaluate()呼び出しの間に入り込むケースです。DOM・HTTPレスポンス・別タブ・サーバーまで含めた、一つの原子的なスナップショットを保証するものではありません。
また、保存完了を何で判断するかはアプリごとの仕様です。今回は既知の正常な値を待っています。時間を固定して待つことや、現在の実装に合わせて期待値を変えることを、仕様の代わりにはしていません。
自分のE2Eで保存前後の不思議な差分が出たら、アプリの書き込みだけでなく、検査側の複数の読み取りの間に、状態が変わる余地がないかも確認すると切り分けに役立ちます。
App Crash Labには、この修正を含む「保存→リロード」と「無効更新を拒否した後のデータ保持」の二つの検査があります。実演とv0.2.1のソース・実行手順を公開しています。自分のアプリへ使う場合は、使い捨てのローカル環境と合成データで、保存フロー・観測する値・拒否条件を設定します。