单元测试
单元测试
如果你曾经修过一个 Bug,结果修完之后又引入了新的 Bug,那你就明白为什么需要"单元测试"了。单元测试就是给引擎代码写的"体检报告"——它自动检查引擎的各个功能是否正常工作,确保每次修改不会意外破坏已有功能。
Godot 引擎允许你直接在 C++ 中编写单元测试。引擎集成了 doctest 单元测试框架,让你可以在引擎源码旁边编写测试用例。
平台和编译目标支持
C++ 单元测试可以在 Linux、macOS 和 Windows 上运行。
但有一个限制:测试只能在启用了 tools 的编辑器版本上运行,也就是说,导出模板(export templates)目前无法被测试。
运行测试
编译测试
默认情况下,测试代码不会随引擎一起编译。你需要在编译时显式启用 tests 选项:
scons tests=yes推荐
如果你打算为引擎开发做贡献,推荐使用 dev_mode=yes 选项。它会自动启用测试编译,并且将编译警告视为错误。持续集成(CI)系统如果检测到任何编译警告就会失败,所以在提交 PR 前应该修复所有警告。
执行测试
编译完成后,使用 --test 命令行选项运行测试:
./bin/<godot_binary> --test要查看所有支持的测试选项:
./bin/<godot_binary> --test --help--test 之后的所有参数都会被传递给 doctest 框架。
过滤测试
默认会运行所有测试。你可以使用各种过滤选项只运行感兴趣的测试:
| 过滤选项 | 简写 | 示例 |
|---|---|---|
--test-suite | -ts | -ts="*[GDScript]*" |
--test-case | -tc | -tc="*[String]*" |
--source-file | -sf | -sf="*test_color*" |
例如,只运行 String 相关的单元测试:
./bin/<godot_binary> --test --test-case="*[String]*"用 --success(-s)选项可以查看成功断言的详细输出:
./bin/<godot_binary> --test --source-file="*test_color*" --success用 -exclude 选项跳过特定测试。例如跳过耗时的压力测试:
./bin/<godot_binary> --test --test-case-exclude="*[Stress]*"编写测试
基本结构
测试套件(test suite)是 C++ 头文件,必须被包含在 tests/test_main.cpp 主入口中。大多数测试套件直接放在 tests/ 目录下。
所有测试文件都以 test_ 为前缀——这是 Godot 构建系统用来检测测试的命名约定。
下面是一个最小的测试套件示例:
#pragma once
#include "tests/test_macros.h"
namespace TestString {
TEST_CASE("[String] Hello World!") {
String hello = "Hello World!";
CHECK(hello == "Hello World!");
}
} // namespace TestString快速生成测试
你可以使用 tests/ 目录下的 create_test.py 脚本快速创建新测试文件,它会自动生成所需的模板代码。加上 -i(侵入模式)标志,还会自动将新头文件包含到 tests/test_main.cpp 中。运行 create_test.py -h 查看用法。
tests/test_macros.h 头文件封装了编写 C++ 单元测试所需的一切,包括 doctest 的断言和日志宏(如 CHECK),以及定义测试用例的宏。
测试用例
测试用例使用 TEST_CASE 宏创建。每个用例必须有一个简短描述(写在括号中),可以包含自定义标签(如 [String]、[Stress])用于运行时过滤。
测试用例写在独立的命名空间中。这不是强制的,但可以防止命名冲突,也方便编写静态辅助函数来简化重复的测试逻辑。
子测试(Subcases)
当你有多个测试用例共享相同的初始化设置,只有细微变化时,子测试会很有用:
TEST_CASE("[SceneTree][Node] Testing node operations with a very simple scene tree") {
// ... 公共的初始化(比如创建一个包含几个节点的场景树)
SUBCASE("Move node to specific index") {
// ... 移动节点的设置和检查
}
SUBCASE("Remove node at specific index") {
// ... 移除节点的设置和检查
}
}每个 SUBCASE 会导致 TEST_CASE 从头开始重新执行。子测试可以嵌套到任意深度,但建议嵌套不超过一层。
断言(Assertions)
Godot 测试中常用的断言,按严重程度排序:
| 断言 | 描述 |
|---|---|
REQUIRE | 条件必须为真,否则立即终止整个测试 |
REQUIRE_FALSE | 条件必须为假,否则立即终止整个测试 |
CHECK | 条件必须为真,否则标记失败但继续运行其他断言 |
CHECK_FALSE | 条件必须为假,否则标记失败但继续运行其他断言 |
WARN | 条件必须为真,不会失败但会记录警告 |
WARN_FALSE | 条件必须为假,不会失败但会记录警告 |
以上断言都有对应的 *_MESSAGE 版本,允许附加解释性消息。
建议:简单的断言用 CHECK,复杂的断言用 CHECK_MESSAGE 加上详细解释。
日志输出
测试输出由 doctest 自身处理,不依赖 Godot 的打印或日志功能。推荐使用专用的日志宏:
| 宏 | 描述 |
|---|---|
MESSAGE | 打印一条消息 |
FAIL_CHECK | 标记测试失败但继续执行,可包裹在条件中用于复杂检查 |
FAIL | 立即失败测试,可包裹在条件中用于复杂检查 |
可以将输出重定向到 XML 文件:
./bin/<godot_binary> --test --source-file="*test_validate*" --success --reporters=xml --out=doctest.txt测试失败路径
有时候无法测试"期望结果",而是需要验证引擎在遇到非致命错误时不会崩溃。Godot 的开发理念是引擎不应该崩溃,而应该优雅地恢复。
你可以像测试其他内容一样测试意外行为。唯一的烦恼是引擎自身的错误输出会污染测试日志。解决方法是用 ERR_PRINT_OFF 和 ERR_PRINT_ON 宏临时禁用错误输出:
TEST_CASE("[Color] Constructor methods") {
ERR_PRINT_OFF;
Color html_invalid = Color::html("invalid");
ERR_PRINT_ON; // 别忘了重新启用!
CHECK_MESSAGE(html_invalid.is_equal_approx(Color()),
"Invalid HTML notation should result in a Color with the default values.");
}特殊标签
在测试用例名称中加入以下标签可以扩展测试环境:
| 标签 | 描述 |
|---|---|
[SceneTree] | 提供带有 MessageQueue 的场景树,同时启用模拟渲染服务器和 ThemeDB |
[Editor] | 类似 [SceneTree],但额外提供编辑器相关基础设施(如 EditorSettings) |
[Audio] | 使用模拟音频驱动初始化 AudioServer |
[Navigation2D] | 创建默认的 2D 导航服务器 |
[Navigation3D] | 创建默认的 3D 导航服务器 |
你可以组合使用多个标签来同时启用多种环境扩展。
测试信号
Godot 提供了一组宏来测试信号:
| 宏 | 描述 |
|---|---|
SIGNAL_WATCH(object, "signal_name") | 开始监听指定对象上的信号 |
SIGNAL_UNWATCH(object, "signal_name") | 停止监听指定信号 |
SIGNAL_CHECK("signal_name", Vector<Vector<Variant>>) | 检查所有触发信号的参数 |
SIGNAL_CHECK_FALSE("signal_name") | 检查指定信号是否未被触发 |
SIGNAL_DISCARD("signal_name") | 丢弃指定信号的所有记录 |
示例:
SUBCASE("[Timer] Timer process timeout signal must be emitted") {
SIGNAL_WATCH(test_timer, SNAME("timeout"));
test_timer->start(0.1);
SceneTree::get_singleton()->process(0.2);
Array signal_args;
signal_args.push_back(Array());
SIGNAL_CHECK(SNAME("timeout"), signal_args);
SIGNAL_UNWATCH(test_timer, SNAME("timeout"));
}测试工具(Test Tools)
测试工具是高级方法,允许你运行任意程序来辅助手动测试和调试引擎内部。通过在 --test 后提供工具名称来运行:
./bin/<godot_binary> --test gdscript-tokenizer test.gd
./bin/<godot_binary> --test gdscript-parser test.gd
./bin/<godot_binary> --test gdscript-compiler test.gd如果检测到任何测试工具,则其余的单元测试会被跳过。
测试工具可以在引擎的任何位置注册,注册机制类似于 doctest 的动态初始化技术,通常在对应的 register_types.cpp 中注册:
#ifdef TESTS_ENABLED
void test_tokenizer() {
TestGDScript::test(TestGDScript::TestType::TEST_TOKENIZER);
}
void test_parser() {
TestGDScript::test(TestGDScript::TestType::TEST_PARSER);
}
void test_compiler() {
TestGDScript::test(TestGDScript::TestType::TEST_COMPILER);
}
REGISTER_TEST_COMMAND("gdscript-tokenizer", &test_tokenizer);
REGISTER_TEST_COMMAND("gdscript-parser", &test_parser);
REGISTER_TEST_COMMAND("gdscript-compiler", &test_compiler);
#endifGDScript 集成测试
Godot 使用 doctest 来防止 GDScript 在开发过程中出现回归。可以编写多种类型的测试脚本:
- 错误测试:验证预期的错误输出
- 警告测试:验证预期的警告输出
- 功能测试:验证特定功能是否正常工作
编写 GDScript 集成测试的步骤:
- 在
modules/gdscript/tests/scripts对应的子目录下创建新的.gd测试文件 - 编写 GDScript 代码。测试脚本必须有一个
test()函数(不接受参数),这个函数会被测试运行器调用 - 切换到 Godot 源码根目录,生成预期输出:
bin/<godot_binary> --gdscript-generate-tests modules/gdscript/tests/scripts - 运行 GDScript 测试:
./bin/<godot_binary> --test --test-suite="*GDScript*"
注意
--gdscript-generate-tests生成的*.out文件只应包含新测试的预期结果,无关文件的变更应还原- GDScript 测试运行器用于测试 GDScript 的实现,不是用于测试用户脚本
- 如果测试文件不需要
test()函数,可以将文件命名为*.notest.gd来禁用运行时测试段
最后同步日期
本文档最后同步于 2026-04-15,基于 Godot 官方文档 (latest) 翻译整理。
