三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Godot单元测试实战:GdUnit3插件与TDD开发流程详解

Godot单元测试实战:GdUnit3插件与TDD开发流程详解

1. 项目概述:为什么Godot开发者需要GdUnit3?

如果你在用Godot做项目,尤其是稍微有点规模或者需要长期维护的,肯定遇到过这样的场景:改了一个看似无关紧要的脚本,结果游戏里某个核心功能莫名其妙就崩了,你花了大半天时间才定位到问题。或者,团队协作时,你不敢轻易重构别人的代码,生怕引入新的Bug。这种“牵一发而动全身”的焦虑,在游戏开发里太常见了。游戏逻辑复杂,节点树层层嵌套,脚本间耦合度高,没有一套可靠的自动化测试机制,开发过程就像在走钢丝。

这就是我今天想聊的GdUnit3。它不是Godot官方内置的功能,而是一个由社区驱动的、功能强大的第三方单元测试插件。它的核心价值在于“嵌入式”和“驱动开发”。所谓“嵌入式”,是指它无缝集成在Godot编辑器中,你不需要切换到命令行或者另一个IDE去运行测试,所有操作——创建测试用例、运行测试、查看结果——都在你熟悉的Godot编辑器界面内完成,开发体验非常流畅。而“测试驱动开发”(TDD)是一种先写测试,再写实现代码的开发方法论,它能强制你思考接口设计,确保代码的可测试性和模块化。GdUnit3正是实践TDD理念的绝佳工具。

简单来说,GdUnit3能帮你把“猜测”变成“验证”。你不再需要手动点击游戏、触发各种条件来验证逻辑是否正确;而是写一段测试代码,描述“给定某个输入,我期望得到某个输出”,然后让GdUnit3自动、快速地验证你的代码是否符合预期。这对于提升代码质量、减少回归Bug、增强重构信心至关重要。无论你是独立开发者还是团队一员,花时间搭建GdUnit3测试环境,长期来看绝对是笔划算的投资。

2. GdUnit3核心特性与安装配置详解

2.1 GdUnit3的核心优势解析

在深入安装之前,我们先搞清楚GdUnit3到底强在哪里。市面上也有一些Godot的测试方案,比如用GDScript写简单的assert语句,或者用命令行工具。但GdUnit3提供了更完整的解决方案。

首先,真正的编辑器集成。安装后,Godot编辑器界面会增加一个“GdUnit3”的Dock面板。你可以在这里看到所有测试场景、运行状态(通过/失败)、甚至测试覆盖率报告。你可以一键运行单个测试、一个测试套件、或者全部测试。这种集成度极大地降低了测试的门槛,让你更愿意频繁地运行测试。

其次,丰富的断言库和Mock支持。单元测试的核心是“断言”(Assertion),即判断实际结果是否等于期望结果。GdUnit3提供了非常全面的断言方法,比如assert_that(value).is_equal_to(expected)assert_that(node).is_not_null()assert_that(array).contains(item)等等,语义清晰,报错信息友好。更重要的是,它支持Mock(模拟)Stub(桩)。这是测试复杂依赖关系的关键。比如,你的角色脚本依赖一个“库存管理器”单例来检查物品。在测试时,你并不想启动整个游戏并初始化真实的库存系统,这时就可以用GdUnit3 Mock一个假的库存管理器,并预设它的返回值(例如,让它总是返回“有钥匙”),从而将测试焦点完全隔离在你的角色脚本逻辑上。

第三,测试夹具(Fixture)生命周期管理。GdUnit3会自动管理测试的前置(before())和后置(after())操作。比如,在测试一个需要物理环境的角色移动时,你可以在before()中生成测试用的场景和角色节点,在after()queue_free()清理它们。这保证了每个测试用例都在一个干净、独立的环境中运行,避免了测试间的相互污染。

第四,参数化测试。同一个测试逻辑,你可能想用多组不同的输入数据来验证。GdUnit3支持参数化测试,你只需要定义一个数据提供方法,GdUnit3就会自动为每组数据运行一次测试,并分别报告结果,避免了写大量重复的测试代码。

2.2 一步步安装与配置GdUnit3

