Game Tests

Game Tests

Game Tests 用于在游戏内运行单元测试。该系统旨在通过可扩展、并行的方式高效执行大量不同测试。检查对象交互和行为只是这一框架众多用途的一部分。

创建 Game Test

标准 Game Test 有三个基本步骤:

  1. 加载结构(也称模板),其中保存用于测试交互或行为的场景。
  2. 方法在场景中执行测试逻辑。
  3. 逻辑运行后,若达到成功状态则测试通过;否则失败,并将结果存入场景旁的讲台。

因此,创建测试需要一个保存场景初始状态的现有模板,以及提供执行逻辑的方法。

测试方法

Game Test 方法是 Consumer<GameTestHelper> 引用,即接受 GameTestHelper 且不返回值。必须带有 @GameTest 注解才能被识别:

public class ExampleGameTests {
    @GameTest
    public static void exampleTest(GameTestHelper helper) {
        // Do stuff
    }
}

@GameTest 还包含用于配置测试运行方式的成员:

// In some class
@GameTest(
    setupTicks = 20L, // The test spends 20 ticks to set up for execution
    required = false // The failure is logged but does not affect the execution of the batch
)
public static void exampleConfiguredTest(GameTestHelper helper) {
    // Do stuff
}

相对位置

所有 GameTestHelper 方法都会依据结构方块的当前位置,将结构模板场景内的相对坐标转换为绝对坐标。可分别使用 GameTestHelper#absolutePos 和 GameTestHelper#relativePos 在两种坐标间转换。

在游戏中,可先通过测试命令加载结构,将玩家移动到目标位置,然后运行 /test pos 获取相对位置。命令会取得玩家相对于 200 格内最近结构的坐标,并在聊天中导出可复制的文本组件,供代码用作 final 局部变量。

建议 在 /test pos 末尾附加名称,可指定生成的局部变量名:

/test pos <var> # Exports 'final BlockPos <var> = new BlockPos(...);'

成功完成

测试方法负责在满足有效完成条件时,将测试标记为成功。如果在 GameTest#timeoutTicks 指定的超时时间前未达到成功状态,测试会自动失败。

GameTestHelper 提供了许多用于定义成功状态的抽象方法,其中四个尤其重要:

方法 说明
#succeed 将测试标记为成功。
#succeedIf 立即执行给定 Runnable,未抛出 GameTestAssertException 即成功。若当前刻未通过,测试立即失败。
#succeedWhen 每刻执行给定 Runnable 直到超时;任意一刻未抛出 GameTestAssertException 即成功。
#succeedOnTickWhen 在指定刻执行给定 Runnable,未抛出 GameTestAssertException 即成功。如果在其他刻成功,则测试失败。

注意 测试在标记成功之前会每刻执行。因此,计划在特定刻成功的方法必须确保之前各刻始终失败。

调度操作

并非所有操作都在测试开始时发生,可以将操作安排在特定时间或间隔执行:

方法 说明
#runAtTickTime 在指定刻执行操作。
#runAfterDelay 在当前刻之后的 x 刻执行操作。
#onEachTick 每刻执行操作。

断言

测试过程中可随时使用断言检查条件。GameTestHelper 有很多断言方法,本质上都是在状态不符合要求时抛出 GameTestAssertException。

生成测试方法

如果需要动态生成测试方法,可以创建测试方法生成器。生成器不接受参数,返回 TestFunction 集合,且必须带有 @GameTestGenerator 注解:

public class ExampleGameTests {
    @GameTestGenerator
    public static Collection<TestFunction> exampleTests() {
        // Return a collection of TestFunctions
    }
}

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) {
        // Perform setup
    }

    @GameTest(batch = "firstBatch")
    public static void exampleTest2(GameTestHelper helper) {
        // Do stuff
    }
}

注册 Game Test

必须注册测试才能在游戏内运行。有两种方式:@GameTestHolder 注解和 RegisterGameTestsEvent。两种方式都仍要求测试方法带有 @GameTest、@GameTestGenerator、@BeforeBatch 或 @AfterBatch 之一。

GameTestHolder

@GameTestHolder 会注册指定类型(类、接口、枚举或 record)中的全部测试方法。它唯一的成员有多种用途。在此场景中,#value 必须为 Mod ID,否则默认配置下不会运行测试。

@GameTestHolder(MODID)
public class ExampleGameTests {
    // ...
}

RegisterGameTestsEvent

