Skip to content

游戏测试(Game Tests)

字数
2248 字
阅读时间
9 分钟

游戏测试(Game Tests)

游戏测试是一种在游戏内运行单元测试的方式。该系统设计为可扩展且并行运行,能够高效地执行大量不同的测试。测试对象交互和行为是该框架的众多应用之一。


创建游戏测试

标准的游戏测试遵循三个基本步骤:

  1. 加载一个结构或模板,该模板包含用于测试交互或行为的场景。
  2. 编写一个方法来执行场景中的逻辑。
  3. 执行方法逻辑。如果达到成功状态,则测试成功;否则,测试失败,结果存储在场景附近的讲台(lectern)中。

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


测试方法

游戏测试方法是一个 Consumer<GameTestHelper>​ 引用,这意味着它接收一个 GameTestHelper​ 并返回空。为了使游戏测试方法被识别,必须使用 @GameTest​ 注解:

java
public class ExampleGameTests {
    @GameTest
    public static void exampleTest(GameTestHelper helper) {
        // 执行逻辑
    }
}

@GameTest​ 注解还包含配置游戏测试运行方式的成员:

java
@GameTest(
    setupTicks = 20L, // 测试花费 20 Tick 进行设置
    required = false  // 失败会被记录,但不会影响批处理的执行
)
public static void exampleConfiguredTest(GameTestHelper helper) {
    // 执行逻辑
}

相对定位

所有 GameTestHelper​ 方法都将结构模板场景中的相对坐标转换为绝对坐标。为了简化相对坐标和绝对坐标之间的转换,可以使用 GameTestHelper#absolutePos​ 和 GameTestHelper#relativePos​。

在游戏中,可以通过 /test​ 命令加载结构模板,将玩家放置在所需位置,然后运行 /test pos​ 命令来获取结构的相对坐标。该命令会将相对坐标作为可复制的文本组件输出到聊天中。

提示: 可以通过在命令末尾附加变量名来指定引用名称:

plaintext
/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​ 注解:

java
public class ExampleGameTests {
    @GameTestGenerator
    public static Collection<TestFunction> exampleTests() {
        // 返回 TestFunction 集合
    }
}

TestFunction​ 是 @GameTest​ 注解和运行测试的方法所包含的装箱信息。

提示: 任何使用 @GameTest​ 注解的方法都会通过 GameTestRegistry#turnMethodIntoTestFunction​ 转换为 TestFunction​。该方法可用作创建 TestFunction​ 的参考,而无需使用注解。


批处理

游戏测试可以按批处理执行,而不是按注册顺序执行。通过为 GameTest#batch​ 提供相同的字符串,可以将测试添加到批处理中。

批处理本身并不提供任何有用的功能。但是,批处理可用于在当前测试运行的级别上执行设置和拆卸状态。这是通过使用 @BeforeBatch​ 进行设置或使用 @AfterBatch​ 进行拆卸来实现的。#batch​ 方法必须与游戏测试提供的字符串匹配。

批处理方法是 Consumer<ServerLevel>​ 引用,这意味着它们接收 ServerLevel​ 并返回空:

java
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​ 注解。

  1. GameTestHolder@GameTestHolder​ 注解注册类型(类、接口、枚举或记录)内的所有测试方法。@GameTestHolder​ 包含一个方法,其值必须是模组的 ID,否则测试将不会在默认配置下运行。

    java
    @GameTestHolder(MODID)
    public class ExampleGameTests {
        // ...
    }
  2. RegisterGameTestsEventRegisterGameTestsEvent​ 也可以使用 #register​ 注册类或方法。事件监听器必须添加到模组事件总线。以这种方式注册的测试方法必须在每个使用 @GameTest​ 注解的方法上提供模组 ID 到 GameTest#templateNamespace​。

    java
    public void registerTests(RegisterGameTestsEvent event) {
        event.register(ExampleGameTests.class);
    }
    
    @GameTest(templateNamespace = MODID)
    public static void exampleTest3(GameTestHelper helper) {
        // 执行设置
    }

结构模板

游戏测试在由结构或模板加载的场景中执行。所有模板定义场景的尺寸和将加载的初始数据(方块和实体)。模板必须存储为 data/<namespace>/structures​ 中的 .nbt​ 文件。

提示: 可以使用结构块创建和保存结构模板。

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

  1. 如果指定了模板的命名空间。
  2. 如果类名应附加到模板名称之前。
  3. 如果指定了模板的名称。

运行游戏测试

可以使用 /test​ 命令运行游戏测试。/test​ 命令高度可配置,但只有少数几个对运行测试很重要:

子命令描述
run运行指定的测试:run <test_name>​。
runall运行所有可用的测试。
runclosest运行距离玩家 15 个方块内的最近测试。
runthese运行玩家 200 个方块内的测试。
runfailed运行上次运行中失败的所有测试。

注意: 子命令跟在 /test​ 命令后面:/test <subcommand>​。


构建脚本配置

游戏测试在构建脚本(build.gradle​ 文件)中提供了额外的配置设置,以运行并集成到不同的设置中。

  1. 启用其他命名空间 如果构建脚本按推荐设置,则仅启用当前模组 ID 下的游戏测试。要启用其他命名空间以加载游戏测试,运行配置必须将属性 neoforge.enabledGameTestNamespaces​ 设置为以逗号分隔的每个命名空间的字符串。如果属性为空或未设置,则将加载所有命名空间。

    groovy
    property 'neoforge.enabledGameTestNamespaces', 'modid1,modid2,modid3'

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

  2. 游戏测试服务器运行配置 游戏测试服务器是一种特殊的配置,用于运行构建服务器。构建服务器返回所需失败游戏测试的退出代码。所有失败的测试(无论是必需的还是可选的)都会被记录。可以使用 gradlew runGameTestServer​ 运行此服务器。

  3. 在其他运行配置中启用游戏测试 默认情况下,只有客户端、服务器和 gameTestServer​ 运行配置启用了游戏测试。如果其他运行配置应运行游戏测试,则必须将 neoforge.enableGameTest​ 属性设置为 true​。

    groovy
    property 'neoforge.enableGameTest', 'true'

贡献者

The avatar of contributor named as 小飘 小飘

文件历史

布局切换

调整 VitePress 的布局样式,以适配不同的阅读习惯和屏幕环境。

全部展开
使侧边栏和内容区域占据整个屏幕的全部宽度。
全部展开,但侧边栏宽度可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
全部展开,且侧边栏和内容区域宽度均可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
原始宽度
原始的 VitePress 默认布局宽度

页面最大宽度

调整 VitePress 布局中页面的宽度,以适配不同的阅读习惯和屏幕环境。

调整页面最大宽度
一个可调整的滑块,用于选择和自定义页面最大宽度。

内容最大宽度

调整 VitePress 布局中内容区域的宽度,以适配不同的阅读习惯和屏幕环境。

调整内容最大宽度
一个可调整的滑块,用于选择和自定义内容最大宽度。

聚光灯

支持在正文中高亮当前鼠标悬停的行和元素,以优化阅读和专注困难的用户的阅读体验。

ON开启
开启聚光灯。
OFF关闭
关闭聚光灯。

聚光灯样式

调整聚光灯的样式。

置于底部
在当前鼠标悬停的元素下方添加一个纯色背景以突出显示当前鼠标悬停的位置。
置于侧边
在当前鼠标悬停的元素旁边添加一条固定的纯色线以突出显示当前鼠标悬停的位置。