0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

保存できているのにテストが落ちる:localStorageの読み取り競合をPlaywrightで再現

0
Posted at

保存した値をリロード前後で比較する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のソース・実行手順を公開しています。自分のアプリへ使う場合は、使い捨てのローカル環境と合成データで、保存フロー・観測する値・拒否条件を設定します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?