godot-testing-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Testing Patterns

测试模式

GUT framework, assertion patterns, mocking, and async testing define automated validation.
GUT框架、断言模式、模拟和异步测试构成了自动化验证体系。

Available Scripts

可用脚本

integration_test_base.gd

integration_test_base.gd

Base class for GUT integration tests with auto-cleanup and scene helpers.
GUT集成测试的基类,包含自动清理和场景辅助功能。

headless_test_runner.gd

headless_test_runner.gd

Expert headless test runner for CI/CD with JUnit XML output and exit code handling.
适用于CI/CD的专业无头测试运行器,支持JUnit XML输出和退出码处理。

NEVER Do in Testing

测试中的禁忌

  • NEVER test implementation details
    assert_eq(player._internal_state, 5)
    ? Private variables = brittle tests. Test PUBLIC behavior, not internals.
  • NEVER share state between tests — Test 1 modifies global variable, test 2 assumes clean state? Flaky tests. Use
    before_each()
    for fresh setup.
  • NEVER use sleep() for timing
    await get_tree().create_timer(1.0).timeout
    in tests? Slow + unreliable. Use GUT's
    wait_seconds()
    OR manual frame stepping.
  • NEVER skip cleanup in after_each() — Test spawns 100 nodes, doesn't free? Memory leak + slow test suite. ALWAYS free nodes in
    after_each()
    .
  • NEVER test randomness without seeding
    randi()
    in test = non-deterministic failure. Use
    seed(12345)
    for repeatable tests.
  • NEVER forget to watch signals
    assert_signal_emitted(obj, "died")
    without
    watch_signals
    ? Fails silently. MUST call
    watch_signals(obj)
    first.

  • 绝对不要测试实现细节 — 比如
    assert_eq(player._internal_state, 5)
    ?私有变量会导致测试脆弱。应测试公开行为,而非内部实现。
  • 绝对不要在测试间共享状态 — 测试1修改了全局变量,测试2却假设状态是干净的?这会导致测试不稳定。使用
    before_each()
    来实现每次测试的全新初始化。
  • 绝对不要用sleep()来处理时序 — 在测试中使用
    await get_tree().create_timer(1.0).timeout
    ?既缓慢又不可靠。使用GUT的
    wait_seconds()
    或手动帧步进。
  • 绝对不要在after_each()中跳过清理步骤 — 测试生成了100个节点却不释放?会造成内存泄漏并拖慢测试套件。务必在
    after_each()
    中释放节点。
  • 绝对不要在不设置种子的情况下测试随机性 — 测试中使用
    randi()
    会导致非确定性失败。使用
    seed(12345)
    来保证测试可重复。
  • 绝对不要忘记监听信号 — 未调用
    watch_signals
    就使用
    assert_signal_emitted(obj, "died")
    ?会静默失败。必须先调用
    watch_signals(obj)

Installation

安装步骤

  1. Download from AssetLib: "GUT - Godot Unit Test"
  2. Enable in Project Settings → Plugins
  3. Create
    res://test/
    directory
  1. 从AssetLib下载“GUT - Godot Unit Test”
  2. 在项目设置→插件中启用
  3. 创建
    res://test/
    目录

Basic Test

基础测试

gdscript
undefined
gdscript
undefined

test/test_player.gd

test/test_player.gd

extends GutTest
var player: CharacterBody2D
func before_each() -> void: player = preload("res://entities/player/player.tscn").instantiate() add_child(player)
func after_each() -> void: player.queue_free()
func test_initial_health() -> void: assert_eq(player.health, 100, "Player should start with 100 health")
func test_take_damage() -> void: player.take_damage(25) assert_eq(player.health, 75, "Health should be 75 after 25 damage")
func test_cannot_have_negative_health() -> void: player.take_damage(200) assert_gte(player.health, 0, "Health should not go below 0")
undefined
extends GutTest
var player: CharacterBody2D
func before_each() -> void: player = preload("res://entities/player/player.tscn").instantiate() add_child(player)
func after_each() -> void: player.queue_free()
func test_initial_health() -> void: assert_eq(player.health, 100, "Player should start with 100 health")
func test_take_damage() -> void: player.take_damage(25) assert_eq(player.health, 75, "Health should be 75 after 25 damage")
func test_cannot_have_negative_health() -> void: player.take_damage(200) assert_gte(player.health, 0, "Health should not go below 0")
undefined

