From qa-game
Author and run GUT (Godot Unit Test) - the community-canonical GDScript test framework at github.com/bitwes/Gut and gut.readthedocs.io. Covers install (Godot Asset Library or manual `addons/gut/` copy + plugin enable), GUT panel inside the editor, writing tests that extend GutTest with `test_` prefix methods, the assertion family (assert_eq / assert_almost_eq / assert_true / assert_signal_emitted), lifecycle hooks (before_each / after_each / before_all / after_all), inner classes for grouping, parameterized tests via `params=[...]`, doubles / stubs / spies, async / coroutine tests, the command-line runner (`-d -s addons/gut/gut_cmdln.gd -gdir=res://test -gjunit_xml_file=... -gexit`), JUnit XML export, and CI integration. Godot 4.x uses GUT 9.x (current main branch supports 4.6.x; godot_4_7 branch for 4.7.x); Godot 3.x uses GUT 7.x. Use when the unit under test is GDScript code in a Godot project.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qa-game:godot-gut-testsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill wraps [GUT (Godot Unit
This skill wraps GUT (Godot Unit Test) - the community-canonical GDScript test framework. Godot does not ship a first-party equivalent of Unity's Test Framework or Unreal's Automation System for GDScript user code.
Per the GUT README:
| Engine | GUT version | Branch |
|---|---|---|
| Godot 4.6.x | 9.x | main |
| Godot 4.7.x | 9.x | godot_4_7 branch |
| Godot 3.4.x | 7.x (currently 7.4.3) | maintained |
Composes with:
game-test-categories-reference
for the canonical six categories GUT tests map to.platform-cert-overview-reference
for cert-gated requirements GUT tests should cover where the
title ships to Xbox / PlayStation / Switch via Godot exports.gameplay-recording-replay-skill
for replay-driven coverage authored on top of GUT.before_each / after_each / parameterised / mock
test surface inside the editor with a CLI runner for CI.For pure-C# tests inside Godot's C# scripting layer, use .NET-canonical test runners (xUnit, NUnit) rather than GUT - GUT is GDScript-first.
Two paths, per the GUT README:
Asset Library (recommended for compatible engine versions):
Manual:
addons/gut/ directory into your project's addons/
directory.After enabling, a GUT panel appears in the bottom dock.
Typical convention (the GUT runner defaults work with this):
my_godot_project/
addons/gut/ # the framework
src/
health.gd
enemy_ai.gd
test/
unit/
test_health.gd
test_enemy_ai.gd
integration/
test_save_load.gd
.gutconfig.json # optional config file
project.godot
Per gut.readthedocs.io, tests
"extend the GutTest class and use assertion methods like
assert_eq, assert_almost_eq, assert_true, and
assert_signal_emitted". Test methods are prefixed test_.
extends GutTest
func test_damage_deducts_correct_amount():
var health := preload("res://src/health.gd").new()
health.initialize(100)
health.apply_damage(35)
assert_eq(health.current, 65, "65 HP after 35 dmg from 100")
Per the same docs, lifecycle hooks are:
| Hook | Scope |
|---|---|
before_all | Once before every test in the script |
before_each | Before each test_* method |
after_each | After each test_* method |
after_all | Once after every test in the script |
extends GutTest
var _player
func before_each():
_player = preload("res://src/player.gd").new()
func after_each():
_player.queue_free()
_player = null
func test_player_starts_with_full_health():
assert_eq(_player.health, _player.max_health)
Common assertions per gut.readthedocs.io and the GUT README:
| Assertion | Use |
|---|---|
assert_eq(a, b, msg) | Equality |
assert_ne(a, b, msg) | Inequality |
assert_almost_eq(a, b, tol, msg) | Float comparison within tolerance |
assert_true(v, msg) / assert_false(v, msg) | Boolean |
assert_null(v, msg) / assert_not_null(v, msg) | Null check |
assert_has(coll, v, msg) / assert_does_not_have(coll, v, msg) | Membership |
assert_signal_emitted(obj, "signal_name", msg) | Signal emission |
assert_signal_emitted_with_parameters(obj, "name", args, msg) | Signal emission with payload |
assert_gt(a, b, msg) / assert_lt(a, b, msg) | Ordering |
assert_called(double, "method_name", args) | Spy verification |
Per the README, GUT exposes "a plethora of asserts and utility
methods" - check the addons/gut/test.gd source in your installed
version for the complete signature list at the engine version you
ship against.
Per the GUT README, tests can be organised via inner classes:
extends GutTest
class TestAddItem:
extends GutTest
var _inv
func before_each():
_inv = preload("res://src/inventory.gd").new()
func test_increases_count_by_stack():
_inv.add_item("potion", 3)
assert_eq(_inv.count_of("potion"), 3)
func test_rejects_over_max_stack():
var ok = _inv.add_item("potion", 999)
assert_false(ok)
class TestRemoveItem:
extends GutTest
# …
Each inner class reports as its own grouping in the GUT panel.
Per gut.readthedocs.io, GUT
supports "parameterized tests using params=[...]":
extends GutTest
var damage_cases = [
[100, 25, 75],
[100, 100, 0],
[50, 60, 0], # clamps to zero, not negative
[100, 0, 100],
]
func test_apply_damage_table(params=use_parameters(damage_cases)):
var health = preload("res://src/health.gd").new()
health.initialize(params[0])
health.apply_damage(params[1])
assert_eq(health.current, params[2])
Each row in damage_cases becomes its own test case in the
report.
Per the GUT README, GUT supports "full / partial doubles, stubbing, spies". Typical pattern:
extends GutTest
func test_save_calls_backend():
var backend_double = double("res://src/backend.gd").new()
stub(backend_double, "save").to_return(true)
var manager = preload("res://src/save_manager.gd").new()
manager.backend = backend_double
manager.save_game({"hp": 50})
assert_called(backend_double, "save", [{"hp": 50}])
double(path) produces a fake script; stub(...).to_return(value)
configures return values; assert_called(...) is the spy
assertion.
Per gut.readthedocs.io, GUT
supports "coroutines and async test support" - a test_* method
can await signals or timers and the runner waits before moving
on:
extends GutTest
func test_async_load_completes():
var loader = preload("res://src/async_loader.gd").new()
loader.start_load("res://big_scene.tscn")
await get_tree().create_timer(0.5).timeout
assert_true(loader.is_done)
assert_not_null(loader.result)
After enabling the plugin, the GUT panel appears in the bottom dock (Editor → Bottom Panel → GUT). Per gut.readthedocs.io, the panel supports "normal and compact views". Click Run All or right-click a script → Run to execute.
Per gut.readthedocs.io, the
command-line runner is invoked via
-d -s addons/gut/gut_cmdln.gd plus GUT-specific options:
godot \
--headless \
-d \
-s addons/gut/gut_cmdln.gd \
-gdir=res://test \
-gjunit_xml_file=artifacts/gut-junit.xml \
-gexit
Common GUT CLI flags (per the same docs):
| Flag | Effect |
|---|---|
-gdir=res://test | Recurse this directory for tests |
-gtest=res://test/unit/test_health.gd | Run a single test script |
-ginner_class=TestAddItem | Limit to one inner class |
-gunit_test_name=test_increases_count_by_stack | Limit to one test method |
-gconfig=res://.gutconfig.json | Load config from JSON |
-gjunit_xml_file=artifacts/gut-junit.xml | Write JUnit XML report |
-gjunit_xml_timestamp | Add timestamp suffix to filename |
-glog=3 | Log verbosity (0 - 3) |
-gexit | Exit Godot after run (essential in CI) |
--headless runs Godot without a display window - required for
most CI environments. -d runs in debug mode so the test runner
script (addons/gut/gut_cmdln.gd) executes.
A .gutconfig.json at the project root lets the GUT panel and
CLI runner share settings:
{
"dirs": ["res://test/unit", "res://test/integration"],
"include_subdirs": true,
"log_level": 1,
"junit_xml_file": "artifacts/gut-junit.xml",
"double_strategy": "partial"
}
(Field names per
gut.readthedocs.io - check your
installed addons/gut/ version for the authoritative schema.)
GUT exports JUnit XML when -gjunit_xml_file=… is set. Per
gut.readthedocs.io, this is the
recommended CI-consumable output. Top-level shape:
<testsuites name="GUT" tests="42" failures="1" disabled="0" errors="0" time="3.214">
<testsuite name="res://test/unit/test_health.gd"
tests="5" failures="1" errors="0" time="0.124">
<testcase classname="test_health"
name="test_damage_deducts_correct_amount"
time="0.012"/>
<testcase classname="test_health"
name="test_clamps_below_zero"
time="0.014">
<failure message="Expected 0 but was -5"
type="AssertionFailed"/>
</testcase>
</testsuite>
</testsuites>
Standard JUnit XML - consumed by GitHub Actions test reporters, Jenkins JUnit plugin, GitLab CI test report widget, etc.
Per the GUT README, GUT also tracks pre-test errors / orphan nodes / unhandled signals - these surface in the GUT panel and the JUnit report.
GitHub Actions example:
jobs:
gut-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Godot
run: |
GODOT=4.6.1-stable
wget -q https://github.com/godotengine/godot/releases/download/${GODOT}/Godot_v${GODOT}_linux.x86_64.zip
unzip -q Godot_v${GODOT}_linux.x86_64.zip -d godot
mv "godot/Godot_v${GODOT}_linux.x86_64" godot/godot
chmod +x godot/godot
- name: Run GUT
run: |
mkdir -p artifacts
./godot/godot --headless -d \
-s addons/gut/gut_cmdln.gd \
-gdir=res://test \
-gjunit_xml_file=artifacts/gut-junit.xml \
-gexit
- uses: actions/upload-artifact@v4
if: always()
with:
name: gut-junit
path: artifacts/gut-junit.xml
- uses: dorny/test-reporter@v1
if: always()
with:
name: GUT
path: artifacts/gut-junit.xml
reporter: java-junit
For Godot 3.x projects, swap the engine version and use GUT 7.x.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Forgetting --headless in CI | Godot tries to open a window; CI hangs / fails | Always --headless for CI runs |
Forgetting -gexit | Godot stays open after the run; CI step never completes | Always -gexit for CI runs |
Test scripts outside extends GutTest | Runner skips them silently | Per GUT README, every test script extends GutTest (or extends an inner class that does) |
Test methods without test_ prefix | Runner skips them silently | Per the same README, methods must be prefixed test_ |
| Using GUT 9 on Godot 3.x (or 7 on 4.x) | Plugin won't load | Match engine + GUT major-version per the compatibility table in this skill |
| Stubbing without a double | stub(...) requires a doubled object | Use double("res://script.gd").new() first |
Async tests without await | Coroutine completes before assertion | await the signal / timer, then assert |
Asserting assert_eq on floats | Floating-point inequality | Use assert_almost_eq(a, b, tol) per the assertion table above |
| Skipping JUnit XML in CI | CI surfaces no per-test failure detail | Always emit -gjunit_xml_file=… and feed to a CI reporter |
| Tests that depend on autoload singletons | Cross-test contamination | Re-initialise / reset autoloads in before_each |
double("res://path.gd")
needs the script's res:// path; doubling autoloads or engine
C++ classes is not supported directly.failures>0 as the source of truth.--headless CI works but is slower per-test than the
in-editor panel because Godot starts fresh each run.npx claudepluginhub testland/qa --plugin qa-gameProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.