Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

单元测试

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

--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 suites)是 C++ 的实现文件,其中必须包含 TEST_FORCE_LINK() 宏。大多数测试套件都直接位于 tests/ 目录下。

所有测试文件都必须以 test_ 作为前缀,这是 Godot 构建系统用来在整个引擎中识别和检测测试文件所依赖的命名规范。

这是一个最小的工作测试套件,其中只包含单个测试用例:

#include "tests/test_macros.h"

TEST_FORCE_LINK(test_string)

namespace TestString {

TEST_CASE("[String] Hello World!") {
    String hello = "Hello World!";
    CHECK(hello == "Hello World!");
}

} // namespace TestString

备注

你可以使用 tests/ 目录下的 create_test.py 脚本来快速生成新的测试。这个脚本会自动在合适的位置创建一个带有必要样板代码的新测试文件。如果想看具体的使用说明,可以带上 -h 标志来运行这个脚本。

tests/test_macros.h 头文件封装了在 Godot 中编写 C++ 单元测试所需的一切。它包括 doctest 断言和类似如上所述的 CHECK 等日志记录宏,当然还有用于编写测试用例本身的定义。

参见

用于当前实现的宏及其别名的 tests/test_macros.h 源代码。

测试用例是通过类似函数的 TEST_CASE 宏来创建的。每个测试用例都必须在括号里写上一句简短的描述,并且可以(可选地)加上一些自定义标签,方便在运行时筛选测试,比如 [String][Stress] 等等。

测试用例编写在专用的命名空间中。这不是必需的,但可以防止在编写其他静态帮助函数以适应重复测试程序(例如为每个测试填充通用测试数据或编写参数化测试)时发生命名冲突。

Godot 支持为每个 C++ 模块编写测试。有关如何编写模块测试的说明,请参阅 编写自定义单元测试

子用例

当你有多个测试用例需要相同的初始设置,只有细微差别时,子用例(subcases)就会非常有用。下面是一个例子:

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
    }
}

每个 SUBCASE (子用例)都会导致 TEST_CASE (测试用例)从头开始重新执行。子用例可以嵌套到任意深度,但建议嵌套层级不要超过一层。

断言

Godot 测试中常用的断言列表,按严格程度排序。

断言

描述

REQUIRE

检查条件是否成立。如果条件不成立则会立即让整个测试失败。

REQUIRE_FALSE

检查条件是否不成立。如果条件成立则会立即让整个测试失败。

CHECK

检查条件是否成立。会将测试表示为失败,但允许运行其他断言。

CHECK_FALSE

检查条件是否不成立。会将测试表示为失败,但允许运行其他断言。

WARN

检查条件是否成立。任何情况下都不会让测试失败,但是不成立时会记录一条警告。

WARN_FALSE

检查条件是否不成立。任何情况下都不会让测试失败,但是成立时会记录一条警告。

以上断言都有对应的 *_MESSAGE 宏,能够在原有行为的基础上输出可选的消息。

对于能够自我说明的断言请尽量使用 CHECK,如果你认为相对复杂的断言需要更好的解释再使用 CHECK_MESSAGE

日志

测试输出由 doctest 本身处理,完全不依赖于 Godot 打印或日志功能,因此建议使用专用宏,允许以 doctest 编写的格式记录测试输出。

Macro

描述

MESSAGE

打印一条信息.

FAIL_CHECK

将测试标记为失败,但继续执行。可以包含在条件中以进行复杂的检查。

FAIL

立即使测试失败。可以包含在条件语句中以进行复杂的检查。

可以在运行时选择不同的报告器(reporters)。例如,可以通过以下方式将输出重定向到一个 XML 文件中:

./bin/<godot_binary> --test --source-file="*test_validate*" --success --reporters=xml --out=doctest.txt

测试故障路径

有时,测试预期结果并不总是可行的。根据 Godot 开发理念,引擎不应崩溃,并且应在发生非致命错误时正常恢复,因此,重要的是检查这些故障路径确实可以安全执行而不会导致引擎崩溃。

意外行为可以像其他任何行为一样进行测试。这样做的唯一问题是,错误打印会不必要地污染测试输出,因为测试输出中会出现来自引擎本身的错误(即使最终结果是成功的)。

为了缓解这个问题,请在测试用例中直接使用 ERR_PRINT_OFFERR_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.");
}

测试用例名称中的特殊标签

可以将这些标签添加到测试用例的名称中,以此来修改或扩展测试环境。

标签

描述

[SceneTree]

对于那些需要依赖带有消息队列(MessageQueue)的场景树才能正常运行的测试用例来说,这是必需的。此外,它还会启用一个模拟的渲染服务器(mock rendering server)和 主题数据库

[Editor]

类似于 [SceneTree] (场景树),但额外提供了一些编辑器相关的基础设施,比如 编辑器设置

[Audio]

使用一个模拟(Mock)音频驱动来初始化( 音频服务器 )。

[Navigation2D]

创建默认的 2D 导航服务器,并使其可供测试使用。

[Navigation3D]

创建默认的 3D 导航服务器,并使其可供测试使用。

你可以将它们结合起来使用,以组合多个测试环境扩展。

测试信号

可以使用以下宏来测试信号:

描述

SIGNAL_WATCH(object, "signal_name")

开始监视指定对象上的特定信号。

SIGNAL_UNWATCH(object, "signal_name")

停止监视指定对象上的特定信号。

SIGNAL_CHECK("signal_name", Vector<Vector<Variant>>)

检查所有已触发信号的参数。外层的向量(vector)包含了每一个被触发的信号,而内层的向量则包含了该信号对应的参数列表。信号的先后顺序是有意义的。

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 命令行选项后提供工具名称来运行这些工具。例如,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 编写集成测试的步骤如下:

  1. 选择你想要编写的测试脚本类型,然后在 modules/gdscript/tests/scripts 目录下对应的子目录中新建 GDScript 脚本。

  2. 编写 GDScript 代码。测试脚本必须包含一个名叫 test() 的函数,不带任何参数。这个函数会由测试运行器调用。测试不应存在任何依赖项,除非依赖项本身也是测试的一部分。全局类(使用 class_name)是在运行器启动前注册的,所以需要时应该能够正常工作。

    这是一个测试脚本的示例:

    func test():
        if true # Missing colon here.
            print("true")
    
  3. 切换到 Godot 源码仓库的根目录。

    cd godot
    
  4. 生成 *.out 文件,更新期望输出的结果:

    bin/<godot_binary> --gdscript-generate-tests modules/gdscript/tests/scripts
    

你可以加上 --print-filenames 选项,查看生成测试输出时对应的文件名。如果你在开发新功能时造成了硬崩溃,就可以使用使用这个选项快速定位到造成崩溃的测试文件,从而开始调试。

  1. 运行 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”。