
Godot Testing Patterns
- 289 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-testing-patterns for development tasks
About
godot-testing-patterns: A skill for development. This provides functionality for development workflows.
- godot-testing-patterns
Godot Testing Patterns by the numbers
- 289 all-time installs (skills.sh)
- +33 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,381 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-testing-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 289 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-testing-patterns for development tasks
Files
Testing Patterns
GUT framework, assertion patterns, mocking, and async testing define automated validation.
Available Scripts
basic_unit_test.gd
Minimal GdUnit4 test structure for verifying simple logic and arithmetic.
signal_emission_test.gd
Expert pattern for monitoring and verifying signal emissions in decoupled architectures.
mock_dependency_test.gd
Using Mocks and Doubles to isolate test subjects from external services or databases.
scene_integration_test.gd
Full scene lifecycle testing, verifying node interactions after instantiation.
performance_benchmark_runner.gd
High-precision execution time measurement using microsecond-scale timers.
memory_leak_detector.gd
Automated orphan node detection to catch memory leaks during long-running tests.
parameter_fuzz_tester.gd
Stress testing systems with randomized data ranges to catch edge-case crashes.
wait_for_frame_test.gd
Advanced async testing for logic that spans multiple frames or game ticks.
physics_collision_test.gd
Automated verification of physics layer interactions and collision resolution.
test_data_factory.gd
Centralized data generation patterns for clean, schemas-compliant test objects.
NEVER Do in Testing
- NEVER test implementation details —
assert_eq(player._internal_state, 5)? Private variables = brittle tests. Test PUBLIC behavior, not internals [20]. - NEVER share state between tests — Test 1 modifies global variable, test 2 assumes clean state? Flaky tests. Use
before_each()for fresh setup [21]. - NEVER use sleep() for timing —
await get_tree().create_timer(1.0).timeoutin tests? Slow + unreliable. Use GUT'swait_seconds()OR manual frame stepping [22]. - NEVER skip cleanup in after_each() — Test spawns 100 nodes, doesn't free? Memory leak + slow test suite. ALWAYS free nodes in
after_each()[23]. - NEVER test randomness without seeding —
randi()in test = non-deterministic failure. Useseed(12345)for repeatable tests [24]. - NEVER forget to watch signals —
assert_signal_emitted(obj, "died")withoutwatch_signals? Fails silently. MUST callwatch_signals(obj)first [25]. - NEVER perform tests without an explicit "Definition of Done" — Vague tests like
assert_true(true)provide zero value. Every test should verify a specific requirement. - NEVER rely on editor-only features for CI/CD tests — Headless environments lack Viewports. Ensure tests are
headless-compatible. - NEVER ignore the cost of "Integration Tests" — Testing a whole level is slow. Favor narrow Unit Tests for logic and small Scene Tests for interaction.
- NEVER hardcode file paths in tests — Use
Pathconstants or project-relative strings. If a resource directory moves, your suite shouldn't break. - NEVER test third-party plugins — Trust the library; test YOUR integration of it.
---
Installation
1. Download from AssetLib: "GUT - Godot Unit Test" 2. Enable in Project Settings → Plugins 3. 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.gdAssertion 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 stableValidation 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.tresBest 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 change2. 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 interactionsExpert Testing Patterns
1. State-Snapshot-Testing (Data Regression)
Verifying that complex game states (Inventory, Quests, Stats) remain consistent across versions.
- Implementation: Serialize the target node's state into a
Dictionary, then compare against a "Golden JSON" reference.
func test_inventory_snapshot() -> void:
var current_state = inventory.serialize() # Returns Dictionary
var reference = load_json("res://tests/goldens/inventory_v1.json")
assert_eq(current_state, reference, "State drifted from golden reference")- Expert Note: Use
JSON.stringify(dict, "\t")to save snapshots with human-readable indentation for easy git diffing.
2. Snapshot-Testing-UI (Visual Regression)
Verifying that UI layouts remain pixel-perfect across updates.
- Capture: Await
RenderingServer.frame_post_draw, then capture the viewport'sImage. - Comparison: Compare the raw byte data of the current image against a "golden" reference image stored in
res://tests/snapshots/.
2. Headless-CLI-CI (Automated Pipelines)
Running the test suite in non-GUI environments like GitHub Actions.
- Flags: Use the
--headlessengine flag to skip display server initialization. - Scripted Execution: Use
-sto run a master test orchestrator script. - Exit Codes: Explicitly call
SceneTree.quit(0)for success andquit(1)for failures. CI runners use these codes to determine pipeline pass/fail status.
3. Fuzz-Testing-Input (Stress Analysis)
Generating randomized inputs to catch edge-case crashes in input handlers.
- Implementation: Create randomized
InputEventobjects and inject them viaInput.parse_input_event().
func fuzz_inputs(iterations: int = 100):
for i in iterations:
var event = InputEventKey.new()
event.keycode = randi_range(KEY_A, KEY_Z)
event.pressed = true
Input.parse_input_event(event)- Benefit: Discovers unhandled null-refs or state-machine illegal transitions that manual testing misses.
4. CI/CD-Performance-Gate (Automated Audit)
Automatically failing builds that regress in performance metrics.
- Metric Tracking: Use
Performance.get_monitor(Performance.TIME_PROCESS)andPerformance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME. - Implementation:
func test_performance_bench() -> void:
# Run heavy gameplay simulation for 100 frames
await wait_frames(100)
var avg_draw_calls = _get_average_draw_calls()
assert_lt(avg_draw_calls, 500, "Draw calls exceeded budget!")- Expert Note: Combined with
--headless, this ensures that architectural "slop" (like material duplication) blocks the merge.
5. Mock-Network-Provider (Local Multiplayer Testing)
Testing RPC logic and replication without requiring a live server or second client.
- Implementation: Create a
MockPeerclass that inherits fromOfflineMultiplayerPeerorSceneMultiplayer. - Pattern: Override
send_packetto redirect messages directly to the localmultiplayer.on_packet_receivedsignal, simulating "Loopback" networking. - Benefit: Allows unit testing of
_rpcmethods andMultiplayerSynchronizerstate in isolation.
Reference
Related
- Master Skill: godot-master
# basic_unit_test.gd
# Minimal GdUnit4 test structure
extends GdUnitTestSuite
# EXPERT NOTE: In GdUnit4, use verify() and assert_that()
# for readable, robust unit testing of logic scripts.
func test_arithmetic_logic():
assert_that(1 + 1).is_equal(2)
assert_that("Godot").is_not_empty()
func test_player_damage():
var player = Player.new()
player.take_damage(20)
assert_that(player.health).is_equal(80)
player.free()
# skills/testing-patterns/code/headless_test_runner.gd
extends Node
## Testing Patterns Expert Pattern
## Implements Headless CI/CD Integration and Signal Assertions.
# This script would typically be part of a GUT (Godot Unit Test) suite.
# It demonstrates the logic for a headless runner and signal tracking.
func _run_ci_tests() -> void:
# 1. Headless CI/CD Runner
# Professional pattern: Run tests without a window and exit with code.
print("Starting Headless Test Suite...")
if DisplayServer.get_name() == "headless":
print("Running in CI environment.")
# Mocking GUT execution
var success = _execute_all_tests()
if success:
print("All tests passed.")
# OS.exit_code = 0
else:
printerr("Tests failed!")
# OS.exit_code = 1
func test_signal_decoupling() -> void:
# 2. Signal Coverage Tracking
# Expert logic: Verify that decoupled components communicate correctly.
var emitter = Node.new()
emitter.add_user_signal("data_processed", [{"name": "value", "type": TYPE_INT}])
# Monitor signal without a direct connection (GUT style)
var signal_received = false
var received_value = -1
emitter.data_processed.connect(func(v):
signal_received = true
received_value = v
)
# Trigger logic
emitter.emit_signal("data_processed", 42)
# Assertions
assert(signal_received, "Signal 'data_processed' was not emitted.")
assert(received_value == 42, "Signal payload was incorrect.")
## EXPERT NOTE:
## Use 'Visual Regression Testing': Compare 'Viewport.get_texture().get_image()'
## against a saved 'gold_standard.png' to detect UI layout shifts.
## For 'testing-patterns', use 'Mocking' for external APIs. Create a
## 'MockAuthService' that returns a hardcoded 'true' instead of
## hitting a real server during unit tests.
## NEVER test private variables directly; test the PUBLIC output
## or state change to ensure your tests don't break during refactoring.
## Use 'Benchmark Suites' to measure execution time of core algorithms:
## var start = Time.get_ticks_usec(); _run_algo(); var end = Time.get_ticks_usec().
# skills/testing-patterns/scripts/integration_test_base.gd
extends "res://addons/gut/test.gd"
## Integration Test Base Expert Pattern
## Base class for integration tests ensuring clean state and real-node interactions.
class_name IntegrationTestBase
# Scene references to be loaded for tests
var _level_instance: Node
var _player: Node
func before_all() -> void:
# Run once before all tests in script
pass
func after_all() -> void:
# Run once after all tests
pass
func before_each() -> void:
# Setup clean environment
_level_instance = Node2D.new()
add_child_autofree(_level_instance) # GUT helper to free on teardown
# Mock or Real Player
_player = CharacterBody2D.new()
_player.name = "Player"
_level_instance.add_child(_player)
func after_each() -> void:
# Cleanup happens via add_child_autofree, but custom logic goes here
pass
func test_player_initial_state() -> void:
# Tests
assert_not_null(_player, "Player should exist")
assert_eq(_player.get_parent(), _level_instance, "Player should be in level")
func simulate_frames(frames: int) -> void:
for i in range(frames):
await wait_frames(1)
## EXPERT USAGE:
## extends IntegrationTestBase
## func test_combat(): ...
# memory_leak_detector.gd
# Capturing object count regressions
extends Node
# EXPERT NOTE: A sudden spike in Performance.OBJECT_COUNT
# usually indicates nodes or resources that aren't being
# freed correctly (orphans).
func _process(_delta):
if Input.is_action_just_pressed("ui_focus_next"):
var orphans = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)
if orphans > 0:
print_rich("[color=red]ORPHAN NODES DETECTED: [/color]", orphans)
# mock_dependency_test.gd
# Using Mocks to isolate test subjects
extends GdUnitTestSuite
# EXPERT NOTE: Mocking allows you to test a class without
# requiring its real dependencies (e.g. Database, Network).
func test_inventory_save():
var mock_storage = mock(StorageProvider)
var inventory = Inventory.new()
inventory.storage = mock_storage
inventory.save()
verify(mock_storage).save_data(any_dictionary())
inventory.free()
# mock_network_provider.gd
# Expert utility for simulating network latency and packet loss during local tests.
# Grounded in Godot 4.x ENetMultiplayerPeer simulation properties.
extends Node
class_name MockNetworkProvider
## Configures the multiplayer peer with simulated latency/loss.
static func configure_simulated_network(peer: ENetMultiplayerPeer, latency_ms: int = 50, jitter_ms: int = 10, loss_percent: float = 0.05) -> void:
if peer == null:
return
# ENet-specific simulation (available via host/peer settings)
# Note: In Godot 4, some of these are set via the ENetConnection.
print("Network Simulator: Latency=%dms, Jitter=%dms, Loss=%.1f%%" % [latency_ms, jitter_ms, loss_percent * 100])
# Placeholder for lower-level ENet configurations
# peer.get_host().set_bandwidth_limit(...) etc.
## Expert Tip: Use this provider in 'Integration Test' scenes to verify
## that prediction/reconciliation logic handles real-world lag.
# parameter_fuzz_tester.gd
# Stress testing systems with random data
extends Node
# EXPERT NOTE: Fuzz testing catches edge-case crashes
# by feeding unexpected ranges into your functions.
func fuzz_test_damage_system():
var combat = CombatLogic.new()
for i in 100:
var rand_dmg = randf_range(-1000, 5000)
combat.apply(rand_dmg) # Should not crash
combat.free()
# performance_benchmark_runner.gd
# Measuring execution time exactly
extends Node
# EXPERT NOTE: Benchmarking in Godot should use
# Time.get_ticks_usec() for microsecond precision.
func benchmark_loop_performance():
var start = Time.get_ticks_usec()
for i in 1000000:
var x = i * i
var duration = Time.get_ticks_usec() - start
print("Execution took: ", duration, " microseconds")
# physics_collision_test.gd
# Verifying world intersections
extends GdUnitTestSuite
# EXPERT NOTE: Automated physics tests ensure that
# changes to collision layers don't break gameplay.
func test_wall_collision():
var ball = preload("res://ball.tscn").instantiate()
var wall = preload("res://wall.tscn").instantiate()
add_child(ball)
add_child(wall)
ball.global_position = Vector2(0, 0)
wall.global_position = Vector2(5, 0)
await yield_frames(5) # Give physics time to resolve
assert_that(ball.is_on_wall()).is_true()
# scene_integration_test.gd
# Testing full scene interaction
extends GdUnitTestSuite
# EXPERT NOTE: Integration tests ensure that nodes in
# a scene interact correctly after .instantiate().
func test_ui_button_press():
var scene = spy("res://menu.tscn")
var button = scene.get_node("StartButton")
# Simulate user interaction
button.emit_signal("pressed")
assert_that(scene.is_game_started).is_true()
scene.free()
# signal_emission_test.gd
# Verifying signal behavior in tests
extends GdUnitTestSuite
# EXPERT NOTE: Verifying that signals fire correctly is
# critical for decoupled Godot architectures.
func test_signal_fired_on_death():
var player = Player.new()
var monitor = monitor_signals(player)
player.health = 0
player.check_death()
verify(monitor).is_emitted("died")
player.free()
# snapshot_tester.gd
# Expert utility for visual regression testing (Snapshot Testing).
# Grounded in Godot 4.x RenderingServer and Image comparison.
extends Node
class_name SnapshotTester
## Takes a screenshot and compares it to a "golden" reference image.
func run_snapshot_test(scene_name: String) -> bool:
# Wait for frame draw to complete
await RenderingServer.frame_post_draw
var viewport = get_viewport()
var img = viewport.get_texture().get_image()
var ref_path = "res://tests/snapshots/%s.png" % scene_name
if not FileAccess.file_exists(ref_path):
img.save_png(ref_path) # Save first run as reference
print("Snapshot: Reference saved for %s" % scene_name)
return true
var ref_img = Image.load_from_file(ref_path)
var diff = img.compute_image_metrics(ref_img, false)
if diff["max"] > 0.01: # Threshold for drift
push_error("Snapshot Test FAILED for %s (Diff: %.4f)" % [scene_name, diff["max"]])
return false
print("Snapshot Test PASSED for %s" % scene_name)
return true
# test_data_factory.gd
# Generating mock game data for testing
class_name TestDataFactory extends Object
# EXPERT NOTE: Centralizing data generation makes tests
# cleaner and easier to update when schemas change.
static func create_maxed_player() -> Player:
var p = Player.new()
p.hp = 999
p.str = 99
return p
# wait_for_frame_test.gd
# Testing asynchronous behavior
extends GdUnitTestSuite
# EXPERT NOTE: Use await yield_signal() or yield_frames()
# in GdUnit4 to test logic that spans multiple frames.
func test_delayed_respawn():
var player = Player.new()
player.die()
# Wait for the respawn timer to finish
await yield_seconds(2.0)
assert_that(player.is_alive).is_true()
player.free()