Debugging, Profiling & Unit Testing

Also: OS.get_static_memory_usage(), orphan node detection with print_orphan_nodes() (debug builds), --verbose CLI flag, and file logging (*Debug → File Logging*).

36.1 Printing and assertions

print_debug("Value: ", x)               # includes file and line
push_error("Invalid state: %s" % state) # red entry with stack trace in the Debugger
push_warning("Deprecated call")
assert(health >= 0, "Health went negative")  # only in debug builds; stripped in release
print(get_stack())                      # current call stack (debug builds)
breakpoint                              # keyword: pause the debugger here

36.2 The debugger

Click the gutter to set breakpoints; use *Step Into* (F11), *Step Over* (F10), *Continue* (F12).

Inspect locals/members in the Stack Variables panel; hover variables in the editor while paused.

Remote scene tree (Scene dock → Remote) shows the live game tree; edit properties at runtime.

*Debug* menu: Visible Collision Shapes, Visible Navigation, Visible Paths, Synchronize Scene/Script Changes (live editing), Deploy with Remote Debug.

Errors tab collects runtime errors and warnings with stack traces.

Godot 4.5 can include script backtraces in release builds (*Debug → Settings → GDScript*), useful for crash reports.

36.3 Profilers and monitors

Time per function (script and engine), frame time Visual Profiler CPU/GPU time per rendering stage

FPS, memory, object/node counts, draw calls, physics stats Network Profiler RPC and synchronization bandwidth Texture/mesh memory usage

Also: OS.get_static_memory_usage(), orphan node detection with print_orphan_nodes() (debug builds), --verbose CLI flag, and file logging (*Debug → File Logging*).

func _ready() -> void:
	Performance.add_custom_monitor("game/enemies", func(): return get_tree().get_node_count_in_group("enemies"))

func _process(_delta: float) -> void:
	$DebugLabel.text = "FPS: %d  Nodes: %d  Draw calls: %d" % [
		Engine.get_frames_per_second(),
		Performance.get_monitor(Performance.OBJECT_NODE_COUNT),
		Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME),
	]

func measure() -> void:
	var start := Time.get_ticks_usec()
	expensive_function()
	print("took %d µs" % (Time.get_ticks_usec() - start))

36.4 Unit testing with GUT

GUT (Godot Unit Test) is a popular testing addon (install from the Asset Library; use GUT 9.x for Godot 4).

Run from the GUT panel, or headless in CI:

GUT also offers doubles/stubs (double(), stub()), parameterized tests, and await wait_seconds(1) / await wait_frames(5) for async tests.

# res://test/unit/test_health_component.gd
extends GutTest

var health: HealthComponent

func before_each() -> void:
	health = HealthComponent.new()
	health.max_health = 10
	add_child_autofree(health)          # freed automatically after the test

func test_starts_full() -> void:
	assert_eq(health.current, 10)

func test_damage_reduces_health() -> void:
	health.damage(3)
	assert_eq(health.current, 7, "Health should drop by damage amount")

func test_died_signal_emitted_at_zero() -> void:
	watch_signals(health)
	health.damage(100)
	assert_signal_emitted(health, "died")
	assert_eq(health.current, 0)

func test_damage_never_negative() -> void:
	for i in 5: health.damage(5)
	assert_true(health.current >= 0)
godot --headless -s addons/gut/gut_cmdln.gd -gdir=res://test -ginclude_subdirs -gexit

36.5 Unit testing with gdUnit4

gdUnit4 provides fluent assertions, mocking, scene runners, and an editor inspector.

Run from the gdUnit4 panel or via its command-line runner (addons/gdUnit4/runtest.sh / runtest.cmd -a res://test). Both frameworks integrate with GitHub Actions using headless Godot.

# res://test/player_test.gd
extends GdUnitTestSuite

func test_player_moves_right() -> void:
	var runner := scene_runner("res://scenes/player.tscn")
	runner.simulate_action_press("move_right")
	await runner.simulate_frames(30)
	runner.simulate_action_release("move_right")
	assert_float(runner.get_property("position").x).is_greater(0.0)

func test_inventory_add() -> void:
	var inv: Inventory = auto_free(Inventory.new())
	inv.add("potion", 3)
	assert_int(inv.count("potion")).is_equal(3)
	assert_array(inv.items()).contains(["potion"])

36.6 Testable code

Put logic in plain classes (RefCounted) or small components that don't depend on the whole scene.

Inject dependencies (exported references, constructor args) instead of hard-coded paths.

Keep randomness seedable (RandomNumberGenerator.seed).