游戏测试
游戏测试(Game Test)是在游戏内运行单元测试的方式。该系统可扩展且支持并行,能高效运行大量不同测试。测试对象交互与行为,只是这套框架的众多用途之一。它既可以完全由代码实现,也可以通过数据包实现,下面将介绍这两种方式。
创建游戏测试
标准游戏测试包含四个基本步骤:
- 加载包含测试场景的结构(也称模板),在其中测试交互或行为。
- 准备供测试运行的环境。
- 使用已注册函数运行测试逻辑。达到成功状态时测试通过;否则测试失败,结果存放在场景旁的讲台中。
- 使用测试实例,将以上三个对象关联起来。
测试数据
所有测试实例都持有某个 TestData,定义游戏测试的运行方式,包括初始配置、环境和所用结构模板。TestData 通过 MapCodec 序列化,因此这些数据会与其他实例专用参数一同存储在文件根层级。
JSON
// For some game test examplemod:example_test
// In 'data/examplemod/test_instance/example_test.json'
{
// `TestData`
// The environment to run the test in
// Points to 'data/examplemod/test_environment/example_environment.json'
"environment": "examplemod:example_environment",
// The structure used for the game test
// Points to 'data/examplemod/structure/example_structure.nbt'
"structure": "examplemod:example_structure",
// The number of ticks that the game test will run until it automatically fails
"max_ticks": 400,
// The number of ticks that are used to setup everything required for the game test
// This is not counted towards the maximum number of ticks the test can take
// If not specified, defaults to 0
"setup_ticks": 50,
// Whether the test is required to succeed to mark the batch run as successful
// If not specified, defaults to true
"required": true,
// Specifies how the structure and all subsequent helper methods should be rotated for the test
// If not specified, nothing is rotated
// Can be 'none', 'clockwise_90', '180', 'counterclockwise_90'
"rotation": "clockwise_90",
// When true, the test can only be ran through the `/test` command
// If not specified, defaults to false
"manual_only": true,
// Specifies the maximum number of times that the test can be reran
// If not specified, defaults to 1
"max_attempts": 3,
// Specifies the minimum number of successes that must occur for a test to be marked as successful
// This must be less than or equal to the maximum number of attempts allowed
// If not specified, defaults to 1
"required_successes": 1,
// Returns whether the structure boundary should keep the top empty
// This is currently only used in block-based test instances
// If not specified, defaults to false
"sky_access": false
// ...
}
数据生成
// Let's assume we have some test environment
public static final ResourceKey<TestEnvironmentDefinition<?>> EXAMPLE_ENVIRONMENT = ResourceKey.create(
Registries.TEST_ENVIRONMENT,
Identifier.fromNamespaceAndPath("examplemod", "example_environment")
);
@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
event.createDatapackRegistryObjects(
new RegistrySetBuilder().add(Registries.TEST_INSTANCE, bootstrap -> {
// Use this to get the test environments
HolderGetter<TestEnvironmentDefinition<?>> environments = bootstrap.lookup(Registries.TEST_ENVIRONMENT);
// Register a game test
// Any fields not relevant to the test data are hidden
bootstrap.register(..., new FunctionGameTestInstance(...,
new TestData<>(
// The environment to run the test in
// Points to 'data/examplemod/test_environment/example_environment.json'
environments.getOrThrow(EXAMPLE_ENVIRONMENT),
// The structure used for the game test
// Points to 'data/examplemod/structure/example_structure.nbt'
Identifier.fromNamespaceAndPath("examplemod", "example_structure"),
// The number of ticks that the game test will run until it automatically fails
400,
// The number of ticks that are used to setup everything required for the game test
// This is not counted towards the maximum number of ticks the test can take
// If not specified, defaults to 0
50,
// Whether the test is required to succeed to mark the batch run as successful
// If not specified, defaults to true
true,
// Specifies how the structure and all subsequent helper methods should be rotated for the test
// If not specified, nothing is rotated
// Can be 'none', 'clockwise_90', '180', 'counterclockwise_90'
Rotation.CLOCKWISE_90,
// When true, the test can only be ran through the `/test` command
// If not specified, defaults to false
true,
// Specifies the maximum number of times that the test can be reran
// If not specified, defaults to 1
3,
// Specifies the minimum number of successes that must occur for a test to be marked as successful
// This must be less than or equal to the maximum number of attempts allowed
// If not specified, defaults to 1
1,
// Returns whether the structure boundary should keep the top empty
// This is currently only used in block-based test instances
// If not specified, defaults to false
false
)
));
})
);
}
结构模板
游戏测试在由结构(或模板)加载的场景中执行。所有模板都定义场景尺寸,以及初始加载的数据(方块和实体)。模板必须以 .nbt 文件存放在 data/<namespace>/structure 中。TestData#structure 使用相对 Identifier 引用 NBT 文件,例如 examplemod:example_structure 指向 data/examplemod/structure/example_structure.nbt。
测试环境
所有游戏测试都在某个 TestEnvironmentDefinition 中运行,由它决定如何设置当前 ServerLevel。测试完成后会清理环境,让后续一个或多个实例运行。环境会按批次处理:具有相同环境的多个测试实例会同时运行。测试环境文件位于 data/<namespace>/test_environment/<path>.json。
原版提供了不会修改 ServerLevel 的 minecraft:default。此外,还支持以下定义类型来构建环境。
游戏规则
此环境类型设置测试使用的游戏规则。清理环境时,游戏规则会恢复默认值。
JSON
// examplemod:example_environment
// In 'data/examplemod/test_environment/example_environment.json'
{
"type": "minecraft:game_rules",
// A map of game rules to their set values
"rules": {
"minecraft:fire_damage": false,
"minecraft:players_sleeping_percentage": 50
}
}
数据生成
// Let's assume we have some test environment
public static final ResourceKey<TestEnvironmentDefinition<?>> EXAMPLE_ENVIRONMENT = ResourceKey.create(
Registries.TEST_ENVIRONMENT,
Identifier.fromNamespaceAndPath("examplemod", "example_environment")
);
@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
event.createDatapackRegistryObjects(
new RegistrySetBuilder().add(Registries.TEST_ENVIRONMENT, bootstrap -> {
// Register the environment
bootstrap.register(
EXAMPLE_ENVIRONMENT,
new TestEnvironmentDefinition.SetGameRules(
new GameRuleMap.Builder()
// A map of game rules to their set values
.set(GameRules.FIRE_DAMAGE, false)
.set(GameRules.PLAYERS_SLEEPING_PERCENTAGE, 50)
.build()
)
);
})
);
}
时钟时间
此环境类型将指定 WorldClock 的时间设为某个非负整数,类似使用 /time of <clock> set <number> 命令。
JSON
// examplemod:example_environment
// In 'data/examplemod/test_environment/example_environment.json'
{
"type": "minecraft:clock_time",
// The clock to set the time of
// Points to a registered clock at `data/<namespace>/world_clock/<path>.json`
"clock": "minecraft:overworld",
// Sets the time of the clock
"time": 13000
}
数据生成
// Let's assume we have some test environment
public static final ResourceKey<TestEnvironmentDefinition<?>> EXAMPLE_ENVIRONMENT = ResourceKey.create(
Registries.TEST_ENVIRONMENT,
Identifier.fromNamespaceAndPath("examplemod", "example_environment")
);
@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
event.createDatapackRegistryObjects(
new RegistrySetBuilder().add(Registries.TEST_ENVIRONMENT, bootstrap -> {
// Getting clocks
HolderGetter<WorldClock> clocks = bootstrap.lookup(Registries.WORLD_CLOCK);
// Register the environment
bootstrap.register(
EXAMPLE_ENVIRONMENT,
new TestEnvironmentDefinition.ClockTime(
// The clock to set the time of
clocks.getOrThrow(WorldClocks.OVERWORLD),
// Sets the time of the clock
13000
)
);
})
);
}
时间线属性
此环境类型设置要应用于世界环境属性的时间线。
JSON
// examplemod:example_environment
// In 'data/examplemod/test_environment/example_environment.json'
{
"type": "minecraft:timeline_attributes",
// The timelines to apply to the level
"timelines": [
"minecraft:day",
"minecraft:moon"
]
}
数据生成
// Let's assume we have some test environment
public static final ResourceKey<TestEnvironmentDefinition<?>> EXAMPLE_ENVIRONMENT = ResourceKey.create(
Registries.TEST_ENVIRONMENT,
Identifier.fromNamespaceAndPath("examplemod", "example_environment")
);
@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
event.createDatapackRegistryObjects(
new RegistrySetBuilder().add(Registries.TEST_ENVIRONMENT, bootstrap -> {
// Getting timelines
HolderGetter<Timeline> timelines = bootstrap.lookup(Registries.TIMELINE);
// Register the environment
bootstrap.register(
EXAMPLE_ENVIRONMENT,
new TestEnvironmentDefinition.Timelines(
// The timelines to apply to the level
List.of(
timelines.getOrThrow(Timelines.OVERWORLD_DAY),
timelines.getOrThrow(Timelines.MOON)
)
)
);
})
);
}
天气
此环境类型设置天气,类似使用 /weather 命令。
JSON
// examplemod:example_environment
// In 'data/examplemod/test_environment/example_environment.json'
{
"type": "minecraft:weather",
// Can be one of three values:
// - clear (No weather)
// - rain (Rain)
// - thunder (Rain and thunder)
"weather": "thunder"
}
数据生成
// Let's assume we have some test environment
public static final ResourceKey<TestEnvironmentDefinition<?>> EXAMPLE_ENVIRONMENT = ResourceKey.create(
Registries.TEST_ENVIRONMENT,
Identifier.fromNamespaceAndPath("examplemod", "example_environment")
);
@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
event.createDatapackRegistryObjects(
new RegistrySetBuilder().add(Registries.TEST_ENVIRONMENT, bootstrap -> {
// Register the environment
bootstrap.register(
EXAMPLE_ENVIRONMENT,
new TestEnvironmentDefinition.Weather(
// Can be one of three values:
// - clear (No weather)
// - rain (Rain)
// - thunder (Rain and thunder)
TestEnvironmentDefinition.Weather.Type.THUNDER
)
);
})
);
}
Minecraft 函数
此环境类型提供两个指向 mcfunction 的标识符,分别用于设置和清理世界。
JSON
// examplemod:example_environment
// In 'data/examplemod/test_environment/example_environment.json'
{
"type": "minecraft:function",
// The setup mcfunction to use
// If not specified, nothing will be ran
// Points to 'data/examplemod/function/example/setup.mcfunction'
"setup": "examplemod:example/setup",
// The teardown mcfunction to use
// If not specified, nothing will be ran
// Points to 'data/examplemod/function/example/teardown.mcfunction'
"teardown": "examplemod:example/teardown"
}
数据生成
// Let's assume we have some test environment
public static final ResourceKey<TestEnvironmentDefinition<?>> EXAMPLE_ENVIRONMENT = ResourceKey.create(
Registries.TEST_ENVIRONMENT,
Identifier.fromNamespaceAndPath("examplemod", "example_environment")
);
@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
event.createDatapackRegistryObjects(
new RegistrySetBuilder().add(Registries.TEST_ENVIRONMENT, bootstrap -> {
// Register the environment
bootstrap.register(
EXAMPLE_ENVIRONMENT,
new TestEnvironmentDefinition.Functions(
// The setup mcfunction to use
// If not specified, nothing will be ran
// Points to 'data/examplemod/function/example/setup.mcfunction'
Optional.of(Identifier.fromNamespaceAndPath("examplemod", "example/setup")),
// The teardown mcfunction to use
// If not specified, nothing will be ran
// Points to 'data/examplemod/function/example/teardown.mcfunction'
Optional.of(Identifier.fromNamespaceAndPath("examplemod", "example/teardown"))
)
);
})
);
}
组合环境
组合环境类型可以合并多个环境。定义列表可以包含对现有定义的引用,也可以包含内联定义。
JSON
// examplemod:example_environment
// In 'data/examplemod/test_environment/example_environment.json'
{
"type": "minecraft:all_of",
// A list of test environments to use
// Can either specified the registry name or the environment itself
"definitions": [
// Points to 'data/minecraft/test_environment/default.json'
"minecraft:default",
{
// A raw environment definition
"type": "..."
}
// ...
]
}
数据生成
// Let's assume we have some test environment
public static final ResourceKey<TestEnvironmentDefinition<?>> EXAMPLE_ENVIRONMENT = ResourceKey.create(
Registries.TEST_ENVIRONMENT,
Identifier.fromNamespaceAndPath("examplemod", "example_environment")
);
@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
event.createDatapackRegistryObjects(
new RegistrySetBuilder().add(Registries.TEST_ENVIRONMENT, bootstrap -> {
// Getting existing environments
HolderGetter<TestEnvironmentDefinition<?>> environments = bootstrap.lookup(Registries.TEST_ENVIRONMENT);
// Register the environment
bootstrap.register(
EXAMPLE_ENVIRONMENT,
new TestEnvironmentDefinition.AllOf(
List.of(
// Points to 'data/minecraft/test_environment/default.json'
environments.getOrThrow(GameTestEnvironments.DEFAULT_KEY),
Holder.direct(
// Create a new TestEnvironmentDefinition here
...
)
// ...
)
)
);
})
);
}
自定义定义类型
自定义 TestEnvironmentDefinition<SavedDataType> 类型提供三个方法:setup 修改 ServerLevel 并返回之前的 SavedDataType 泛型状态;teardown 使用 SavedDataType 恢复被修改的内容;codec 提供用于编解码该类型的 MapCodec:
public record ExampleEnvironmentType(int value1, boolean value2) implements TestEnvironmentDefinition<Pair<Integer, Boolean>> {
// Construct the map codec to register
public static final MapCodec<ExampleEnvironmentType> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Codec.INT.fieldOf("value1").forGetter(ExampleEnvironmentType::value1),
Codec.BOOL.fieldOf("value2").forGetter(ExampleEnvironmentType::value2)
).apply(instance, ExampleEnvironmentType::new)
);
@Override
public Pair<Integer, Boolean> setup(ServerLevel level) {
// Setup whatever is necessary here
// return the original values of the modified level data
}
@Override
public void teardown(ServerLevel level, Pair<Integer, Boolean> originalState) {
// Undo whatever was changed within the setup method
// This use the original state to reset the data
}
@Override
public MapCodec<ExampleEnvironmentType> codec() {
return CODEC;
}
}
接着,可以注册该 MapCodec:
public static final DeferredRegister<MapCodec<? extends TestEnvironmentDefinition>> TEST_ENVIRONMENT_DEFINITION_TYPES = DeferredRegister.create(
BuiltInRegistries.TEST_ENVIRONMENT_DEFINITION_TYPE,
"examplemod"
);
public static final Supplier<MapCodec<ExampleEnvironmentType>> EXAMPLE_ENVIRONMENT_CODEC = TEST_ENVIRONMENT_DEFINITION_TYPES.register(
"example_environment_type",
() -> ExampleEnvironmentType.CODEC
);
最后,就可以在环境定义中使用此类型:
JSON
// examplemod:example_environment
// In 'data/examplemod/test_environment/example_environment.json'
{
"type": "examplemod:example_environment_type",
"value1": 0,
"value2": true
}
数据生成
// Let's assume we have some test environment
public static final ResourceKey<TestEnvironmentDefinition<?>> EXAMPLE_ENVIRONMENT = ResourceKey.create(
Registries.TEST_ENVIRONMENT,
Identifier.fromNamespaceAndPath("examplemod", "example_environment")
);
@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
event.createDatapackRegistryObjects(
new RegistrySetBuilder().add(Registries.TEST_ENVIRONMENT, bootstrap -> {
// Register the environment
bootstrap.register(
EXAMPLE_ENVIRONMENT,
new ExampleEnvironmentType(
0, true
)
);
})
);
}
测试函数
游戏测试的基本概念围绕一个接收 GameTestHelper、不返回值的方法展开。调用 GameTestHelper 中的方法,决定测试成功还是失败。每个测试函数都需要注册,以便在测试实例中引用:
public class ExampleFunctions {
// Here is our example function
public static void exampleTest(GameTestHelper helper) {
// Do Stuff
}
}
// Register our function for use
public static final DeferredRegister<Consumer<GameTestHelper>> TEST_FUNCTION = DeferredRegister.create(
BuiltInRegistries.TEST_FUNCTION,
"examplemod"
);
public static final DeferredHolder<Consumer<GameTestHelper>, Consumer<GameTestHelper>> EXAMPLE_FUNCTION = TEST_FUNCTION.register(
"example_function",
() -> ExampleFunctions::exampleTest
);
相对定位
所有测试函数都根据结构方块当前位置,将结构模板场景中的相对坐标转换为绝对坐标。为方便在相对和绝对位置间转换,可以分别使用 GameTestHelper#absolutePos 和 GameTestHelper#relativePos。
在游戏内,可以通过测试命令加载结构,将玩家移动到目标位置,再运行 /test pos,获取结构模板内的相对位置。该命令会根据玩家 200 格以内最近的结构,获取玩家的相对坐标,并在聊天中输出可复制的文本组件,供用作 final 局部变量。
建议 在
/test pos末尾追加名称,可以指定生成局部变量的引用名称:/test pos <var> # Exports 'final BlockPos <var> = new BlockPos(...);'
成功完成
测试函数负责一件事:在满足有效完成条件时,将测试标记为成功。如果在 TestData#maxTicks 定义的超时前没有达到成功状态,测试就会自动失败。
GameTestHelper 中有许多用于定义成功状态的抽象方法,其中四个尤其重要。
| 方法 | 描述 |
|---|---|
#succeed |
将测试标记为成功。 |
#succeedIf |
立即执行提供的 Runnable;没有抛出 GameTestAssertException 就成功。如果当前 tick 未成功,测试就标记为失败。 |
#succeedWhen |
在超时前每 tick 执行提供的 Runnable;某个 tick 的检查没有抛出 GameTestAssertException 就成功。 |
#succeedOnTickWhen |
在指定 tick 检查提供的 Runnable;没有抛出 GameTestAssertException 就成功。如果 Runnable 在其他 tick 成功,则测试标记为失败。 |
注意 游戏测试会每 tick 执行,直到被标记为成功。因此,安排在指定 tick 成功的方法,必须确保在此前所有 tick 都失败。
调度操作
并非所有操作都在测试开始时发生。可以安排操作在特定时刻或间隔执行:
| 方法 | 描述 |
|---|---|
#runAtTickTime |
在指定 tick 执行操作。 |
#runAfterDelay |
在当前 tick 之后的 x tick 执行操作。 |
#onEachTick |
每 tick 执行操作。 |
断言
游戏测试期间可以随时断言,检查某个条件是否为真。GameTestHelper 包含众多断言方法,基本原则是在不满足相应状态时抛出 GameTestAssertException。
注册测试实例
准备好 TestData、TestEnvironmentDefinition 和测试函数后,就可以通过 GameTestInstance 将它们关联起来。每个测试实例代表一个待运行的游戏测试。所有实例位于 data/<namespace>/test_instance/<path>.json。
基于函数的测试
FunctionGameTestInstance 将 TestData 关联到已注册的测试函数。实例被调用时,会运行该测试函数。
JSON
// For some game test examplemod:example_test
// In 'data/examplemod/test_instance/example_test.json'
{
// `TestData`
"environment": "examplemod:example_environment",
"structure": "examplemod:example_structure",
"max_ticks": 400,
"setup_ticks": 50,
"required": true,
"rotation": "clockwise_90",
"manual_only": true,
"max_attempts": 3,
"required_successes": 1,
"sky_access": false,
// `FunctionGameTestInstance`
"type": "minecraft:function",
// Points to a 'Consumer<GameTestHelper>' in the test function registry
"function": "examplemod:example_function"
}
数据生成
// The test instance key
public static final ResourceKey<GameTestInstance> EXAMPLE_TEST_INSTANCE = ResourceKey.create(
Registries.TEST_INSTANCE,
Identifier.fromNamespaceAndPath("examplemod", "example_test")
);
// Let's assume we have some test environment
public static final ResourceKey<TestEnvironmentDefinition<?>> EXAMPLE_ENVIRONMENT = ResourceKey.create(
Registries.TEST_ENVIRONMENT,
Identifier.fromNamespaceAndPath("examplemod", "example_environment")
);
@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
event.createDatapackRegistryObjects(
new RegistrySetBuilder().add(Registries.TEST_INSTANCE, bootstrap -> {
// Use this to get the test environments
HolderGetter<TestEnvironmentDefinition<?>> environments = bootstrap.lookup(Registries.TEST_ENVIRONMENT);
// Register a game test
// Any fields not relevant to the test data are hidden
bootstrap.register(EXAMPLE_TEST_INSTANCE,
new FunctionGameTestInstance(
// Points to a 'Consumer<GameTestHelper>' in the test function registry
EXAMPLE_FUNCTION.getKey()
new TestData<>(
environments.getOrThrow(EXAMPLE_ENVIRONMENT),
Identifier.fromNamespaceAndPath("examplemod", "example_structure"),
400,
50,
true,
Rotation.CLOCKWISE_90,
true,
3,
1,
false
)
));
})
);
}
基于方块的测试
BlockBasedTestInstance 是依赖 Blocks#TEST_BLOCK 收发红石信号的特殊测试实例。结构模板必须至少包含两个测试方块:恰好一个设为 TestBlockMode#START,另一个设为 TestBlockMode#ACCEPT。测试开始时,起始测试方块被触发,发出持续一个 tick、强度 15 的脉冲。预期该信号最终触发处于 LOG、FAIL 或 ACCEPT 状态的其他测试方块。LOG 方块激活时也会发出强度 15 的脉冲。ACCEPT 和 FAIL 方块分别使实例成功或失败。同一 tick 中,ACCEPT 始终优先于 FAIL。
JSON
// For some game test examplemod:example_test
// In 'data/examplemod/test_instance/example_test.json'
{
// `TestData`
"environment": "examplemod:example_environment",
"structure": "examplemod:example_structure",
"max_ticks": 400,
"setup_ticks": 50,
"required": true,
"rotation": "clockwise_90",
"manual_only": true,
"max_attempts": 3,
"required_successes": 1,
"sky_access": false,
// `BlockBasedTestInstance`
"type": "minecraft:block_based"
}
数据生成
// The test instance key
public static final ResourceKey<GameTestInstance> EXAMPLE_TEST_INSTANCE = ResourceKey.create(
Registries.TEST_INSTANCE,
Identifier.fromNamespaceAndPath("examplemod", "example_test")
);
// Let's assume we have some test environment
public static final ResourceKey<TestEnvironmentDefinition<?>> EXAMPLE_ENVIRONMENT = ResourceKey.create(
Registries.TEST_ENVIRONMENT,
Identifier.fromNamespaceAndPath("examplemod", "example_environment")
);
@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
event.createDatapackRegistryObjects(
new RegistrySetBuilder().add(Registries.TEST_INSTANCE, bootstrap -> {
// Use this to get the test environments
HolderGetter<TestEnvironmentDefinition<?>> environments = bootstrap.lookup(Registries.TEST_ENVIRONMENT);
// Register a game test
// Any fields not relevant to the test data are hidden
bootstrap.register(EXAMPLE_TEST_INSTANCE,
new BlockBasedTestInstance(
new TestData<>(
environments.getOrThrow(EXAMPLE_ENVIRONMENT),
Identifier.fromNamespaceAndPath("examplemod", "example_structure"),
400,
50,
true,
Rotation.CLOCKWISE_90,
true,
3,
1,
false
)
));
})
);
}
自定义测试实例
如果需要自行实现测试逻辑,可以继承 GameTestInstance。必须实现两个方法:表示测试函数的 run,以及提供实例描述的 typeDescription。如果需要在数据生成中使用该实例,它就必须有一个需要注册的 MapCodec。
public class ExampleTestInstance extends GameTestInstance {
public ExampleTestInstance(int value1, boolean value2, TestData<Holder<TestEnvironmentDefinition>> info) {
super(info);
}
@Override
public void run(GameTestHelper helper) {
// Run whatever game test commands you want
helper.assertBlockPresent(...);
// Make sure you have some way to succeed
helper.succeedIf(() -> ...);
}
@Override
public MapCodec<ExampleTestInstance> codec() {
return EXAMPLE_INSTANCE_CODEC.get();
}
@Override
protected MutableComponent typeDescription() {
// Provides a description about what this test is supposed to be
// Should use a translatable component
return Component.literal("Example Test Instance");
}
}
// Register our test instance for use
public static final DeferredRegister<MapCodec<? extends GameTestInstance>> TEST_INSTANCE = DeferredRegister.create(
BuiltInRegistries.TEST_INSTANCE_TYPE,
"examplemod"
);
public static final Supplier<MapCodec<? extends GameTestInstance>> EXAMPLE_INSTANCE_CODEC = TEST_INSTANCE.register(
"example_test_instance",
() -> RecordCodecBuilder.mapCodec(instance -> instance.group(
Codec.INT.fieldOf("value1").forGetter(test -> test.value1),
Codec.BOOL.fieldOf("value2").forGetter(test -> test.value2),
TestData.CODEC.forGetter(ExampleTestInstance::info)
).apply(instance, ExampleTestInstance::new)
)
);
接着,就可以在数据包中使用测试实例:
JSON
// For some game test examplemod:example_test
// In 'data/examplemod/test_instance/example_test.json'
{
// `TestData`
"environment": "examplemod:example_environment",
"structure": "examplemod:example_structure",
"max_ticks": 400,
"setup_ticks": 50,
"required": true,
"rotation": "clockwise_90",
"manual_only": true,
"max_attempts": 3,
"required_successes": 1,
"sky_access": false,
// `ExampleTestInstance`
"type": "examplemod:example_test_instance",
"value1": 0,
"value2": true
}
数据生成
// The test instance key
public static final ResourceKey<GameTestInstance> EXAMPLE_TEST_INSTANCE = ResourceKey.create(
Registries.TEST_INSTANCE,
Identifier.fromNamespaceAndPath("examplemod", "example_test")
);
// Let's assume we have some test environment
public static final ResourceKey<TestEnvironmentDefinition<?>> EXAMPLE_ENVIRONMENT = ResourceKey.create(
Registries.TEST_ENVIRONMENT,
Identifier.fromNamespaceAndPath("examplemod", "example_environment")
);
@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
event.createDatapackRegistryObjects(
new RegistrySetBuilder().add(Registries.TEST_INSTANCE, bootstrap -> {
// Use this to get the test environments
HolderGetter<TestEnvironmentDefinition<?>> environments = bootstrap.lookup(Registries.TEST_ENVIRONMENT);
// Register a game test
// Any fields not relevant to the test data are hidden
bootstrap.register(EXAMPLE_TEST_INSTANCE,
new ExampleTestInstance(
0,
true,
new TestData<>(
environments.getOrThrow(EXAMPLE_ENVIRONMENT),
Identifier.fromNamespaceAndPath("examplemod", "example_structure"),
400,
50,
true,
Rotation.CLOCKWISE_90,
true,
3,
1,
false
)
));
})
);
}
跳过数据包
如果不想通过数据包构建游戏测试,可以监听 Mod 事件总线上的 RegisterGameTestsEvent,分别通过 registerEnvironment 和 registerTest 注册环境与测试实例。
@SubscribeEvent // on the mod event bus
public static void registerTests(RegisterGameTestsEvent event) {
Holder<TestEnvironmentDefinition<?>> environment = event.registerEnvironment(
// The name of the test environment
EXAMPLE_ENVIRONMENT.identifier(),
// A varargs of test environment definitions
new ExampleEnvironmentType(
0, true
)
);
event.registerTest(
// The name of the test instance
EXAMPLE_TEST_INSTANCE.identifier(),
new ExampleTestInstance(
0,
true,
new TestData<>(
environment,
Identifier.fromNamespaceAndPath("examplemod", "example_structure"),
400,
50,
true,
Rotation.CLOCKWISE_90,
true,
3,
1,
false
)
)
);
}
运行游戏测试
可以使用 /test 命令运行游戏测试。该命令配置灵活,以下子命令与运行测试尤其相关:
| 子命令 | 描述 |
|---|---|
run |
运行指定测试:run <test_name>。 |
runall |
运行所有可用测试。 |
runclosest |
运行玩家 15 格内最近的测试。 |
runthese |
运行玩家 200 格以内的测试。 |
runfailed |
运行上一次运行中失败的所有测试。 |
提示 子命令放在 test 命令之后:
/test <subcommand>。
构建脚本配置
游戏测试在构建脚本(build.gradle)中提供额外配置,可运行测试并将其集成到不同环境。
游戏测试服务器运行配置
游戏测试服务器是一种用于运行构建服务器的特殊配置。构建服务器返回的退出码等于必需但失败的游戏测试数量。所有失败的测试(无论是否必需)都会记录到日志。可以使用 gradlew runGameTestServer 运行该服务器。
在其他运行配置中启用游戏测试
默认情况下,只有 client 和 gameTestServer 运行配置启用游戏测试。要让其他运行配置运行游戏测试,必须将 neoforge.enableGameTest 属性设为 true。
// Inside a run configuration
property 'neoforge.enableGameTest', 'true'
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。