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?

セーブ処理をVitestでどこまでテストするか【第37回】

0
Last updated at Posted at 2026-09-16

4Hえんぴつ|React+TypeScriptでWeb RPGを作る
第7章「テストと品質管理」
第37回:セーブ処理をVitestでどこまでテストするか

連載トップ・第0回はこちら

1. 今回のテーマ

ここまで、

セーブ処理はかなり複雑になってきました。

扱っているのは、

SaveData validation
saveSchemaVersion
Migration
future schema
current
backup
quarantine
Recovery
IndexedDB

です。

これらを、

変更のたびに、

すべて手作業で確認するのは現実的ではありません。

そこで今回は、

セーブ処理のどこまでをVitestで自動テストするのか

を整理します。


2. まず「全部E2Eで確認する」は避けたい

セーブ処理には、

ブラウザ操作を伴うものもあります。

そのため、

最初から、

Playwrightで全部確認すればよい

とも考えられます。

しかし、

すべてをE2Eにすると、

実行時間が長い
原因切り分けが難しい
テスト準備が大きい
細かな分岐を網羅しにくい

という問題があります。

そこで、

小さいロジックはVitest、ユーザー操作全体はPlaywright

と役割を分けます。


3. Vitestで確認したいのは「ロジック」

Vitestで確認しやすいのは、

例えば、

入力
↓
判定
↓
期待する結果

が明確な処理です。

セーブ機能なら、

SaveData validation

schema判定

Migration

future schema判定

Recovery条件判定

などです。


4. UIを通さずに直接テストする

例えば、

壊れたSaveDataを検証したい場合、

毎回、

ゲーム起動
↓
セーブ画面
↓
データを壊す
↓
ロード

までやる必要はありません。

Vitestでは、

validation関数へ直接、

入力データを渡せます。

以下は考え方を説明するため、実装を簡略化しています。

const result =
  validateSaveData(candidate);

そして、

expect(result.status).toBe("invalid");

のように確認します。


5. validationはVitest向き

第34回では、

セーブデータを、

valid
migratable
unsupported
invalid

へ分類しました。

この種の判定は、

Vitestと相性がよい部分です。

例えば、

正常なSchema 3
↓
valid

正常なSchema 2
↓
migratable

Schema 99
↓
unsupported

必須項目欠落
↓
invalid

という入力と期待結果を、

個別にテストできます。


6. JSON破損も分けて確認する

例えば、

