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?

保存テスト18件がPASSしたコードに、1キーだけ失敗するVitestを足した

0
Last updated at Posted at 2026-10-19

保存まわりの既存テストが通ることと、書き込みの途中で1つのキーだけ失敗したときの振る舞いを確認することは、別でした。

dayGLANCEの公開ソースを変更せず、既存のVitest環境へ124行のテストを1ファイル追加しました。保存モジュールに隣接する既存18件と、新しい正常対照1件はPASS。localStorage.setItem() の対象キー1つだけを拒否すると、古いログ全体は保持された一方、2つの更新markerは先へ進みました。

その観測、再実行手順、別のアプリへ持っていくときの注意点をまとめます。ブラウザの容量を実際に埋めた検証ではなく、実際のsave/loadモジュールへ模擬故障を与えた単体試験です。

既存のVitestで自分のアプリを検査する場合は、App Crash Labのデータ層検査の依頼文をコピーし、いつものコーディングAIへ渡せます。本体やスキルの導入は不要で、アプリ側の既存Vitestを使います。ブラウザを検査する依頼とは入口を分けています。

再実行するファイルと結果

2026年10月19日、Node 22.16.0 / Vitest 4.1.11での運営側の実行結果です。アプリはv5.9.0、コミットはa48be4b242b2741877edd4af71acc73f3f497c00です。

検査 結果
隣接する既存の保存テスト 18 PASS
正常な保存と、実loadDataによる全ログの読み込み 1 PASS
habit-logの保存だけ拒否し、旧ログと選んだmarkerを比較 1 FAIL:markerの期待値2箇所が不一致

合計19 PASS / 1 FAIL、skipは0、終了コードは1でした。全プロジェクトのテストを実行したという意味ではありません。

すべての書き込みを失敗させない

今回の対象キーはday-planner-habit-logsです。日時と2つの習慣IDを持つ架空のログを使っています。

全キーを無条件に拒否すると、テストの準備や無関係な設定まで壊れ、どの失敗を見ているのか分からなくなります。そこで、対象キーへの書き込みだけQuotaExceededErrorを投げ、他のキーはMapへ保存しました。

以下は配布したテストの実際の抜粋です。周辺の定義・importを含む完全なファイルはZIPに入っています。

vi.stubGlobal('localStorage', {
  getItem: key => store.get(String(key)) ?? null,
  setItem: (key, value) => {
    if (rejectLogWrite && key === LOGS_KEY) {
      rejectedWrites += 1;
      throw new DOMException('Synthetic exact-key rejection', 'QuotaExceededError');
    }
    store.set(String(key), String(value));
  },
  removeItem: key => store.delete(String(key)),
});

テスト対象のsaveData()とloadData()は実モジュールからimportしています。これら自体を「成功するモック」に差し替えてはいません。native-calendar連携は、隣接する既存テストと同じ境界で無効化しています。

古いデータを本物の保存処理で作ってから壊す

最初からMapへ正解のデータを直接入れると、「アプリが保存した」という前提を確かめられません。

両方のケースで、まず旧ログを実saveData()で保存します。旧ログ全体と、2つのmarkerが合成時刻09:00になったことを確かめてから、時計を10:00へ進めます。

正常対照では、新ログを保存したあと、次の3点を確かめています。

  1. 保存されたJSON文字列全体が新ログと一致する。
  2. JSONをparseしたレコード全体も一致する。
  3. 空の入力と新しいsetter群で実loadData()を呼ぶと、新ログ全体が渡される。

この対照が通ったあとで、対象キーだけを拒否します。故障を入れたつもりで実際には対象コードへ到達していないケースを避けるため、拒否した回数も正の値であることを要求します。

expect(rejectedWrites, 'PRECONDITION: exact target write must be attempted')
  .toBeGreaterThan(0);
expect(store.get(LOGS_KEY)).toBe(JSON.stringify(OLD_LOGS));
expect(JSON.parse(store.get(LOGS_KEY))).toEqual(OLD_LOGS);
expect.soft(store.get(SYNC_KEY), 'failed habit-log save advanced sync stamp')
  .toBe(BEFORE);
