游戏测试
游戏测试(Game Test)是一种运行游戏内单元测试的方式。该系统设计为可扩展、并行运行,以高效执行大量不同测试。测试对象交互和行为只是这一框架的部分用途。
原创说明图,依据 Forge 1.21.x 官方文档 绘制。
创建游戏测试
标准游戏测试包含三个基本步骤:
- 加载一个结构或模板,其中包含用于测试交互或行为的场景。
- 一个方法提供对场景执行的逻辑。
- 执行该方法的逻辑。如果达到成功状态,测试成功;否则测试失败,结果保存在场景旁的讲台中。
因此,创建游戏测试需要包含场景初始状态的现有模板,以及提供执行逻辑的方法。
测试方法
游戏测试方法是 Consumer<GameTestHelper> 引用,即接收 GameTestHelper,不返回值。要被识别,方法必须带有 @GameTest 注解:
public class ExampleGameTests {
@GameTest
public static void exampleTest(GameTestHelper helper) {
// 执行操作
}
}
@GameTest 注解还包含配置测试运行方式的成员:
@GameTest(
setupTicks = 20L, // 测试花 20 tick 准备执行
required = false // 失败会被记录,但不影响批次执行
)
public static void exampleConfiguredTest(GameTestHelper helper) {
// 执行操作
}
相对位置
所有 GameTestHelper 方法都使用结构方块的当前位置,将结构模板场景中的相对坐标转换为绝对坐标。可分别通过 GameTestHelper#absolutePos 和 #relativePos 方便地转换。
在游戏中可通过测试命令加载结构,将玩家放在目标位置,再运行 /test pos,取得玩家相对于 200 格内最近结构的坐标。命令会在聊天中导出可复制文本组件,可用作 final 局部变量。
提示:在
/test pos命令末尾添加名称,可指定生成局部变量的引用名:
/test pos <var> # 导出 'final BlockPos <var> = new BlockPos(...);'
成功完成
游戏测试方法负责在有效完成时将测试标记为成功。如果在 GameTest#timeoutTicks 定义的超时之前未达到成功状态,测试自动失败。
GameTestHelper 有许多定义成功状态的抽象方法,以下四个尤其重要:
| 方法 | 说明 |
|---|---|
#succeed |
将测试标记为成功。 |
#succeedIf |
立即测试提供的 Runnable,不抛出 GameTestAssertException 即成功;如果当前 tick 未成功,则标记为失败。 |
#succeedWhen |
每 tick 测试提供的 Runnable 直到超时;某个 tick 检查未抛出 GameTestAssertException 即成功。 |
#succeedOnTickWhen |
在指定 tick 测试提供的 Runnable,未抛出 GameTestAssertException 即成功;如果在其他 tick 成功,则标记为失败。 |
重要:游戏测试每 tick 执行,直到标记为成功。因此,安排在指定 tick 成功的方法必须确保此前每个 tick 都失败。
安排操作
并非所有操作都在测试开始时发生,可安排在特定时间或间隔执行:
| 方法 | 说明 |
|---|---|
#runAtTickTime |
在指定 tick 执行。 |
#runAfterDelay |
在当前 tick 之后 x 个 tick 执行。 |
#onEachTick |
每 tick 执行。 |
断言
游戏测试中的任何时候都可断言某条件是否为真。GameTestHelper 有许多断言方法,其本质是在未达到相应状态时抛出 GameTestAssertException。
生成的测试方法
如果需要动态生成测试方法,可创建测试方法生成器。这类方法不接收参数,返回 TestFunction 集合;必须带有 @GameTestGenerator 注解才会被识别:
public class ExampleGameTests {
@GameTestGenerator
public static Collection<TestFunction> exampleTests() {
// 返回 TestFunction 集合
}
}
TestFunction
TestFunction 包装 @GameTest 注解中的信息和运行测试的方法。
提示:带
@GameTest的方法都会通过GameTestRegistry#turnMethodIntoTestFunction转换为TestFunction。不用注解创建时可参考该方法。
批处理
游戏测试可以按批次执行,而非按注册顺序执行。提供相同的 GameTest#batch 字符串即可将测试加入同一批次。
批处理本身没有额外作用,但可以对测试运行的当前维度执行准备和清理。为方法添加 @BeforeBatch 注解进行准备,或添加 @AfterBatch 注解进行清理。其 #batch 值必须与游戏测试提供的字符串一致。
批处理方法是 Consumer<ServerLevel> 引用,即接收 ServerLevel,不返回值:
public class ExampleGameTests {
@BeforeBatch(batch = "firstBatch")
public static void beforeTest(ServerLevel level) {
// 执行准备
}
@GameTest(batch = "firstBatch")
public static void exampleTest2(GameTestHelper helper) {
// 执行操作
}
}
注册游戏测试
游戏测试必须注册才能在游戏中运行。可通过 @GameTestHolder 注解或 RegisterGameTestsEvent 注册;两种方式都仍要求测试方法带 @GameTest、@GameTestGenerator、@BeforeBatch 或 @AfterBatch 注解。
GameTestHolder
@GameTestHolder 注册其类型(类、接口、枚举或 record)中的测试方法。该注解的单个方法有多种用途;在此,提供的 #value 必须是模组 ID,否则默认配置下测试不会运行。
@GameTestHolder(MODID)
public class ExampleGameTests {
// ...
}
RegisterGameTestsEvent
RegisterGameTestsEvent 也可通过 #register 注册类或方法。事件监听器必须添加到模组事件总线。以此方式注册的测试,必须在每个 @GameTest 方法的 templateNamespace 中提供模组 ID。
public void registerTests(RegisterGameTestsEvent event) {
event.register(ExampleGameTests.class);
}
// 在 ExampleGameTests 中
@GameTest(templateNamespace = MODID)
public static void exampleTest3(GameTestHelper helper) {
// 执行准备
}
注意:
GameTestHolder#value和GameTest#templateNamespace可以与当前模组 ID 不同,但需要更改构建脚本中的配置。
结构模板
游戏测试在由结构或模板加载的场景中执行。所有模板都定义场景尺寸及待加载的初始数据(方块和实体)。模板必须作为 .nbt 文件保存在 data/<namespace>/structures 中。
提示:可使用结构方块创建并保存结构模板。
模板位置取决于是否指定命名空间、是否在名称前加类名,以及是否指定模板名。命名空间先由 GameTest#templateNamespace 决定;未指定时使用 GameTestHolder#value;两者均未指定时使用 minecraft。
如果对类或带测试注解的方法应用 @PrefixGameTestTemplate 并设为 false,模板名不会加简单类名前缀;否则将小写简单类名加点号放在模板名前。模板名称由 GameTest#template 决定,未指定时使用小写方法名。
// 所有结构的模组 ID 为 MODID
@GameTestHolder(MODID)
public class ExampleGameTests {
// 加类名前缀,未指定模板名:modid:examplegametests.exampletest
@GameTest public static void exampleTest(GameTestHelper helper) { /*...*/ }
// 无类名前缀,未指定模板名:modid:exampletest2
@PrefixGameTestTemplate(false)
@GameTest public static void exampleTest2(GameTestHelper helper) { /*...*/ }
// 加类名前缀,指定模板名:modid:examplegametests.test_template
@GameTest(template = "test_template")
public static void exampleTest3(GameTestHelper helper) { /*...*/ }
// 无类名前缀,指定模板名:modid:test_template2
@PrefixGameTestTemplate(false)
@GameTest(template = "test_template2")
public static void exampleTest4(GameTestHelper helper) { /*...*/ }
}
运行游戏测试
使用 /test 命令运行。该命令有很多配置,以下子命令对运行测试尤其重要:
| 子命令 | 说明 |
|---|---|
run <test_name> |
运行指定测试。 |
runall |
运行所有可用测试。 |
runthis |
运行玩家 15 格内最近的测试。 |
runthese |
运行玩家 200 格内的测试。 |
runfailed |
运行上次运行中失败的所有测试。 |
注意:子命令跟在测试命令后:
/test <subcommand>。
构建脚本配置
游戏测试在构建脚本(build.gradle)中提供额外配置,以适应不同运行和集成环境。
启用其他命名空间
如果构建脚本按推荐方式配置,只会启用当前模组 ID 下的测试。要从其他命名空间加载测试,运行配置必须将 forge.enabledGameTestNamespaces 设为逗号分隔各命名空间的字符串。属性为空或未设置时,会加载所有命名空间。
// 在运行配置中
property 'forge.enabledGameTestNamespaces', 'modid1,modid2,modid3'
警告:命名空间之间不能有空格,否则无法正确加载。
游戏测试服务器运行配置
游戏测试服务器是运行构建服务器的特殊配置。构建服务器返回的退出码为失败的必需游戏测试数量;无论必需还是可选,所有失败测试都会记录。可使用 gradlew runGameTestServer 运行。
在其他运行配置中启用游戏测试
默认只有 client、server 和 gameTestServer 运行配置启用游戏测试。若其他运行配置也要运行测试,必须将 forge.enableGameTest 设为 true。
// 在运行配置中
property 'forge.enableGameTest', 'true'
原文:Game Tests。