Unit Testing Godot Games: GUT Framework Getting Started

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

  1. Open your Godot project
  2. Go to AssetLib tab in the editor
  3. Search for "GUT"
  4. Click "Godot Unit Testing" → "Download" → "Install"
  5. GUT installs to res://addons/gut/
  6. Go to Project → Project Settings → Plugins and enable "GUT"
git submodule add https://github.com/bitwes/Gut.git addons/gut

Then 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.gd

Configure 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 <= 0

GUT 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 null

Type Assertions

assert_is(instance, SomeClass, "message")  # Instance of class
assert_typeof(value, TYPE_INT, "message")  # Godot type constant

String 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():
    pass

Testing 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

  1. Open the GUT panel (bottom of editor)
  2. Click Run All to run all discovered tests
  3. Red = failing, Green = passing
  4. 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=1

Exit 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 with test_
  • 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.

Read more