Running Tests

运行测试

gdscript
undefined
gdscript
undefined

Via GUT panel in editor

Via GUT panel in editor

Or command line:

Or command line:

godot --headless -s addons/gut/gut_cmdln.gd

godot --headless -s addons/gut/gut_cmdln.gd

undefined
undefined

Assertion Patterns

断言模式

gdscript
undefined
gdscript
undefined

Equality

Equality

assert_eq(actual, expected, "message") assert_ne(actual, not_expected, "message")
assert_eq(actual, expected, "message") assert_ne(actual, not_expected, "message")

Comparison

Comparison

assert_gt(value, min_value, "should be greater") assert_lt(value, max_value, "should be less") assert_gte(value, min_value, "should be >= min") assert_lte(value, max_value, "should be <= max")
assert_gt(value, min_value, "should be greater") assert_lt(value, max_value, "should be less") assert_gte(value, min_value, "should be >= min") assert_lte(value, max_value, "should be <= max")

Boolean

Boolean

assert_true(condition, "should be true") assert_false(condition, "should be false")
assert_true(condition, "should be true") assert_false(condition, "should be false")

Null

Null

assert_not_null(object, "should exist") assert_null(object, "should be null")
assert_not_null(object, "should exist") assert_null(object, "should be null")

Arrays

Arrays

assert_has(array, element, "should contain element") assert_does_not_have(array, element, "should not contain")
assert_has(array, element, "should contain element") assert_does_not_have(array, element, "should not contain")

Signals

Signals

watch_signals(object) assert_signal_emitted(object, "signal_name")
undefined
watch_signals(object) assert_signal_emitted(object, "signal_name")
undefined

Testing Signals

信号测试

gdscript
func test_death_signal() -> void:
    watch_signals(player)
    
    player.take_damage(100)
    
    assert_signal_emitted(player, "died")
    assert_signal_emitted_with_parameters(player, "died", [player])
gdscript
func test_death_signal() -> void:
    watch_signals(player)
    
    player.take_damage(100)
    
    assert_signal_emitted(player, "died")
    assert_signal_emitted_with_parameters(player, "died", [player])

Testing Async

异步测试

gdscript
func test_delayed_action() -> void:
    player.start_ability()
    
    # Wait for timer
    await wait_seconds(1.0)
    
    assert_true(player.ability_active, "Ability should be active after delay")
gdscript
func test_delayed_action() -> void:
    player.start_ability()
    
    # Wait for timer
    await wait_seconds(1.0)
    
    assert_true(player.ability_active, "Ability should be active after delay")

Mock/Stub Patterns

模拟/存根模式

gdscript
undefined
gdscript
undefined

Double (mock) pattern

Double (mock) pattern

func test_with_mock() -> void: var mock_enemy := double(Enemy).new() stub(mock_enemy, "get_damage").to_return(50)
player.collide_with(mock_enemy)

assert_eq(player.health, 50, "Should take mocked damage")
undefined
func test_with_mock() -> void: var mock_enemy := double(Enemy).new() stub(mock_enemy, "get_damage").to_return(50)
player.collide_with(mock_enemy)

assert_eq(player.health, 50, "Should take mocked damage")
undefined

Integration Testing

集成测试

gdscript
undefined
gdscript
undefined

test/test_combat_system.gd

test/test_combat_system.gd

extends GutTest
func test_player_kills_enemy() -> void: var level := preload("res://levels/test_arena.tscn").instantiate() add_child(level)
var player := level.get_node("Player")
var enemy := level.get_node("Enemy")

# Simulate combat
for i in range(5):
    player.attack(enemy)
    await wait_frames(1)

