Testing Patterns GUT framework, assertion patterns, mocking, and async testing define automated validation. Available Scripts integration_test_base.gd Base class for GUT integration tests with auto-cleanup and scene helpers. headless_test_runner.gd Expert headless test runner for CI/CD with JUnit XML output and exit code handling. 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. Installation Download from AssetLib: "GUT - Godot Unit Test" Enable in Project Settings → Plugins Create res://test/ directory Basic Test
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" ) Running Tests
Via GUT panel in editor
Or command line:
godot --headless -s addons/gut/gut_cmdln.gd
Assertion Patterns
Equality
assert_eq ( actual , expected , "message" ) assert_ne ( actual , not_expected , "message" )
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" )
Boolean
assert_true ( condition , "should be true" ) assert_false ( condition , "should be false" )
Null
assert_not_null ( object , "should exist" ) assert_null ( object , "should be null" )
Arrays
assert_has ( array , element , "should contain element" ) assert_does_not_have ( array , element , "should not contain" )
Signals
watch_signals ( object ) assert_signal_emitted ( object , "signal_name" ) Testing Signals 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 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
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" ) Integration Testing
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 ( ) Manual Testing Checklist
Gameplay
[ ] Player can move in all directions
[ ] Jump height feels right
[ ] Enemies respond to player
[ ] Damage numbers are correct
UI
[ ] All buttons work
[ ] Text is readable
[ ] Responsive on different resolutions
Audio
[ ] Music plays
[ ] SFX trigger correctly
[ ] Volume levels balanced
Performance
[ ] Maintains 60 FPS
[ ] No stuttering
[ ] Memory stable Validation Helpers
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 ) 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 Best Practices 1. Test Edge Cases 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
- Isolate Tests
Each test should be independent
func before_each ( ) -> void :
Fresh setup for each test
player
create_fresh_player ( ) 3. Test Critical Paths First Priority: 1. Core gameplay (movement, combat) 2. Save/load system 3. Level transitions 4. UI interactions Reference GUT Documentation Related Master Skill: godot-master