Skip to main content
Version: 1.21.4

游戏测试(Game Tests)

游戏测试(Game Tests)是一种在游戏内运行单元测试的方式。该系统设计为可扩展且支持并行运行,能够高效地执行大量不同的测试。测试对象之间的交互和行为只是这个框架众多应用场景中的一部分。

创建一个游戏测试(Game Test)

一个标准的游戏测试通常遵循以下三个基本步骤:

  1. 加载一个结构(structure)或模板(template),用于承载需要进行交互或行为测试的场景。
  2. 用一个方法(method)来实现对该场景进行操作的逻辑。
  3. 执行该方法的逻辑。如果达到了预期的成功状态,则测试通过。否则,测试失败,结果会被存储在场景旁边的一个讲台(lectern)中。

因此,要创建一个游戏测试,必须有一个包含场景初始状态的模板,以及一个实现具体测试逻辑的方法。

测试方法(The Test Method)

一个游戏测试方法是一个 Consumer<GameTestHelper> 类型的引用,意味着它接收一个 GameTestHelper 参数且不返回任何内容。要让游戏测试方法被识别,必须加上 @GameTest 注解(annotation):

public class ExampleGameTests {
@GameTest
public static void exampleTest(GameTestHelper helper) {
// 执行测试相关操作
}
}

@GameTest 注解还包含了一些成员,用于配置该游戏测试的运行方式。

// 在某个类中
@GameTest(
setupTicks = 20L, // 测试在执行前会花费 20 个 tick 进行设置
required = false // 失败时只记录日志,不影响该批次的执行
)
public static void exampleConfiguredTest(GameTestHelper helper) {
// 执行测试相关操作
}

相对定位(Relative Positioning)

所有 GameTestHelper 的方法都会将结构模板场景中的相对坐标,转换为基于结构方块(structure block)当前位置的绝对坐标。为了便于相对坐标与绝对坐标之间的转换,可以分别使用 GameTestHelper#absolutePosGameTestHelper#relativePos

结构模板的相对位置可以通过在游戏中使用 test 指令 加载结构、将玩家移动到期望的位置,然后运行 /test pos 指令来获得。该指令会获取距离玩家 200 格范围内最近结构的相对坐标,并以可复制的文本组件形式输出到聊天栏,方便作为最终的本地变量使用。

tip

通过 /test pos 生成的本地变量可以通过在命令末尾添加引用名称来指定变量名:

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

成功完成(Successful Completion)

一个游戏测试方法的唯一职责是:在达到有效的完成状态时标记测试为成功。如果在超时时间内(由 GameTest#timeoutTicks 定义)未达到成功状态,则测试会自动判定为失败。 GameTestHelper 中包含许多可用于定义测试成功状态的抽象方法;但其中有四个方法尤其重要,需要重点了解。

方法描述
#succeed将测试标记为成功。
#succeedIf立即测试所提供的 Runnable,如果未抛出 GameTestAssertException,则测试成功。如果在当前 tick 未成功,则测试标记为失败。
#succeedWhen每 tick 都会测试所提供的 Runnable,直到超时为止;如果某个 tick 的检查未抛出 GameTestAssertException,则测试成功。
#succeedOnTickWhen在指定的 tick 运行所提供的 Runnable,如果未抛出 GameTestAssertException,则测试成功。如果在其他任意 tick 成功,则测试被标记为失败。
caution

游戏测试会在每一个 tick 被执行,直到测试被标记为成功。因此,那些安排在特定 tick 上成功的方法,必须确保在之前的任何 tick 都会失败。

调度操作(Scheduling Actions)

并不是所有操作都会在测试开始时立即执行。你可以将操作调度到特定时间点或间隔来执行:

方法描述
#runAtTickTime在指定的 tick 执行该操作。
#runAfterDelay在当前 tick 之后延迟 x 个 tick 执行该操作。
#onEachTick在每一个 tick 执行该操作。

断言(Assertions)

在游戏测试的任何时刻,都可以进行断言以检查某个条件是否为真。GameTestHelper 提供了多种断言方法;本质上,当状态不符合预期时,这些方法会抛出 GameTestAssertException

动态生成测试方法(Generated Test Methods)

如果需要动态生成游戏测试方法,可以创建一个测试方法生成器(test method generator)。这些方法不接收任何参数,并返回一个 TestFunction 的集合。要让测试方法生成器被识别,必须使用 @GameTestGenerator 注解:

public class ExampleGameTests {
@GameTestGenerator
public static Collection<TestFunction> exampleTests() {
// 返回一组 TestFunction
}
}

TestFunction

TestFunction 是一个包含了 @GameTest 注解信息以及实际运行测试方法的封装对象。

tip

任何使用 @GameTest 注解的方法,都会通过 GameTestRegistry#turnMethodIntoTestFunction 被转换为 TestFunction。你可以参考该方法,手动创建不依赖注解的 TestFunction

批量执行(Batching)

游戏测试可以按批次(batch)执行,而不是按照注册顺序执行。只需为测试指定相同的 GameTest#batch 字符串,即可将其归入同一批次。 单独使用批处理(batching)本身并没有什么实际作用。然而,批处理可以用于在当前测试运行的关卡(level)上执行初始化(setup)和清理(teardown)操作。要实现这一点,可以通过为某个方法添加 @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) {
// 执行测试相关操作
}
}