安装GdUnit3非常简单,主要通过Godot的AssetLib(资源库)完成。这里以Godot 3.5.x稳定版为例,Godot 4.x的用户需要注意,GdUnit4正在积极开发中,但本文聚焦于成熟的GdUnit3。

  1. 打开AssetLib:在Godot编辑器顶部菜单栏,点击“AssetLib”选项卡。
  2. 搜索插件:在搜索框输入“GdUnit3”。通常第一个结果就是。点击进入详情页。
  3. 下载与安装:点击“Download”按钮,等待下载完成。然后点击“Install”。Godot会弹出一个安装确认窗口,通常保持默认设置,直接点击“Install”即可。安装完成后,会提示你重启编辑器。
  4. 启用插件:重启Godot后,进入“项目” -> “项目设置”。在左侧列表中找到“插件”选项卡。你应该能看到“GdUnit3”插件,将其状态从“Inactive”切换到“Active”。Godot可能会再次要求重启,照做即可。
  5. 验证安装:重启后,你应该能在编辑器界面的右侧Dock区域,看到一个名为“GdUnit3”的新面板。如果没看到,可以去顶部菜单栏的“视图” -> “面板”中勾选“GdUnit3”。

注意:有时从AssetLib安装的插件版本可能不是最新的。如果你需要最新特性或Bug修复,可以去GdUnit3的GitHub仓库(搜索“gdunit3”即可找到),下载最新的gdunit3文件夹,直接放到你项目的addons/目录下,然后在项目设置中启用。这种方式能确保你用到最新的功能。

安装完成后,我建议先为你的项目创建一个专门的测试目录结构,这有助于保持项目整洁。常见的做法是在项目根目录下创建一个tests/文件夹。你可以在GdUnit3面板的设置中,将默认的测试根目录指向这里。

3. 测试驱动开发(TDD)循环与GdUnit3实践

3.1 理解TDD的红-绿-重构循环

测试驱动开发不是一个测试工具,而是一种设计方法论。它的核心工作流是一个严格的“红-绿-重构”短循环:

  1. 红(Red):先写一个必定会失败的测试。这个测试描述了你期望的某个功能或行为。此时,对应的实现代码还不存在,或者不完整,所以运行测试会失败(在GdUnit3面板中显示为红色)。这一步的关键是定义清晰的需求接口。你在为“代码应该做什么”立法。
  2. 绿(Green):用最快、最简单、甚至有点“脏”的方式,编写刚好能让这个测试通过的实现代码。目的不是写出完美代码,而是让测试从红变绿(GdUnit3面板显示绿色)。这一步验证了你的实现思路在逻辑上是可行的。
  3. 重构(Refactor):在测试保护伞下(所有测试都是绿的),放心地改进你的实现代码。优化结构、消除重复、提高可读性,而不改变其外部行为。因为你有测试,所以可以确信重构没有破坏任何已有功能。

这个循环通常以分钟为单位进行,它强迫你在写代码前就思考如何调用它(设计接口),并自然地催生出高内聚、低耦合的模块化代码。

3.2 实战:用TDD开发一个简单的生命值组件

让我们用一个游戏开发中最常见的例子——生命值(Health)组件,来完整走一遍TDD循环。假设我们想要一个Health节点,它有最大生命值和当前生命值,可以被治疗和伤害,并且在生命值降到0时发出“死亡”信号。

第一步:创建测试场景(红)

  1. tests/目录下右键,选择“新建场景”。实际上,GdUnit3测试本身也是Godot场景。更便捷的方式是:在GdUnit3面板中,通常有“Create Test Scene”按钮,或者你可以自己创建一个空场景,其根节点是一个GdUnitTest(安装插件后会出现这个节点类型)。
  2. 将场景保存为test_health.gd(GdUnit3通常能识别test_开头的脚本)。
  3. 打开关联的脚本,开始编写第一个测试。我们先测试初始化。
# test_health.gd extends GdUnitTest func test_health_initializes_with_max_health(): # 1. 准备(Arrange):创建测试对象 var health = Health.new() health.max_health = 100 # 2. 执行(Act):调用初始化方法(假设我们有一个init方法,或者直接在_ready设置) add_child(health) # 将节点加入场景树以触发_ready yield(get_tree(), "idle_frame") # 等待一帧,确保_ready执行完毕 # 3. 断言(Assert):验证当前生命值等于最大生命值 assert_that(health.current_health).is_equal_to(100) # 此时Health类还不存在,这个测试必定失败(红)

运行这个测试(在GdUnit3面板中找到该测试并点击运行),你会看到失败,因为Health类未定义。这正是我们想要的“红”状态。

