Unit Testing Godot Games: GUT Framework Getting Started
GUT (Godot Unit Test) is the de facto unit testing framework for Godot games. It integrates directly into the Godot editor, lets you write tests in GDScript alongside your game code, and runs headlessly for CI pipelines. This guide covers installation, writing your first tests, and the assertion API.
Key Takeaways
GUT tests are GDScript scripts that extend GutTest. Each test method starts with test_. The framework discovers and runs them automatically.
Install GUT via the Asset Library or as a submodule. The Godot Asset Library version is easiest for new projects. For CI/CD, the git submodule approach gives you version control.
Assertions use assert_* methods. assert_eq, assert_true, assert_called, assert_signal_emitted — these are all built-in GUT assertions, not Godot's built-in assert.
Watch the before_each / after_each lifecycle. Tests share no state by default, but if you instantiate nodes or scenes you must free them in after_each to avoid memory leaks between tests.
Run GUT from the Godot editor or from the CLI. The editor panel is for development iteration. The CLI headless mode (godot --headless) is for CI.
What Is GUT?
GUT (Godot Unit Test) is the most popular unit testing framework for Godot. It provides:
- A test runner panel inside the Godot editor
- GDScript test scripts with a familiar xUnit-style API
- Assertions for values, signals, methods, and scene nodes
- CLI runner for headless execution in CI
- Mock objects and spy functionality
GUT works with Godot 4.x and has a separate branch for Godot 3.x.
Installation
Option 1: Godot Asset Library (Recommended for New Projects)
- Open your Godot project
- Go to AssetLib tab in the editor
- Search for "GUT"
- Click "Godot Unit Testing" → "Download" → "Install"
- GUT installs to
res://addons/gut/ - Go to Project → Project Settings → Plugins and enable "GUT"
Option 2: Git Submodule (Recommended for CI)
git submodule add https://github.com/bitwes/Gut.git addons/gutThen enable the plugin in project settings.
Setting Up the Test Directory
Create a test/ directory in your project for test scripts:
res://
├── addons/gut/
├── scripts/
│ └── player.gd
│ └── inventory.gd
└── test/
└── test_player.gd
└── test_inventory.gdConfigure GUT via Project → Project Settings → GUT:
- Directories:
[res://test] - Prefix:
test_(test file prefix)
Writing Your First Test
Test scripts extend GutTest:
# test/test_player.gd
extends GutTest
var player
func before_each():
player = Player.new()
add_child(player)
func after_each():
player.queue_free()
func test_player_starts_with_full_health():
assert_eq(player.health, 100, "Player should start with 100 health")
func test_player_takes_damage():
player.take_damage(20)
assert_eq(player.health, 80, "Player should have 80 health after 20 damage")
func test_player_cannot_have_negative_health():
player.take_damage(999)
assert_gte(player.health, 0, "Health should never go below 0")
func test_player_dies_when_health_reaches_zero():
player.take_damage(100)
assert_true(player.is_dead(), "Player should be dead at 0 health")
func test_player_heals_correctly():
player.take_damage(50)
player.heal(30)
assert_eq(player.health, 80, "Player should heal correctly")The game class being tested:
# scripts/player.gd
extends Node
var health: int = 100
func take_damage(amount: int) -> void:
health = max(0, health - amount)
func heal(amount: int) -> void:
health = min(100, health + amount)
func is_dead() -> bool:
return health <= 0GUT Assertion API
Value Assertions
assert_eq(got, expected, "message") # Equal
assert_ne(got, expected, "message") # Not equal
assert_gt(got, expected, "message") # Greater than
assert_gte(got, expected, "message") # Greater than or equal
assert_lt(got, expected, "message") # Less than
assert_lte(got, expected, "message") # Less than or equal
assert_true(value, "message") # Truthy
assert_false(value, "message") # Falsy
assert_null(value, "message") # Is null
assert_not_null(value, "message") # Is not nullType Assertions
assert_is(instance, SomeClass, "message") # Instance of class
assert_typeof(value, TYPE_INT, "message") # Godot type constantString Assertions
assert_string_contains(text, "substring", "message")
assert_string_starts_with(text, "prefix", "message")
assert_string_ends_with(text, "suffix", "message")Array Assertions
assert_has(array_or_dict, value, "message") # Contains value
assert_does_not_have(array, value, "message") # Does not contain
assert_has_index(array, index, "message") # Index exists
func test_inventory_contains_items():
var inv = Inventory.new()
inv.add_item("sword")
inv.add_item("shield")
assert_has(inv.items, "sword")
assert_has(inv.items, "shield")
assert_does_not_have(inv.items, "axe")Test Lifecycle
extends GutTest
func before_all():
# Runs once before any test in this script
# Good for expensive setup (loading resources)
func before_each():
# Runs before each test method
# Good for creating fresh instances
func after_each():
# Runs after each test method
# IMPORTANT: free nodes you created
func after_all():
# Runs once after all tests complete
# Good for cleanup
func test_example():
passTesting with Scenes
Test scene instantiation in before_each:
extends GutTest
var scene_instance
func before_each():
var scene = load("res://scenes/game_level.tscn")
scene_instance = scene.instantiate()
add_child(scene_instance)
func after_each():
scene_instance.queue_free()
func test_level_has_spawn_point():
var spawn = scene_instance.get_node("SpawnPoint")
assert_not_null(spawn, "Level should have a SpawnPoint node")
func test_level_enemy_count():
var enemies = scene_instance.get_tree().get_nodes_in_group("enemies")
assert_gte(enemies.size(), 1, "Level should have at least one enemy")Running Tests
From the Godot Editor
- Open the GUT panel (bottom of editor)
- Click Run All to run all discovered tests
- Red = failing, Green = passing
- Click a failing test to see the assertion that failed
From the Command Line (Headless)
godot --headless -s addons/gut/gut_cmdln.gd \
-gdir=res://test \
-gprefix=test_ \
-gsuffix=.gd \
-glog=1Exit code is 0 on success, non-zero on failure. Use this in CI.
Common Mistakes
Not freeing nodes in after_each: Leads to memory leaks and test interference. Always call queue_free() on instantiated nodes.
Testing implementation details: Test behavior, not internal state. assert_eq(player._hp_internal, 80) is fragile. assert_eq(player.health, 80) is stable.
Relying on node tree order: Tests run in the order GUT discovers them, which may not match file order. Don't write tests that depend on execution order.
Using Godot's built-in assert(): GUT's assert_eq() and similar methods report properly. Godot's native assert() crashes the engine on failure — never use it in GUT tests.
Summary
GUT gives Godot developers a capable unit testing framework that lives inside the editor:
- Tests extend
GutTest, methods start withtest_ - Rich assertion library for values, types, strings, and arrays
- Scene and node testing with proper lifecycle hooks
- CLI runner for CI integration
Start by testing your pure logic classes (damage calculation, inventory management, save/load) — these have no scene dependencies and are the easiest to test first.