目次
テスト用に注入した状態を実際のプレイで取得したセーブデータに置き換えたところ、既存のテストが隠していたGodotのソフトロックが明らかになった。同じ手法で、開発途中のセーブデータ移行処理が、旧形式の実際のセーブデータを読み込めることも確認できた。
要点#
- 通常のプレイでセーブデータを取得し、プレイヤーが実際に到達できる状態をテストの前提条件にする。
- 取得したすべてのフィクスチャを本番用ローダーで読み込み、現在のゲームAPIで値を確認する。
- ゲームに必要な復旧処理の欠落を隠しかねない、テスト専用のリセット処理を削除する。
- 残っている状態注入の呼び出し数を数え、継続的に減らすことを必須にする。
状態注入では不十分だった理由#
Webゲームの回復テストは、チェックポイントへワープし、体力を5に設定してから回復アクションを呼び出していた。これで検証できたのは、人工的な状態からの回復だった。同じ所持品、戦闘結果、進行状況でプレイヤーがそのチェックポイントに到達できるかは確認していなかった。
14個のspecファイルには、勝利や敗北の強制、ワープ、所持品の注入など、状態を変更する呼び出しが18か所あった。いずれもテストを簡単に書けるようにする一方、通常のプレイでその開始状態を作れることは証明していなかった。
置き換えのルールは単純だった。必要に応じてシード、選択肢、入力を固定しても、結果の状態は通常のゲームコードで作る。
取得し、読み込み、検証する#
この手法は、ターン制Webゲーム、Godotゲーム、再利用可能なWebゲームテンプレートに適用した。Webプロジェクトでの手順は次のとおりだった。
1. プレイヤーが使える操作でボットにプレイさせる。 2. 適切なチェックポイントでlocalStorageの全内容をエクスポートする。 3. 取得したデータをそれぞれfixtures/saves/<app-version>/<name>.jsonに保存する。 4. すべてのフィクスチャを本番用ローダーで読み込み、その一部をE2Eテストの開始状態として使う。
各フィクスチャには、生のセーブ文字列と、バージョン、取得コマンド、シナリオ、期待値を記述したマニフェストが含まれる。読み込み後は、現在のゲームAPIを通じて期待する値を取得する。これにより、セーブ内部のフィールド名が変わっても検証を維持できる。
回復テストは、取得済みのチェックポイントのセーブデータから始まるようになった。回復前に、プレイヤーがチェックポイントにいて、体力が最大値未満であることを確認する。そのため、読み込みに失敗して黙って新規ゲームが始まると、前提条件のチェックで失敗する。敗北テストにはフィクスチャは不要だ。ボットがターンを見送り、敵の攻撃で通常の敗北に至るまで続ける。
期待値をレビューする#
取得スクリプトは、読み取った値をそのまま自動的に期待値にしてはならない。それでは、正しい状態と同じように誤った状態も固定してしまう。
新しく取得したデータでは、invariantsをnullのままにする。回帰テストは意図的に失敗し、読み込み後に返された値を出力する。レビュアーがその値を取得時の画面と生のセーブデータに照らして確認してから、期待値として記録する。また、フィクスチャが0件の場合もテストを失敗させ、空のディレクトリで見かけ上の成功が出ないようにする。
これにより、具体的な移行チェックができた。コミット済みのWebゲームで取得したv3のセーブデータを、開発途中のv4移行処理がある別のワークツリーで読み込んだ。フィクスチャのテストは両方とも成功した。ローダーは古いキーを受け付け、セーブデータを移行し、検証対象の値を保持した。これで確認できたのは、そのフィクスチャとの互換性であり、あらゆる旧セーブデータとの互換性ではない。
ボットが実際のソフトロックに遭遇した#
Godotの取得用ボットは、最初のマイルストーン後のオートセーブを記録するはずだった。3つのシードでそれぞれ60回挑戦したが、一度も到達できなかった。最初の敗北後、パーティーメンバーの体力は0のままだった。それ以降の挑戦はほぼ即座に終わり、プレイヤーが使える回復手段もなかった。
既存のバランステストがこれを見逃していたのは、Bot.simulateが毎回の挑戦前にrestore()を呼び出していたためだ。テスト用ボットがゲームに代わって状態を修復していた。取得用ボットはゲームの挙動に従い、プレイヤーと同じ場所で行き詰まった。
修正では、挑戦から戻ったときに全員の体力を回復させた。セーブは挑戦の合間に行われるため、読み込み時にも同じ回復処理を使う。体力0のキャラクターを含む古いセーブデータも対象になる。敗北後の回復と、そのようなセーブデータの読み込み時の回復を2つのテストで検証し、修正を取り除くと両方とも失敗した。ボット内部のrestore()呼び出しも削除した。
近道が復活しないようにする#
Web側で置き換えたのは回復と敗北のセットアップだけで、状態注入の呼び出しは15か所残った。現在は、件数を数えるテストでこの基準値を守っている。増えれば失敗し、減った場合も基準値を引き下げるまで失敗するため、一度減らした件数を維持できる。
取得の質は、依然としてボットに左右される。スモークテスト用のランダムな入力でもセーブデータは作れるが、特定のチェックポイントに到達するにはゲーム固有の判断が必要だ。生のセーブデータは1行のJSON文字列として保存され、レビューしにくいため、フィクスチャのマニフェストにシナリオと取得手順を記述する必要がある。
テスト用の保存先も分ける必要がある。今回の作業中、既存のGodotテストが本番用のuser://でスロットを作成・削除していたことがわかった。現在、テストのセーブデータはuser://verify/tests/配下に保存している。
次の一歩#
状態注入を使うE2Eテストのセットアップを1つ、通常のプレイで取得したセーブデータに置き換え、その読み込みで意図した開始状態が復元されなければテストを失敗させよう。
出典: https://zenn.dev/gzer0/articles/real-play-save-fixtures-for-ai-tests
目次