第二步:实现最小功能(绿)

  1. 现在创建真正的Health脚本。在游戏脚本目录(如scripts/components/)下创建health.gd
  2. 编写最简单的实现,只为了让测试通过。
# health.gd extends Node class_name Health var max_health := 100 var current_health := 100 func _ready(): current_health = max_health
  1. 回到测试场景,再次运行测试。现在测试应该通过了(绿)!我们完成了第一个循环。

第三步:添加更多测试与重构

现在,我们为“受到伤害”功能添加测试。

# 在 test_health.gd 中添加新测试 func test_take_damage_reduces_current_health(): var health = Health.new() health.max_health = 100 add_child(health) yield(get_tree(), "idle_frame") # 执行:受到30点伤害 health.take_damage(30) # 断言:当前生命值应为70 assert_that(health.current_health).is_equal_to(70)

运行测试,再次变“红”,因为take_damage方法不存在。我们去实现它:

# 在 health.gd 中添加 func take_damage(amount: int): current_health -= amount

运行测试,变“绿”。继续添加边界测试,比如伤害值超过当前生命值,生命值不应为负数:

func test_take_damage_does_not_go_below_zero(): var health = Health.new() health.max_health = 100 add_child(health) yield(get_tree(), "idle_frame") health.take_damage(150) # 过量伤害 assert_that(health.current_health).is_equal_to(0) # 应该停在0,而不是-50

运行测试,可能失败(因为我们还没处理边界)。我们去修复实现:

func take_damage(amount: int): current_health = max(0, current_health - amount) # 使用max函数确保不低于0

测试通过。现在,我们进入“重构”阶段。看看health.gd,我们可能发现_ready中的初始化和max_health的默认值设置有点分散。我们可以重构,使用export变量让最大生命值可在编辑器中设置,并在_ready中统一初始化:

# health.gd 重构后 extends Node class_name Health export var max_health := 100 var current_health := 0 func _ready(): current_health = max_health func take_damage(amount: int): current_health = max(0, current_health - amount)

重要提示:重构后,必须立即重新运行所有相关的测试(最好是整个测试套件)。如果测试依然全绿,说明重构成功,没有引入回归错误。这就是TDD给你的“安全网”。

通过这个简单例子,你应该能感受到TDD的节奏:一个小测试、一个小实现、一点点重构。循环很快,反馈即时,代码在测试的驱动下自然生长,并且始终处于可验证的状态。

4. GdUnit3高级功能:Mock、参数化与异步测试

4.1 使用Mock和Stub隔离依赖

单元测试的核心原则是“隔离”。我们只想测试当前单元(如一个函数、一个类)的逻辑,而不想启动整个游戏世界、加载资源、或依赖不稳定的外部服务(如网络)。Mock(模拟)和Stub(桩)就是用来模拟这些依赖对象的工具。

场景:假设我们有一个Player脚本,它依赖一个Inventory单例来检查玩家是否拥有钥匙,以决定是否能打开门。

# player.gd func can_open_door(door_id: String) -> bool: var inventory = Inventory.get_instance() # 获取全局库存单例 return inventory.has_item("key", door_id)

直接测试can_open_door会很麻烦,因为需要设置真实的Inventory单例并添加物品。使用GdUnit3的Mock功能,我们可以轻松解决:

# test_player.gd extends GdUnitTest func test_player_can_open_door_when_has_key(): # 1. 创建Player测试对象 var player = Player.new() add_child(player) # 2. 创建并注册一个Mock的Inventory实例 var mock_inventory = mock(Inventory, "Inventory") # 创建一个Mock对象 # 预设Mock对象的行为:当调用 has_item("key", "door_1") 时,返回 true do_return(true).on(mock_inventory).has_item("key", "door_1") # 将Mock对象注入到Player可能访问到的地方。这里需要Player能使用这个Mock实例。 # 假设我们通过一个可设置的属性或方法来注入(这是更好的设计!)。 player.inventory = mock_inventory # 3. 执行测试 var result = player.can_open_door("door_1") # 4. 断言 assert_that(result).is_true() # 5. (可选)验证Mock的交互:确认has_item方法确实以预期的参数被调用了一次 verify(mock_inventory, 1).has_item("key", "door_1")