{"slotId":

のような、

JSONとして壊れた文字列があります。

これは、

SaveData validationより前の、

parse段階で失敗します。

つまり、

parse error

と、

validation error

を分けて確認できます。


7. 「読めない」と「形式が違う」を分ける

例えば、

JSON parse失敗

と、

future schema

は、

どちらも通常ロードできません。

しかし、

意味は違います。

そこでVitestでも、

JSON破損
↓
invalid

future schema
↓
unsupported

と、

別の期待値を持たせます。


8. MigrationもVitest向き

第31回では、

Schema 2
↓
Schema 3

へのMigrationを扱いました。

Migrationは、

入力と出力が明確です。

例えば、

v2 SaveData
↓
migrateV2ToV3()
↓
v3 SaveData

です。

この処理もVitestで確認できます。


9. Migrationでは何を確認するか

単に、

saveSchemaVersion === 3

だけを見るのでは足りません。

例えば、

slotIdが維持されている
appVersionが保持されている
GameStateが正しく引き継がれている
新しい必須項目が適切に補われている
migratedFromSchemaVersionが設定されている

などを確認します。


10. 「意味が変わっていないか」を見る

Migrationでは、

型が合っているだけでなく、

ゲーム上の意味が保たれているか

が重要です。

例えば、

questProgress
eventProgress
unlockedTownIds
playerStatus

などが、

Migration前後で意図どおり維持されているかを確認します。


11. future schemaもVitestで固定できる

第32回で扱ったfuture schemaも、

Vitestで確認できます。

例えば、

saveSchemaVersion = 99

を与えて、

unsupported

になることを確認します。


12. 「ロード不可」だけではなく「上書き不可」も見る

future schemaでは、

通常ロード拒否

だけでなく、

通常上書き拒否

も重要でした。

そのため、

判定関数があるなら、

canLoad = false
canOverwrite = false

になることを、

Vitestで固定できます。


13. Recovery条件もVitestで確認する

第36回では、

current invalid
+
backup valid
↓
Recovery

を扱いました。

この条件分岐も、

Vitestで確認できます。

例えば、

current valid
backup valid
↓
Recoveryしない
current invalid
backup valid
↓
Recoveryする
current invalid
backup invalid
↓
Recoveryしない

です。


14. Recovery結果を直接確認する

RecoveryをRepositoryレベルまで含めてテストするなら、

例えば、

current invalid
backup valid

という状態を用意します。

その後、

Recovery処理を実行して、

currentが正常になった
元currentがquarantineへ残った
他slotは変わっていない

ことを確認します。


15. currentだけ確認して終わらない

例えば、

Recovery後に、

currentがvalid

だけなら、

一見成功です。

しかし、

元の破損currentが、

消えてしまっている可能性があります。

そのため、

current
backup
quarantine

それぞれの状態まで確認します。


16. backupなしも別ケースにする

例えば、

current invalid
backupなし

があります。

期待する結果は、

Recoveryしない
currentを通常利用しない
quarantineへ保持
slotを再利用可能

です。

正常backupありの場合とは、

別のテストケースとして持ちます。


17. 他スロットが変わらないこともテストする

3スロットある場合、

例えば、

slot_1 valid
slot_2 invalid
slot_3 valid

という状態を用意します。

slot_2をRecoveryした後、

slot_1 unchanged
slot_3 unchanged

も確認します。


18. 「壊れた1件だけ」を直すことを保証する

このテストにより、

slot_2を直したら
slot_1まで書き換わった

といった事故を防ぎやすくなります。

つまり、

Recoveryの正しさだけでなく、

影響範囲

もテストします。


19. IndexedDBをどうVitestで扱うか

現在の正式公開版では、

保存先として、

IndexedDB
+
Dexie.js

を使っています。

しかし、

Vitest実行環境では、

ブラウザのIndexedDBがそのまま使えない場合があります。

そこで、

正式版では、

fake-indexeddb 6.2.5

を利用しています。


20. fake-indexeddbの役割

fake-indexeddbを使うことで、

テスト環境でも、

IndexedDB相当のAPIを使えます。

これにより、

Repository
Migration
Save / Load
backup
quarantine

などを、

ブラウザE2Eだけに頼らず確認できます。


21. Dexie.jsも実際のRepository経由で確認する

例えば、

RepositoryがDexie.jsを使っているなら、

テストでも、

できるだけ、

Repository
↓
Dexie.js
↓
fake-indexeddb

という経路を通します。

これにより、

単なるモック以上に、

実際の永続化層に近い動作を確認できます。


22. 何でもmockすればよいわけではない

例えば、

Repository自体を完全にmockすると、

Save Service
↓
mock

だけになります。

これでは、

Dexie.js側の、

書き込み
読み込み
更新
削除

の実装ミスを見つけられません。

そのため、

どこを本物で動かすか

を意識します。


23. テスト対象ごとに境界を変える

例えば、

純粋なvalidationなら、

Storageは不要です。

SaveData
↓
validateSaveData()

だけで十分です。

一方、

Recoveryなら、

Repository
↓
fake-indexeddb

まで使う価値があります。


24. 単体テストと統合テストを分ける

整理すると、

Vitestの中でも、

Unit Test
↓
小さい関数

Integration Test
↓
複数の処理やRepository

に分けられます。

例えば、

schema判定
↓
Unit
current / backup / quarantine Recovery
↓
Integration寄り

です。


25. テストデータは小さく作る

GameState全体は、

項目が多くなっています。

毎回巨大なJSONを、

手書きでテストへ貼ると、

変更に弱くなります。

そこで、

正常な最小SaveDataを作る
↓
必要な箇所だけ変える

方が扱いやすくなります。


26. factoryを使う

例えば、

以下は考え方を説明するため、実装を簡略化しています。

function createValidSaveData(
  overrides: Partial<SaveData> = {}
): SaveData {
  return {
    saveSchemaVersion: 3,
    appVersion: "1.6.0",
    slotId: "slot_1",
    savedAt: "2026-08-29T00:00:00Z",
    state: createValidGameState(),
    ...overrides,
  };
}

これなら、

テストごとに、

変更したい部分だけ指定できます。


27. 壊れたデータも「差分」で作る

例えば、

const save =
  createValidSaveData({
    slotId: "slot_99" as SaveData["slotId"],
  });

のように、

正常データから、

意図的に一部分だけ壊します。

これにより、

何をテストしているのかが分かりやすくなります。


28. ただし型に邪魔されることもある

破損データをテストするときは、

TypeScriptから見ると、

そんな値は作れない

ケースがあります。

これは当然です。

本番コード側では、

その値を作らないよう型で守っているからです。


29. invalidテストではunknownを使う

そのような場合は、

正常なSaveData型として作るのではなく、

unknown

として、

外部入力を再現します。

以下は考え方を説明するため、実装を簡略化しています。

const candidate: unknown = {
  saveSchemaVersion: 3,
  slotId: "slot_99",
};

これをvalidationへ渡します。


30. テストコードまで型安全にしすぎない

invalidデータのテストで、

無理に、

as SaveData

としてしまうと、

本来、

外部入力として異常な値を確認したいのに、

意図が分かりにくくなります。

そのため、

正常系
↓
SaveData

異常系
↓
unknown

と分ける方が自然です。


31. parameterized testも使える

似たvalidationケースが多い場合は、

表形式でまとめる方法もあります。

以下は考え方を説明するため、実装を簡略化しています。

it.each([
  ["slot_99", "invalid"],
  ["slot_1", "valid"],
])(
  "slotId=%s => %s",
  (slotId, expected) => {
    // test
  }
);

同じ構造のテストを、

大量に重複させずに済みます。


32. テスト名は仕様として読めるようにする

例えば、

test1
test2
test3

ではなく、

future schemaはunsupportedになる

currentがinvalidでbackupがvalidならRecoveryする

backupなしの破損currentはquarantineされる

のようにします。

テスト名を見るだけで、

仕様が分かる状態を目指します。


33. テストが仕様書の一部になる

セーブ処理は、

分岐が多くなります。

そのため、

テストケース自体が、

この場合はこう動く

という仕様の記録になります。

特に、

unsupported
invalid
migratable

のような、

似た状態を区別するときに役立ちます。


34. 何をVitestで確認しないか

逆に、

Vitestだけでは十分でないものもあります。

例えば、

画面から実際にセーブできる

ボタン操作でロードできる

画面遷移後も状態が正しい

ブラウザ再読み込み後も続きから遊べる

です。

これらは、

UI・Router・Storage・GameStateがつながった状態を確認する必要があります。


35. それはPlaywright側へ任せる

そこで、

小さなロジック
↓
Vitest

ユーザー操作全体
↓
Playwright

という分担にします。

同じ内容を、

両方で完全に重複させる必要はありません。


36. ただし重要な仕様は層をまたいで確認する

例えば、

current破損
+
backup正常
↓
Recovery

は重要です。

そのため、

Vitestでは、

Recoveryロジック

を確認し、

Playwrightでは、

実際にゲームから再開できる

ことを確認します。


37. 同じ仕様を違う角度から見る

つまり、

Vitest
↓
ロジックが正しいか
Playwright
↓
ユーザー操作として成立するか

です。

重複というより、

異なる層で同じ品質を確認する

と考えます。


38. Vitestで確認したい主なセーブケース

ここまでをまとめると、

Vitest側では、

例えば、

  • 正常Schema 3のvalidation
  • 旧Schemaのmigratable判定
  • future schemaのunsupported判定
  • 不正データのinvalid判定
  • v2→v3 Migration
  • current invalid+backup validのRecovery
  • current invalid+backupなし
  • current invalid+backup invalid
  • quarantineへの退避
  • 他スロット非干渉
  • IndexedDB Repositoryのsave / load

などを確認できます。


39. すべての組み合わせを無限に増やさない

とはいえ、

全項目
×
全schema
×
全slot
×
全Recovery状態

を網羅すると、

テスト数は際限なく増えます。

そこで、

設計上の分岐点

を中心にテストします。


40. 仕様上意味が変わる境界を優先する

例えば、

Schema 3
↓
compatible

Schema 2
↓
migratable

Schema 99
↓
unsupported

は、

意味が変わる境界です。

同様に、

backupあり
backupなし
backup invalid

も、

Recoveryの結果が変わります。

こうした境界を優先します。


41. happy pathだけでは足りない

ゲーム開発では、

まず、

正常にセーブできる
正常にロードできる

を確認したくなります。

しかし、

今回のような品質基盤では、

むしろ、

壊れている
古い
新しすぎる
復旧元がない

といったケースが重要です。


42. 異常系こそ自動化の価値が高い

正常系は、

ゲームを遊んでいれば、

比較的確認しやすい部分です。

一方、

future schema 99を作る

currentだけ壊す

backupだけ正常にする

といった状態は、

毎回手作業で作るのが面倒です。

だからこそ、

自動テストに向いています。


43. 正式公開版では68テストまで増えた

最終的な正式公開版では、

Vitestは、

7/7 files・68/68 tests

をPASSしています。

ここで重要なのは、

数字そのものではありません。

セーブ互換性やRecoveryを含めて、

公開前に繰り返し確認できる状態にしたこと

です。


44. テスト数を目標にしない

例えば、

100テストある

こと自体が、

品質を保証するわけではありません。

同じような正常系を100個並べても、

重要な分岐が抜けていれば、

問題は残ります。

見るべきなのは、

重要な仕様分岐が
テストで固定されているか

です。


45. 変更するとテストが守ってくれる

例えば、

将来、

SaveDataの項目を変更したとします。

その結果、

MigrationやRecoveryへ影響が出た場合、

既存テストが失敗すれば、

どこかの互換性が壊れた

ことに気づけます。


46. テストは「壊さないための記憶」になる

人間は、

以前なぜその分岐を入れたのかを、

時間とともに忘れることがあります。

しかし、

future schemaは通常上書きしない

というテストが残っていれば、

将来の変更時にも、

そのルールを守れます。


47. 今回のポイント

今回のポイントは3つです。

  • Vitestでは、validation・schema判定・Migration・Recoveryなど、入力と結果が明確なロジックを中心に確認する
  • IndexedDBを使う処理はfake-indexeddbを使い、Repositoryまで含めた統合テストも行う
  • UI全体はPlaywrightへ任せ、VitestとE2Eで役割を分ける

整理すると、

Pure Logic
↓
Vitest

Repository / IndexedDB
↓
Vitest + fake-indexeddb

User Flow
↓
Playwright

です。

テストの目的は、

テスト数を増やすことではなく、変更しても守りたい仕様を自動で確認できるようにすること

です。


48. 次回

次回は、

手動確認していた冒険をPlaywrightでE2Eテストにする【第38回】

です。

ここまでVitestでは、

validation
Migration
Recovery
Repository

を確認しました。

しかし、

プレイヤーが実際に行うのは、

タイトル
↓
キャラクター作成
↓
町
↓
依頼
↓
セーブ
↓
ロード
↓
再開

という一連の操作です。

次回は、

これをPlaywrightで、

実際のブラウザ操作として自動化する

ところへ進みます。


この記事を最後まで読んでいただき、ありがとうございます。

いいね」を押していただけるとうれしいです。これからの記事づくりの励みになります。

もし気に入っていただけましたら、フォローもよろしくお願いします。


前の記事

第36回 壊れたcurrentをbackupから復旧する――Recoveryの実装フロー

次の記事

第38回 手動確認していた冒険をPlaywrightでE2Eテストにする

連載トップ

第0回 React+TypeScriptで異世界RPGを作る――連載の目的と開発ロードマップ

note関連記事

手動確認していた冒険を、E2Eテストにする #21

noteでは、これまで人が画面を操作して確認していた流れを自動化し、VitestとPlaywrightで役割を分けた背景を制作側の視点から書いています。

note記事を読む

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?