单元测试
Godot 引擎允许直接用 C++ 编写单元测试。引擎集成了 doctest 单元测试框架,该框架允许在生产代码旁边编写测试套件和测试用例,但由于 Godot 中的测试通过不同的 main
入口点,因此测试在专用的 tests/
目录中,该目录位于引擎源代码的根目录中。
平台及目标支持
C++ 单元测试可以在 Linux、macOS 和 Windows 操作系统上运行。
测试只能在启用编辑器 tools
的情况下运行,这意味着目前无法测试导出模板。
运行测试
在测试被实际运行之前,必须在启用 tests
构建选项(并使用你通常使用的任何其他构建选项)的情况下编译引擎,因为默认情况下测试不会作为引擎的一部分进行编译:
scons tests=yes
构建完成后,使用 --test
命令行选项运行测试:
./bin/<godot_binary> --test
可以使用各种 doctest 特定的命令行选项来配置测试运行。要检索受支持选项的完整列表,请使用 --help
选项运行 --test
命令:
./bin/<godot_binary> --test --help
--test
命令后的任何其他选项和参数都被视为 doctest 的参数。
备注
如果你使用 dev_mode=yes
SCons 选项,则测试会被自动编译。如果你计划为引擎开发做出贡献,建议使用 dev_mode=yes
,因为它会自动将编译警告视为错误。如果检测到任何编译警告,持续集成系统将失败,因此你应该努力在打开拉取请求之前修复所有警告。
筛选测试
默认情况下,如果你在 --test
命令后不提供任何额外参数,则所有测试都会运行。但是,如果你正在编写新测试,或者希望出于调试目的查看来自这些测试的成功断言输出,则可以使用 doctest 提供的各种过滤选项运行感兴趣的测试。
支持通配符语法 *
来匹配测试套件、测试用例和源文件名中的任意数量的字符:
过滤选项 |
Shorthand |
Examples |
|
|
|
|
|
|
|
|
|
例如,要仅运行 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]*"
编写测试
测试套件代表 C++ 头文件,必须将其作为主测试入口点的一部分包含在 tests/test_main.cpp
中。大多数测试套件都直接位于 tests/
目录下。
所有头文件都以 test_
为前缀,这是一个命名约定,Godot 构建系统将依赖于此来检测整个引擎的测试。
这是一个最小的工作测试套件,其中只包含单个测试用例:
#ifndef TEST_STRING_H
#define TEST_STRING_H
#include "tests/test_macros.h"
namespace TestString {
TEST_CASE("[String] Hello World!") {
String hello = "Hello World!";
CHECK(hello == "Hello World!");
}
} // namespace TestString
#endif // TEST_STRING_H
备注
You can quickly generate new tests using the create_test.py
script found in the tests/
directory.
This script automatically creates a new test file with the required boilerplate code in the appropriate location.
It's also able to automatically include the new header in tests/test_main.cpp
using invasive mode (-i
flag).
To view usage instructions, run the script with the -h
flag.
tests/test_macros.h
头文件封装了在 Godot 中编写 C++ 单元测试所需的一切。它包括 doctest 断言和类似如上所述的 CHECK
等日志记录宏,当然还有用于编写测试用例本身的定义。
参见
用于当前实现的宏及其别名的 tests/test_macros.h 源代码。
Test cases are created using TEST_CASE
function-like macro. Each test case
must have a brief description written in parentheses, optionally including
custom tags which allow to filter the tests at runtime, such as [String]
,
[Stress]
etc.
测试用例编写在专用的命名空间中。这不是必需的,但可以防止在编写其他静态帮助函数以适应重复测试程序(例如为每个测试填充通用测试数据或编写参数化测试)时发生命名冲突。
Godot 支持为每个 C++ 模块编写测试。有关如何编写模块测试的说明,请参阅 编写自定义单元测试。
Subcases
In situations where you have a common setup for several test cases with only slight variations, subcases can be very helpful. Here's an example:
TEST_CASE("[SceneTree][Node] Testing node operations with a very simple scene tree") {
// ... common setup (e.g. creating a scene tree with a few nodes)
SUBCASE("Move node to specific index") {
// ... setup and checks for moving a node
}
SUBCASE("Remove node at specific index") {
// ... setup and checks for removing a node
}
}
Each SUBCASE
causes the TEST_CASE
to be executed from the beginning.
Subcases can be nested to an arbitrary depth, but it is advised to limit nesting to no more than one level deep.
断言
Godot 测试中常用的断言列表,按严格程度排序。
断言 |
描述 |
|
检查条件是否成立。如果条件不成立则会立即让整个测试失败。 |
|
检查条件是否不成立。如果条件成立则会立即让整个测试失败。 |
|
检查条件是否成立。会将测试表示为失败,但允许运行其他断言。 |
|
检查条件是否不成立。会将测试表示为失败,但允许运行其他断言。 |
|
检查条件是否成立。任何情况下都不会让测试失败,但是不成立时会记录一条警告。 |
|
检查条件是否不成立。任何情况下都不会让测试失败,但是成立时会记录一条警告。 |
以上断言都有对应的 *_MESSAGE
宏,能够在原有行为的基础上输出可选的消息。
对于能够自我说明的断言请尽量使用 CHECK
,如果你认为相对复杂的断言需要更好的解释再使用 CHECK_MESSAGE
。
参见
日志
测试输出由 doctest 本身处理,完全不依赖于 Godot 打印或日志功能,因此建议使用专用宏,允许以 doctest 编写的格式记录测试输出。
Macro |
描述 |
|
打印一条信息. |
|
将测试标记为失败,但继续执行。可以包含在条件中以进行复杂的检查。 |
|
立即使测试失败。可以包含在条件语句中以进行复杂的检查。 |
Different reporters can be chosen at runtime. For instance, here's how the output can be redirected to an XML file:
./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; // Don't forget to re-enable!
CHECK_MESSAGE(html_invalid.is_equal_approx(Color()),
"Invalid HTML notation should result in a Color with the default values.");
}
Testing signals
The following macros can be use to test signals:
Macro |
描述 |
---|---|
|
Starts watching the specified signal on the given object. |
|
Stops watching the specified signal on the given object. |
|
Checks the arguments of all fired signals. The outer vector contains each fired signal, while the inner vector contains the list of arguments for that signal. The order of signals is significant. |
|
Checks if the specified signal was not fired. |
|
Discards all records of the specified signal. |
Below is an example demonstrating the use of these macros:
//...
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
命令行选项后提供工具名称来运行这些工具。例如,GDScript 模块实现并注册了几个工具来帮助调试标记器、解析器和编译器:
./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
源(每个模块或核心)中注册。
下面是 GDScript 如何在 modules/gdscript/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);
#endif
自定义命令行解析可以在 OS 的 get_cmdline_args 方法的帮助下由测试工具本身执行。
GDScript 的集成测试
Godot 使用 doctest 来防止在开发时引入回归问题。可以编写以下几种测试脚本:
对预期错误的测试;
对警告的测试;
对功能的测试。
因此,为 GDScript 编写集成测试的步骤如下:
选择你想要编写的测试脚本类型,然后在
modules/gdscript/tests/scripts
目录下对应的子目录中新建 GDScript 脚本。编写 GDScript 代码。测试脚本必须包含一个名叫
test()
的函数,不带任何参数。这个函数会由测试运行器调用。测试不应存在任何依赖项,除非依赖项本身也是测试的一部分。全局类(使用class_name
)是在运行器启动前注册的,所以需要时应该能够正常工作。这是一个测试脚本的示例:
func test(): if true # Missing colon here. print("true")
切换到 Godot 源码仓库的根目录。
cd godot
生成
*.out
文件,更新期望输出的结果:bin/<godot_binary> --gdscript-generate-tests modules/gdscript/tests/scripts
你可以加上 --print-filenames
选项,查看生成测试输出时对应的文件名。如果你在开发新功能时造成了硬崩溃,就可以使用使用这个选项快速定位到造成崩溃的测试文件,从而开始调试。
运行 GDScript 测试:
./bin/<godot_binary> --test --test-suite="*GDScript*"
此处也能够使用 --print-filenames
选项(见上文)。
如果没有打印错误并且一切顺利,那么就完成了!
警告
在提交拉取请求之前,请确保输出确实具有预期值。如果 --gdscript-generate-tests
生成的 *.out
文件与新添加的测试无关,则应恢复这些文件并仅为新测试提交 *.out
文件。
备注
GDScript 测试运行器用于测试 GDScript 实现,而不是用于测试用户脚本或测试引擎使用脚本。我们建议为已解决的 GitHub 上与 GDScript 相关的问题编写新测试,或为当前正在工作的功能编写测试。
备注
如果你的测试用例要求脚本文件中不存在 test()
函数,你可以通过命名脚本文件使其与模式 *.notest.gd
匹配来禁用测试的运行时部分。例如,“test_empty_file.notest.gd”。