注册游戏测试(Registering a Game Test)

要在游戏内运行,游戏测试(Game Test)必须先进行注册。有两种注册方式:通过 @GameTestHolder 注解,或者通过 RegisterGameTestsEvent。无论采用哪种注册方式,测试方法都必须使用 @GameTest@GameTestGenerator@BeforeBatch@AfterBatch 其中之一进行注解。

GameTestHolder

@GameTestHolder 注解会注册该类型(类、接口、枚举或 record)中的所有测试方法。@GameTestHolder 只包含一个方法,但用途多样。在这里,提供的 #value 必须是该模组(mod)的 mod id,否则测试在默认配置下不会运行。

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

RegisterGameTestsEvent

RegisterGameTestsEvent 也可以通过 #register 方法注册类或方法。事件监听器必须被添加到 mod 事件总线(event bus)。以这种方式注册的测试方法,必须为每个带有 @GameTest 注解的方法,通过 GameTest#templateNamespace 指定其 mod id。

// 在某个类中
public void registerTests(RegisterGameTestsEvent event) {
event.register(ExampleGameTests.class);
}

// 在 ExampleGameTests 中
@GameTest(templateNamespace = MODID)
public static void exampleTest3(GameTestHelper helper) {
// 执行初始化操作
}
note

GameTestHolder#valueGameTest#templateNamespace 所提供的值可以与当前的 mod id 不同。此时需要在 构建脚本 中进行相应配置。

结构模板(Structure Templates)

游戏测试(Game Test)是在由结构(structure)或模板(template)加载的场景中执行的。所有模板都定义了场景的尺寸和初始数据(包括方块和实体)。模板必须以 .nbt 文件的形式存储在 data/<namespace>/structure 目录下。

tip

可以使用结构方块(structure block)创建并保存结构模板。

模板的位置由以下几个因素决定:

  • 是否指定了模板的命名空间(namespace)。
  • 是否需要将类名添加在模板名称前。
  • 是否指定了模板的名称。 模板的命名空间(namespace)由 GameTest#templateNamespace 决定;如果未指定,则使用 GameTestHolder#value;如果两者都未指定,则默认为 minecraft

如果在带有测试注解的类或方法上应用了 @PrefixGameTestTemplate 并将其设置为 false,则模板名称前不会加上简单类名(simple class name)。否则,简单类名会被转换为小写,并在模板名称前加上一个点进行拼接。

模板的名称由 GameTest#template 决定。如果未指定,则使用方法名的小写形式作为模板名。

// 所有结构的 modid 都将为 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) { /*...*/ }
}

运行 Game Test(运行游戏测试)

可以使用 /test 命令来运行 Game Test(游戏测试)。test 命令具有高度可配置性;不过,只有少数子命令与实际运行测试密切相关:

子命令(Subcommand)说明(Description)
run运行指定的测试:run <test_name>
runall运行所有可用的测试。
runclosest运行距离玩家 15 格以内最近的测试。
runthese运行距离玩家 200 格以内的所有测试。
runfailed运行上一次测试运行中失败的所有测试。
note

子命令需跟在 test 命令之后使用:/test <subcommand>

Buildscript 配置(Buildscript Configurations)

Game Test(游戏测试)在构建脚本(buildscript,即 build.gradle 文件)中提供了额外的配置选项,用于在不同环境下运行和集成测试。

启用其他命名空间(Enabling Other Namespaces)

如果你的构建脚本已按推荐方式设置,那么只有当前 mod id 下的 Game Test(游戏测试)会被启用。若要让其他命名空间(namespace)下的 Game Test 也被加载,需要在运行配置中设置属性 neoforge.enabledGameTestNamespaces,并将每个命名空间用逗号分隔填写为字符串。如果该属性为空或未设置,则会加载所有命名空间。

// 在运行配置中
property 'neoforge.enabledGameTestNamespaces', 'modid1,modid2,modid3'
caution

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

Game Test 服务器运行配置(Game Test Server Run Configuration)

Game Test 服务器是一种特殊配置,用于运行构建服务器。该构建服务器会返回一个退出码,表示必需的、失败的 Game Test 数量。所有失败的测试(无论是否必需)都会被记录到日志中。可以通过 gradlew runGameTestServer 启动此服务器。

关于 NeoGradle 的重要信息
caution

由于 Gradle 的工作机制存在一些特殊情况,默认情况下,如果某个任务强制系统退出,Gradle 守护进程会被终止,导致 Gradle 运行器报告构建失败。NeoGradle 默认会对运行任务设置强制退出,以保证不会顺序执行任何子项目。但因此,Game Test 服务器总是会失败。

你可以通过在运行配置中使用 #setForceExit 方法禁用强制退出来解决这个问题:

// Game Test 服务器运行配置
gameTestServer {
// ...
setForceExit false
}

在其他运行配置中启用 Game Test(Enabling Game Tests in Other Run Configurations)

默认情况下,只有 clientservergameTestServer 运行配置启用了 Game Test。如果你希望其他运行配置也能运行 Game Test,需要将 neoforge.enableGameTest 属性设置为 true

// 在运行配置中
property 'neoforge.enableGameTest', 'true'