目录
将测试中注入的状态替换为实际游玩获取的存档后,暴露出了被现有测试掩盖的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角色的旧存档也适用。两个测试分别验证战败后的恢复,以及加载此类存档时的恢复;移除修复后,两者均会失败。同时也删除了机器人内部的restore()调用。
防止捷径卷土重来#
Web端只替换了恢复和战败的测试准备逻辑,仍有15处状态注入调用。目前通过计数测试维持这一基准值。数量增加时测试会失败;数量减少时,也会一直失败,直到调低基准值,从而守住已经减少的数量。
存档获取的质量仍取决于机器人。冒烟测试使用的随机输入也能生成存档,但要到达特定检查点,就需要针对游戏作出判断。原始存档以单行JSON字符串保存,不便审查,因此需要在测试夹具清单中记录场景和获取步骤。
测试用的存储位置也需要隔离。此次工作中发现,现有Godot测试会在生产环境使用的user://下创建和删除存档槽。现在,测试存档保存在user://verify/tests/下。
下一步#
将一个使用状态注入的E2E测试准备流程替换为正常游玩获取的存档,并在加载未能恢复预期初始状态时让测试失败。
来源: https://zenn.dev/gzer0/articles/real-play-save-fixtures-for-ai-tests
目录