在这个测试中,Inventory的真实逻辑完全被绕过了。我们只关心Player的逻辑:当库存说有钥匙时,它是否返回true。Mock让我们能精确控制依赖的返回值,并验证单元与依赖的交互是否符合预期。为了便于注入Mock,通常需要将代码设计为依赖注入(Dependency Injection)模式,而不是硬编码单例调用,这本身也是TDD推动产生更好设计的一个体现。

4.2 参数化测试:用多组数据验证同一逻辑

对于像伤害计算、经验值公式、状态判定这类有明确输入输出关系的逻辑,写多个几乎相同的测试函数很繁琐。参数化测试允许你定义一个数据源,GdUnit3会为每组数据自动运行测试。

# test_damage_calculator.gd extends GdUnitTest # 使用 @dataProvider 注解指向提供数据的方法 func test_damage_calculation(data: TestData) -> void: # data参数包含了每组测试数据 var base_attack: int = data.get(0) var defense: int = data.get(1) var expected_damage: int = data.get(2) var calculator = DamageCalculator.new() var actual_damage = calculator.calculate(base_attack, defense) assert_that(actual_damage).is_equal_to(expected_damage) # 数据提供方法,必须返回一个数组的数组 func data_provider_for_damage_calculation() -> Array: return [ # [基础攻击力, 防御力, 期望伤害值] [100, 20, 80], # 简单减法 [50, 60, 10], # 伤害最低为10 [30, 100, 10], # 防御远高于攻击,伤害保底10 [0, 10, 0], # 攻击为0,伤害为0 ]

在GdUnit3面板中运行这个测试,你会看到它被扩展为四个子测试,每个对应一组数据,并独立报告成功或失败。这极大地提高了测试的覆盖率和可维护性。

4.3 测试异步与信号:处理Godot的时序逻辑

游戏开发中充满了异步操作:等待定时器、等待动画播放完毕、等待HTTP请求返回、等待信号发出。GdUnit3提供了强大的工具来测试这些异步逻辑。

最常用的方法是结合yield和GdUnit3的assert_signal或超时控制。

测试信号发射

# 测试Health组件在生命值降为0时发出`died`信号 func test_health_emits_died_signal_when_reaches_zero(): var health = Health.new() health.max_health = 50 add_child(health) yield(get_tree(), "idle_frame") # 监听信号 var signal_watcher = watch_signals(health) # 执行操作 health.take_damage(50) # 断言信号被发出 assert_that(signal_watcher).is_emitted("died") # 还可以进一步断言信号携带的参数 # assert_that(signal_watcher).is_emitted_with("died", [expected_arg1, arg2])

测试带延时的异步流程

# 测试一个技能释放后有2秒的冷却时间 func test_skill_cooldown(): var skill = Skill.new() skill.cooldown_time = 2.0 add_child(skill) assert_that(skill.is_ready()).is_true() # 初始应可用 skill.cast() assert_that(skill.is_ready()).is_false() # 释放后应进入冷却 # 使用 yield 等待冷却时间结束,并设置一个稍长的超时时间 yield(await_time(2.5), "completed") # await_time 是GdUnit3提供的工具 # 冷却结束后,技能应恢复可用 assert_that(skill.is_ready()).is_true()

实操心得:测试异步逻辑时,超时时间的设置很关键。设得太短,可能因为帧率波动导致测试在就绪前失败,形成“假阴性”(Flaky Test)。设得太长,又会拖慢测试套件的整体运行速度。我的经验是,在理论等待时间上加一个缓冲(比如20%-50%),并在CI环境中注意监控测试的稳定性。对于网络请求等不确定操作,一定要用Mock替换掉真实调用。

5. 构建可维护的测试套件与持续集成

5.1 测试代码的组织与结构