expect.soft(store.get(EDIT_KEY), 'failed habit-log save advanced local-edit stamp')
  .toBe(BEFORE);

実行結果では、故障注入の前提と旧ログ全体の保持は通りました。不一致になったのは、次の2値を旧時刻に保つという期待値です。

  • day-planner-cloud-sync-local-modified
  • day-planner-local-edit-at

どちらも観測値は09:00から10:00へ進んでいました。

markerの意味と、FAILの意味を分ける

アプリの実装には、書き込み失敗をcatchして一度warning toastを設定し、その後のキーへ進む処理があります。無警告だった、画面がクラッシュした、という結果ではありません。 今回はtoastの実画面表示も検証していません。

また、これらのmarkerはソース上ではローカルの変更や編集を記録する値です。名前だけで「保存完了時刻」と決めつけることはできません。

今回のテストは、元のIssueが扱う保存失敗時の一貫性に合わせて、対象ログが保存できなかったときに選んだmarkerを進めない、という条件を置いたものです。FAILはこの条件との不一致を示します。上流作者がこの期待値に合意したこと、同期先のデータが壊れたこと、全キーのatomicityが失われたことを、この試験だけで証明したわけではありません。

別のアプリへ移す場合、markerが編集の発生を記録するのか、永続化の完了を記録するのかを先に読み、期待値を決める必要があります。新しいデータが保存されなかったことと、古いデータが消えたことも別です。今回、旧ログは残りました。

再実行手順

使い捨てのdayGLANCEチェックアウトで、先ほどの固定コミットへ移動してください。既存の作業ディレクトリへ上書きする手順ではありません。

Node 22.16.0で、アプリのlockfileを使ってnpm ci --ignore-scriptsを実行した環境です。ZIPをそのチェックアウトのルートへ展開すると、テストがsrc/hooks/useDataPersistence.crash-lab.test.jsへ入ります。アプリのソース・設定・lockfileの変更や、新しい依存の追加はありません。

ZIPのSHA-256は次です。

62f2675f27d254188e51a4363357b34575b9244bfbaa07f101ef873b32b0c092
npm test -- src/hooks/useDataPersistence.test.js src/hooks/useDataPersistence.crash-lab.test.js --maxWorkers=1

今回の固定コミットでは、上記のmarker比較がFAILし、終了コード1になる想定です。終了コードを0へ変更したり、その比較をskipしたりする手順ではありません。修正版を試した結果や、Windowsでの実行は未確認です。

自分のアプリで同じ境界を調べるには

ここからはdayGLANCEのファイルをそのままコピーする方法ではなく、自分の保存・読込処理に合わせる方法です。キー名だけを置換しても、引数やload時の仕様は一致しません。

私は保存境界の検査手順をApp Crash LabというMITのOSSとCodexスキルにまとめています。既存テスト用の依頼文から「Vitest: data layer only」を選ぶと、本体やスキルをインストールせずに検査を依頼できます。Codexスキルv0.2.2を導入して使う場合は、次の依頼も使えます。

$app-crash-lab
今回は既存Vitestを使ったdata-layerの検査をお願いします。
実際のsave/load処理と既存テストを先に読み、
保存する完全なレコードと、読込時に仕様で変換する値を分けてください。
正常保存・読込の対照を先に通し、対象キー1つだけの保存拒否を検査してください。
更新markerの意味をソースで確かめ、期待値の根拠を説明してください。
アプリを修正せず、結果と再実行コマンドを残してください。

明示的なdata-layer検査では、既存のテスト環境を使い、別のApp Crash LabランナーやJSON契約は必要ありません。一方、ブラウザ再読込、古いタブからの更新、画面のエラー表示、実ファイルのバックアップ復元を知りたい場合は、それぞれ別のブラウザ検査が必要です。単体試験の成功では代用できません。

dayGLANCEは元の作者のMITプロジェクトです。ZIPには今回追加したテスト、手順、MIT表記を含み、アプリ本体を再配布していません。この記事は人間の責任者のもとでAIを使って実装・実行検証・執筆した運営側の記録です。作者によるApp Crash Labの利用報告ではありません。

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?