assert_true(enemy.is_dead, "Enemy should be dead")
assert_gt(player.score, 0, "Player should have score")

level.queue_free()
undefined
extends GutTest
func test_player_kills_enemy() -> void: var level := preload("res://levels/test_arena.tscn").instantiate() add_child(level)
var player := level.get_node("Player")
var enemy := level.get_node("Enemy")

# Simulate combat
for i in range(5):
    player.attack(enemy)
    await wait_frames(1)

assert_true(enemy.is_dead, "Enemy should be dead")
assert_gt(player.score, 0, "Player should have score")

level.queue_free()
undefined

Manual Testing Checklist

手动测试清单

markdown
undefined
markdown
undefined

Gameplay

Gameplay

  • Player can move in all directions
  • Jump height feels right
  • Enemies respond to player
  • Damage numbers are correct
  • Player can move in all directions
  • Jump height feels right
  • Enemies respond to player
  • Damage numbers are correct

UI

UI

  • All buttons work
  • Text is readable
  • Responsive on different resolutions
  • All buttons work
  • Text is readable
  • Responsive on different resolutions

Audio

Audio

  • Music plays
  • SFX trigger correctly
  • Volume levels balanced
  • Music plays
  • SFX trigger correctly
  • Volume levels balanced

Performance

Performance

  • Maintains 60 FPS
  • No stuttering
  • Memory stable
undefined
  • Maintains 60 FPS
  • No stuttering
  • Memory stable
undefined

Validation Helpers

验证辅助工具

gdscript
undefined
gdscript
undefined

validation.gd (for runtime checks)

validation.gd (for runtime checks)

class_name Validation
static func assert_valid_health(health: int) -> void: assert(health >= 0 and health <= 100, "Invalid health: %d" % health)
static func assert_valid_position(pos: Vector2, bounds: Rect2) -> void: assert(bounds.has_point(pos), "Position out of bounds: %s" % pos)
undefined
class_name Validation
static func assert_valid_health(health: int) -> void: assert(health >= 0 and health <= 100, "Invalid health: %d" % health)
static func assert_valid_position(pos: Vector2, bounds: Rect2) -> void: assert(bounds.has_point(pos), "Position out of bounds: %s" % pos)
undefined

Test Organization

测试组织结构

test/
├── unit/
│   ├── test_player.gd
│   ├── test_enemy.gd
│   └── test_inventory.gd
├── integration/
│   ├── test_combat.gd
│   └── test_save_load.gd
└── fixtures/
    ├── test_level.tscn
    └── mock_data.tres
test/
├── unit/
│   ├── test_player.gd
│   ├── test_enemy.gd
│   └── test_inventory.gd
├── integration/
│   ├── test_combat.gd
│   └── test_save_load.gd
└── fixtures/
    ├── test_level.tscn
    └── mock_data.tres

Best Practices

最佳实践

1. Test Edge Cases

1. 测试边缘情况

gdscript
func test_edge_cases() -> void:
    player.take_damage(0)  # Zero damage
    assert_eq(player.health, 100)
    
    player.take_damage(-10)  # Negative (heal?)
    assert_eq(player.health, 100)  # Should not change
gdscript
func test_edge_cases() -> void:
    player.take_damage(0)  # Zero damage
    assert_eq(player.health, 100)
    
    player.take_damage(-10)  # Negative (heal?)
    assert_eq(player.health, 100)  # Should not change

2. Isolate Tests

2. 隔离测试

gdscript
undefined
gdscript
undefined

Each test should be independent

Each test should be independent

func before_each() -> void: # Fresh setup for each test player = create_fresh_player()
undefined
func before_each() -> void: # Fresh setup for each test player = create_fresh_player()
undefined

3. Test Critical Paths First

3. 优先测试关键路径

Priority:
1. Core gameplay (movement, combat)
2. Save/load system
3. Level transitions
4. UI interactions
Priority:
1. Core gameplay (movement, combat)
2. Save/load system
3. Level transitions
4. UI interactions

Reference

参考资料

Related

相关内容

  • Master Skill: godot-master
  • Master Skill: godot-master