也可使用 RegisterGameTestsEvent#register 注册类或方法。事件监听器必须添加到 Mod 事件总线。通过此方式注册时,每个带 @GameTest 的方法都必须用 GameTest#templateNamespace 提供 Mod ID。

// In some event handler class
@SubscribeEvent  // on the mod event bus
public static void registerTests(RegisterGameTestsEvent event) {
    event.register(ExampleGameTests.class);
}

// In ExampleGameTests
@GameTest(templateNamespace = MODID)
public static void exampleTest3(GameTestHelper helper) {
    // Perform setup
}

提示 GameTestHolder#value 与 GameTest#templateNamespace 可以不同于当前 Mod ID,但需要修改构建脚本配置。

结构模板

测试在由结构(模板)加载的场景中执行。模板定义场景尺寸,以及要加载的初始方块、实体数据。模板必须以 .nbt 文件保存于 data/<namespace>/structure。

建议 可以使用结构方块创建和保存模板。

模板位置取决于三个因素:是否指定命名空间、是否在模板名前添加类名,以及是否指定模板名。

命名空间优先使用 GameTest#templateNamespace;未指定时使用 GameTestHolder#value;两者都未指定时使用 minecraft。

如果在带测试注解的类或方法上添加 @PrefixGameTestTemplate(false),则不会添加简单类名。否则,将简单类名转为小写,后接一个点,再置于模板名前。

模板名由 GameTest#template 决定。未指定时使用小写方法名。

// Modid for all structures will be MODID
@GameTestHolder(MODID)
public class ExampleGameTests {

    // Class name is prepended, template name is not specified
    // Template Location at 'modid:examplegametests.exampletest'
    @GameTest
    public static void exampleTest(GameTestHelper helper) { /*...*/ }

    // Class name is not prepended, template name is not specified
    // Template Location at 'modid:exampletest2'
    @PrefixGameTestTemplate(false)
    @GameTest
    public static void exampleTest2(GameTestHelper helper) { /*...*/ }

    // Class name is prepended, template name is specified
    // Template Location at 'modid:examplegametests.test_template'
    @GameTest(template = "test_template")
    public static void exampleTest3(GameTestHelper helper) { /*...*/ }

    // Class name is not prepended, template name is specified
    // Template Location at 'modid:test_template2'
    @PrefixGameTestTemplate(false)
    @GameTest(template = "test_template2")
    public static void exampleTest4(GameTestHelper helper) { /*...*/ }
}

运行 Game Tests

使用 /test 命令运行。此命令有很多配置,运行测试时主要用到以下子命令:

子命令 说明
run 运行指定测试:run <test_name>。
runall 运行所有可用测试。
runclosest 运行玩家 15 格内最近的测试。
runthese 运行玩家 200 格内的测试。
runfailed 运行上次执行中失败的全部测试。

提示 子命令跟在 test 后:/test <subcommand>。

构建脚本配置

Game Tests 可在 build.gradle 中额外配置,以便在不同设置下运行和集成。

启用其他命名空间

如果按推荐方式设置构建脚本,默认仅启用当前 Mod ID 下的测试。要加载其他命名空间,需在运行配置中将 neoforge.enabledGameTestNamespaces 设为逗号分隔的命名空间字符串。该属性为空或未设置时,将加载所有命名空间。

// Inside a run configuration
property 'neoforge.enabledGameTestNamespaces', 'modid1,modid2,modid3'

注意 命名空间之间不能有空格,否则无法正确加载。

Game Test Server 运行配置

Game Test Server 是用于运行构建服务器的特殊配置。其退出码等于必需测试中失败的数量。所有失败测试都会写入日志,无论是否必需。可通过 gradlew runGameTestServer 运行。

NeoGradle 的重要说明

注意 由于 Gradle 的行为,默认情况下如果任务强制退出系统,会结束 Gradle 守护进程,让运行器报告构建失败。NeoGradle 默认在运行任务上启用强制退出,以免子项目依次执行。因此,Game Test Server 总会被报告为失败。

可通过运行配置的 #setForceExit 方法禁用强制退出:

// Game Test Server run configuration
gameTestServer {
    // ...
    setForceExit false
}

在其他运行配置中启用 Game Tests

默认仅在 client、server 和 gameTestServer 配置中启用测试。如果其他运行配置也要执行测试,必须将 neoforge.enableGameTest 属性设为 true。

// Inside a run configuration
property 'neoforge.enableGameTest', 'true'

来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交 4a680cdd4d15。