当测试越来越多时,良好的组织至关重要,否则测试本身就会变成维护的噩梦。

  1. 目录结构映射:让测试目录tests/的结构与你的游戏源代码目录scripts/scenes/保持一致。例如:

    project/ ├── scripts/ │ ├── player/ │ │ ├── player.gd │ │ └── state_machine.gd │ └── ui/ │ └── hud.gd └── tests/ ├── player/ │ ├── test_player.gd │ └── test_state_machine.gd └── ui/ └── test_hud.gd

    这样,寻找某个功能的测试非常直观。

  2. 测试场景与脚本分离:对于复杂的测试,可能需要布置一个包含多个节点的小场景。建议将场景文件(.tscn)和测试脚本(.gd)分开,但放在同一目录。测试脚本通过preloadload来实例化测试场景。这比把所有节点生成逻辑都写在脚本里更清晰,也便于在编辑器中可视化调试测试场景。

  3. 使用测试夹具(Setup/Teardown):如果多个测试用例需要相同的初始化步骤(比如创建一个复杂的测试环境),不要在每个测试函数里重复写。使用GdUnitTest提供的before()after()方法。

    extends GdUnitTest var _test_world: Node2D var _test_player: KinematicBody2D func before(): # 每个测试用例运行前都会执行 _test_world = preload("res://tests/fixtures/test_world.tscn").instance() get_tree().root.add_child(_test_world) _test_player = _test_world.find_node("Player") # ... 其他通用设置 func after(): # 每个测试用例运行后都会执行 _test_world.queue_free() # ... 其他清理工作 func test_player_movement(): # 这里可以直接使用 _test_player,环境已准备好 _test_player.move_and_slide(Vector2.RIGHT * 100) assert_that(_test_player.position.x).is_greater_than(0)

    这保证了测试的独立性和可重复性。

5.2 将测试集成到自动化流程(CI/CD)

个人开发时手动点运行测试还行,但对于团队项目,必须将测试自动化。每次代码提交(Git Push)后,自动运行完整的测试套件,确保新代码没有破坏现有功能,这就是持续集成(CI)的核心环节之一。

主流方案:使用GitHub Actions、GitLab CI或Jenkins等CI工具。核心思路是:

  1. 在CI服务器上安装Godot(无头模式,即--headless)。
  2. 检出你的项目代码。
  3. 运行Godot并执行GdUnit3测试。

一个简单的GitHub Actions工作流示例(.github/workflows/run-tests.yml):

name: Run GdUnit3 Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Download Godot 3.5 run: | wget -q https://downloads.tuxfamily.org/godotengine/3.5.3/Godot_v3.5.3-stable_linux_headless.64.zip unzip -q Godot_v3.5.3-stable_linux_headless.64.zip chmod +x Godot_v3.5.3-stable_linux_headless.64 sudo mv Godot_v3.5.3-stable_linux_headless.64 /usr/local/bin/godot - name: Run Tests with GdUnit3 run: | godot --path . -s addons/gdunit3/bin/GdUnit3CmdTool.gd --quit-on-finish # 解释: # --path . : 指定项目路径 # -s ... : 执行GdUnit3的命令行工具脚本 # --quit-on-finish : 测试完成后退出

这个工作流会在每次推送或拉取请求时自动运行。如果任何测试失败,CI会标记此次运行为失败,阻止有问题的代码合并到主分支。你还可以配置将测试报告(如JUnit格式)上传,以便在CI界面中直观查看哪些测试失败了。

注意事项:CI环境通常是“无头”的,没有图形界面和声音。这意味着所有依赖OS.window_has_focus()Input事件(非Input单例的is_action_pressed)或音频播放的测试可能会失败。你需要确保测试不依赖这些环境特定的因素,或者使用Mock来模拟它们。这也是为什么单元测试要尽量隔离的原因之一。

6. 常见问题、调试技巧与性能优化

6.1 典型问题排查指南

即使有了完善的测试,编写和调试测试本身也会遇到问题。下面是一些常见坑点及解决方法。

