G

GDScript单元测试框架

作者:鹿Sir开发工具v1

Godot 4 GDScript单元测试框架使用指南,覆盖测试套件结构、断言API、参数化测试、异步与物理帧测试、资源清理和命令行测试执行。适用于编写新测试、运行调试gdUnit4测试、理解断言和Mock API、修复失败测试等场景。

下载量
255
点赞
63
价格
免费

技能文档

---
name: gdscript-unit-test
title: GDScript单元测试框架
category: 开发工具
description: Godot 4 GDScript单元测试框架使用指南,覆盖测试套件结构、断言API、参数化测试、异步与物理帧测试、资源清理和命令行测试执行。适用于编写新测试、运行调试gdUnit4测试、理解断言和Mock API、修复失败测试等场景。
---

# gdUnit4 测试框架

gdUnit4 是 Godot 4 的原生 GDScript 单元测试框架(`addons/gdUnit4/`)。测试就是普通 `.gd` 脚本:`extends GdUnitTestSuite`,每个 `test_` 开头的函数是一个用例。

## 使用前提

- 项目已安装 gdUnit4 插件(存在 `addons/gdUnit4/plugin.cfg`,且 `project.godot` 的 `[editor_plugins] enabled` 中包含它)
- 测试文件放在项目内(惯例:`test/` 目录,按被测模块分子目录),类名与文件名对应

## 测试套件骨架

```gdscript
extends GdUnitTestSuite
## 套件顶部注释:说明被测对象与测试覆盖面(可选但推荐)

## 每个 test_ 开头的函数都是一个独立用例
func test_something() -> void:
	var obj := SomeClass.new()
	obj.do_thing()
	assert_bool(obj.done).is_true()
```

规则:
- 用例函数必须以 `test_` 前缀命名,否则不会被发现
- 私有辅助函数用 `_` 前缀(如 `_build_scene()`),gdUnit4 不会把它们当用例
- 跳过用例:通过特殊参数 `do_skip`(默认值可为表达式)声明,`skip_reason` 说明原因
- 超时控制:通过 `timeout` 参数声明(毫秒),超时即判失败

## 断言 API

通用入口 `assert_that(value)` 按类型分派,推荐直接用类型化断言。**断言失败会立即中止当前用例**(fail fast),失败后同一用例后续代码不再执行。

| 断言 | 用途 | 常用链式匹配器 |
|------|------|----------------|
| `assert_bool(v)` | 布尔 | `is_true()` / `is_false()` |
| `assert_int(v)` | 整数 | `is_equal(n)` / `is_greater(n)` / `is_less(n)` / `is_between(a, b)` / `is_not_zero()` |
| `assert_float(v)` | 浮点 | `is_equal_approx(f, 容差)` / `is_less(f)` / `is_greater(f)` / `is_between(a, b)` / `is_zero()` / `is_not_zero()` |
| `assert_str(v)` | 字符串 | `is_equal(s)` / `contains(s)` / `starts_with(s)` / `ends_with(s)` / `is_empty()` / `is_not_empty()` |
| `assert_object(v)` | 对象 | `is_null()` / `is_not_null()` / `is_same(obj)` / `is_instanceof(Type)` |
| `assert_array(v)` | 数组 | `contains(x)` / `not_contains(x)` / `has_size(n)` / `is_empty()` / `is_not_empty()` |
| `assert_dict(v)` | 字典 | `contains_keys(k1, k2, ...)` / `has_size(n)` |
| `assert_vector(v)` | Vector2/3/4 | `is_equal_approx(v, 容差)` / `is_less(v)` / `is_greater(v)` |
| `assert_signal(obj)` | 信号(异步,需 `await`) | `wait_until(ms)` 设置超时后接 `is_emitted(信号或名字, ...期望参数)` / `is_not_emitted(...)` / `is_signal_exists(s)` |
| `assert_failure(可调用)` | 预期内部断言失败 | `assert_failure(func() -> void: assert_bool(false).is_true()).is_failed()`,还可 `.is_success()`  |

失败信息定制(可选):
```gdscript
assert_that(actual).override_failure_message("自定义失败信息").is_equal(expected)
assert_that(actual).append_failure_message("补充说明").is_equal(expected)
```

显式失败:`fail("原因")`。

## 生命周期钩子

| 钩子 | 时机 |
|------|------|
| `before()` / `after()` | 整个套件运行前后各一次 |
| `before_test()` / `after_test()` | 每个用例执行前/后 |

`before_test()`/`after_test()` 适合成对保存和恢复全局状态(如 `multiplayer.multiplayer_peer`、项目设置、单例状态)。

## 资源管理

- `auto_free(obj)`:注册对象在用例结束后自动释放。**创建的 Node/资源只要不被场景树管理,就应交给 `auto_free()`**,避免用例间泄漏
- 已加入场景树的节点(`get_tree().root.add_child(x)`):在用例末尾显式 `queue_free()`,或设计为测试结束后统一清理
- 内存泄漏会被 gdUnit4 检测并报告(orphan nodes 检查)

## 异步与物理帧测试

用例可以是协程(无需特殊标记):

```gdscript
func test_physics_steps() -> void:
	var body := CharacterBody3D.new()
	get_tree().root.add_child(body)
	for i in 10:
		await get_tree().physics_frame  # 跑 10 个物理帧
	assert_float(body.global_position.y).is_less(0.1)
	body.queue_free()
```

要点:
- `await get_tree().physics_frame`(物理帧)用于物理集成的时序断言
- `await get_tree().process_frame`(渲染帧)用于普通帧逻辑
- `await some_signal` 可等待自定义信号(配合超时断言)
- 场景交互(模拟按键/鼠标/触摸/手柄)用 `scene_runner("res://scene.tscn")`,链式调用 `simulate_key_pressed(KEY_A)`、`simulate_action_press("ui_up")`、`simulate_mouse_button_pressed(MOUSE_BUTTON_LEFT)`、`simulate_mouse_move(pos)` 等,`await` 等待完成
- 注意:headless 模式下 `InputEvent` 不生效,UI 交互测试必须用窗口模式运行

## 用例级配置参数(函数默认参数声明)

这些特殊参数写在用例(或 `before()`)签名上,由框架识别,不属于普通数据参数,可加 `_` 前缀避免未使用警告:

```gdscript
# 跳过用例(默认值可以是真表达式,如环境变量/平台判断)
func test_in_progress(do_skip := true, skip_reason := "尚未实现,见 TODO") -> void:
	pass

# 超时 5 秒,超时判失败并中止
func test_slow(timeout := 5000) -> void:
	await get_tree().create_timer(1.0).timeout
```

## 参数化测试

给用例函数添加名为 `test_parameters` 的默认参数(函数签名中它必须存在)即可按多组数据重复执行。三种数据来源:

```gdscript
# 1. 内联数组字面量:每组一行数据,按用例参数顺序展开
func test_add(a: int, b: int, expected: int, test_parameters := [[1, 2, 3], [3, 4, 7]]) -> void:
	assert_int(a + b).is_equal(expected)

# 2. 属性引用:套件成员变量
var _cases := [["a", 1], ["b", 2]]
func test_with_property(value: String, n: int, test_parameters := _cases) -> void:
	pass

# 3. 可调用表达式:数据源函数
func _case_provider() -> Array:
	return [[1], [2], [3]]
func test_with_callable(n: int, test_parameters := _case_provider()) -> void:
	pass
```

参数化用例失败时,报告会显示具体是哪组数据导致。

## Mocking / Spy(可选)

gdUnit4 内置 mock 与 spy 支持(无需第三方库):
- `mock(Type)` / `mock(instance)`:创建无实现的替身,用 `when(...)` 配置返回值
- `spy(instance)`:保留真实行为并可验证调用;`verify(instance, times(n)).method(...)` 检查调用次数

涉及网络或复杂依赖的组件优先用接口/依赖注入 + mock 解耦,纯逻辑可不用。

## 运行测试(命令行)

标准命令(`res://addons/gdUnit4/bin/GdUnitCmdTool.gd` 是 CLI 入口):

```bash
godot --headless --path . -s --remote-debug tcp://127.0.0.1:0 res://addons/gdUnit4/bin/GdUnitCmdTool.gd -a res://test -c --verbose
```

- `-a <目录|套件路径>`:指定要扫描的测试目录或单个套件(可多次添加);不传则扫描默认 `res://test`
- `-i <套件名|套件名:用例名>`:跳过指定套件/用例
- `-c`:失败不中止(默认 fail fast,遇第一个失败即停)
- `--verbose`(Godot 引擎参数,非 gdUnit4 选项):详细控制台输出
- `--remote-debug tcp://127.0.0.1:0`:防止脚本报错时进入交互式 debug 死循环(runtest 脚本惯例)
- **headless 模式默认被拒绝**,必须加 `--ignoreHeadlessMode`(UI 交互用例除外,见上)
- 退出码:`0` 全部通过,非 0 表示有失败/错误
- `-conf <cfg>`:用 GdUnit4 测试配置文件执行;`--selftest`:框架自检

更多参数与报告说明见 [reference.md](reference.md)。

## 调试失败用例

1. 先单独跑目标套件:`-a res://test/xxx/test_foo.gd`
2. 加 `-i` 排除干扰用例,`-c` 看全部失败
3. 断言失败信息会指出断言行号与期望/实际值;参数化用例会标出数据组
4. 物理相关失败先怀疑时序:帧数跑够没有、`await physics_frame` vs `process_frame` 用错
5. 用例间相互影响:检查 `before_test()/after_test()` 是否恢复全局状态、`auto_free()` 是否覆盖所有创建的资源

## 注意事项

- 不要在用例里直接 `free()` 场景树管理的节点(用 `queue_free()`),否则可能触发 gdUnit4 的内存泄漏报告
- 修改全局单例状态(如 item registry、项目设置)必须在 `after_test()` 恢复,否则后续用例串扰
- 断言匹配器如 `is_equal_approx` 在浮点/向量断言上是 `(期望值, 容差)` 双参数形式;信号断言必须 `await` 才生效
- 参考官方教程:https://mikeschulze.github.io/gdUnit4/

如何安装此技能?

访问技能市场,点击「安装」按钮,按提示将技能包放入 AI 编程助手的 skills 目录即可。

浏览技能市场

支持平台:Qoder · QoderWork · Claude · Codex 等 AI 编程助手

GDScript单元测试框架 - 免费 | 技能派