问题现象可能原因排查步骤与解决方案
测试在编辑器中通过,但在CI(命令行)中失败。1.路径问题:CI环境的工作目录或资源路径不同。
2.依赖缺失:测试依赖的某些项目设置、自动加载单例在CI环境中未正确初始化。
3.时序/竞态条件:无头模式下帧率或物理步进可能与编辑器不同。
1. 检查所有文件路径是否使用res://开头,避免使用相对路径。
2. 在测试的before()方法中,显式地初始化或Mock所有依赖。确保CI运行的Godot项目配置与本地一致。
3. 避免在测试中依赖yield(get_tree().create_timer(0.1), "timeout")这种硬编码短延时,改用信号或状态检查。使用GdUnit3的await_time并给予充足缓冲。
Mock对象的行为不符合预期。1.Mock未正确注入:被测试代码仍然使用了真实的对象实例,而非Mock。
2.预设行为参数不匹配:Mock预设时参数与代码实际调用时的参数不完全一致(如字符串大小写、额外参数)。
1. 确认你的代码设计支持依赖注入(如通过构造函数、setter方法或可覆盖的工厂方法传入依赖)。这是可测试性设计的关键。
2. 使用verify(...).has_been_called()来检查Mock方法是否被调用。使用GdUnit3的do_return(...).on(mock).method_name()时,确保参数完全匹配,或使用参数匹配器如any()
测试运行速度非常慢。1.测试场景过于复杂:每个测试都实例化了一个包含大量节点和资源的大型场景。
2.频繁的文件I/O或网络请求:测试中进行了真实的资源加载或HTTP调用。
3.没有合理使用before()/after():重复的初始化开销。
1. 遵循“单元测试”原则,测试对象尽可能轻量。用Mock代替复杂的子节点。如果必须测试场景集成,考虑将其归为“集成测试”,并减少运行频率。
2. 对所有外部依赖(文件系统、网络、数据库)进行Mock。
3. 将昂贵的初始化操作移到before()中,但注意这会使测试间产生共享状态风险,确保after()清理干净。对于完全独立的测试,有时在每个测试中创建简单对象反而更快。
测试结果不稳定(有时过,有时不过)。“Flaky Test”(不稳定测试),通常由异步操作、随机数、或未清理的全局状态引起。1.消除随机性:测试中如果用到随机数,使用固定的种子,或在测试中Mock随机数生成器。
2.彻底隔离:确保每个测试在after()中清理了自己创建的所有节点和修改的全局状态。
3.强化异步等待:增加yieldawait的超时容忍度,或改用轮询状态直到条件满足。

6.2 调试失败的测试

当测试失败时,GdUnit3面板会显示红色的错误信息。但有时信息不够详细,你需要深入调试。

  1. 使用Godot内置调试器:你可以在测试脚本中设置断点,就像调试普通游戏脚本一样。在编辑器中运行单个测试(而不是运行全部),当执行到断点时,调试器会暂停,你可以查看所有变量的状态。这是最强大的调试手段。
  2. 打印调试信息:在测试代码中临时插入print()语句,输出关键变量的值。这在CI环境中查看日志时也很有用。
  3. 检查测试场景状态:如果测试涉及场景,确保在运行测试前,场景在编辑器中是打开且活动的。有时节点路径错误或信号连接失败,在场景视图中能更直观地发现。
  4. 缩小范围:如果一大段测试代码失败,尝试注释掉一部分,逐步缩小问题范围,定位到具体出错的哪一行断言或操作。

6.3 测试性能优化实践

一个庞大的项目可能有成百上千个测试。如果每个测试都跑得很慢,开发体验会大打折扣,CI也会变得冗长。

  1. 测试分类与选择性运行

    • 单元测试:快速、隔离、不依赖外部资源。这些应该是你测试套件的主体,运行速度极快(毫秒级)。在本地开发时,应频繁运行相关的单元测试。
    • 集成测试:测试多个模块的交互,可能需要加载场景和资源。运行较慢。可以配置GdUnit3的测试套件标签,在本地主要运行单元测试,在CI上才运行全部(包括集成)测试。
    • 在GdUnit3面板中,你可以通过过滤器只运行某个目录或匹配某个名称模式的测试。
  2. 优化测试初始化

    • 对于轻量级对象,直接在测试函数内创建。避免在before()中创建昂贵对象却被所有测试共享(除非确实需要)。
    • 使用preload而不是load来加载测试所需的资源(如场景、纹理),preload在解析脚本时完成,避免了运行时的I/O开销。
  3. Mock一切昂贵操作:数据库查询、网络请求、复杂的文件解析、大型资源加载(如纹理、音频)在测试中必须被Mock掉。你的目标是测试业务逻辑,不是第三方库或Godot引擎本身的加载功能。

  4. 定期审查测试代码:和业务代码一样,测试代码也需要重构。删除过时的测试,合并重复的测试逻辑,将复杂的测试拆分成多个小而专的测试。保持测试套件的健康度。

将GdUnit3融入你的Godot开发工作流,初期会感觉多了一层“束缚”,需要多写不少代码。但一旦习惯,你会发现自己对代码的信心空前增强,重构时不再畏手畏脚,Bug在引入的瞬间就被捕获。它不仅仅是一个测试工具,更是一个推动你写出更清晰、更模块化、更健壮代码的设计助手。从今天开始,尝试为你下一个要修改的Godot脚本,先写一个测试吧。

← 返回列表