From gut-skill
Godot Unit Testing (GUT) framework for GDScript. Covers test structure, assertions, doubles/mocks, async testing, memory management, and CLI execution. Use when writing tests, understanding test patterns, or troubleshooting test failures.
How this skill is triggered — by the user, by Claude, or both
Slash command
/gut-skill:gutThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
GUT is a unit testing framework for Godot (GDScript). Test files live in `res://test/` and must be named with `test_` prefix.
GUT is a unit testing framework for Godot (GDScript). Test files live in res://test/ and must be named with test_ prefix.
extends GutTest
# Lifecycle hooks (all optional)
func before_all(): pass # once before all tests
func before_each(): pass # before each test
func after_each(): pass # after each test
func after_all(): pass # once after all tests
# Tests: any method starting with "test_"
func test_something():
assert_eq(1, 1, "optional message")
extends GutTesttest_pending(), pass_test(), or fail_test() — else marked riskyTest that also extend GutTestbefore_all or referenced in after_all — they are freed after each test finishesassert_eq(got, expected, msg="") # ==
assert_ne(got, expected, msg="") # !=
assert_gt(got, expected, msg="") # >
assert_gte(got, expected, msg="") # >=
assert_lt(got, expected, msg="") # <
assert_lte(got, expected, msg="") # <=
assert_between(got, low, high, msg="") # low <= got <= high
assert_almost_eq(got, expected, error_interval, msg="")
assert_almost_ne(got, expected, error_interval, msg="")
assert_eq_deep(v1, v2) # deep comparison
assert_ne_deep(v1, v2)
assert_true(got, msg="")
assert_false(got, msg="")
assert_null(got, msg="")
assert_not_null(got, msg="")
assert_is(object, a_class, msg="")
assert_typeof(object, type, msg="")
assert_not_typeof(object, type, msg="")
assert_same(v1, v2, msg="") # same instance
assert_not_same(v1, v2, msg="")
assert_has(obj, element, msg="")
assert_does_not_have(obj, element, msg="")
assert_string_contains(text, search, match_case=true)
assert_string_starts_with(text, search, match_case=true)
assert_string_ends_with(text, search, match_case=true)
assert_has_method(obj, method, msg="")
assert_has_signal(object, signal_name, msg="")
assert_exports(obj, property_name, type)
assert_property(obj, property_name, default_value, new_value)
assert_accessors(obj, property, default, set_to)
# Must watch_signals first (or use wait_for_signal which does it automatically)
watch_signals(obj)
assert_signal_emitted(obj, signal_name, msg="")
assert_signal_not_emitted(obj, signal_name, msg="")
assert_signal_emit_count(obj, signal_name, count, msg="")
assert_signal_emitted_with_parameters(obj, signal_name, params, call_index=-1)
assert_engine_error(text, msg="")
assert_engine_error_count(count, msg="")
assert_push_error(text, msg="")
assert_push_error_count(count, msg="")
assert_push_warning(text, msg="")
assert_push_warning_count(count, msg="")
assert_no_new_orphans(msg="")
assert_freed(obj, title="something")
assert_not_freed(obj, title="something")
pending(text) # mark test as pending/todo (text required)
pass_test(text) # force pass (text required)
fail_test(text) # force fail (text required)
Always clean up nodes to avoid orphan warnings:
# autofree calls free() after each test — but NOT in the scene tree
var node = autofree(Node.new())
var node = autoqfree(Node.new()) # uses queue_free()
# add to tree AND auto-free — use these when you need the node in the tree
var node = add_child_autofree(Node2D.new())
var node = add_child_autoqfree(Node2D.new())
before_all (freed after first test)autofree/autoqfree objects are NOT in the scene tree — they still cause assert_no_new_orphans to fail. Use add_child_autofree instead if you also need assert_no_new_orphans to pass.var MyScript = load('res://my_script.gd')
var MyScene = load('res://my_scene.tscn')
# Script double — methods do nothing, return null
var dbl = double(MyScript).new()
# Scene double
var dbl_scene = double(MyScene).instantiate()
# Partial double — retains original behavior unless stubbed
var pdbl = partial_double(MyScript).new()
# Built-in types
var dbl_node = double(Node2D).new()
# Autoload singletons
var dbl_singleton = double_singleton(MySingleton)
var pdbl_singleton = partial_double_singleton(MySingleton)
Inner classes need registration first (do in before_all or pre-run hook):
func before_all():
register_inner_classes(SomeScript)
func test_foo():
var dbl = double(SomeScript.InnerClass).new()
Classes with static methods crash when doubled. Use ignore_method_when_doubling first:
func before_each():
ignore_method_when_doubling(MyScript, 'my_static_method')
func test_foo():
var dbl = double(MyScript).new() # now safe
# Instance-level stub
stub(dbl.some_method).to_return(42)
stub(dbl.some_method).to_do_nothing()
stub(dbl.some_method).to_call_super()
stub(dbl.some_method).to_call(func(val): return val * 2)
# With specific parameters (two equivalent forms)
stub(dbl.some_method.bind("a")).to_return(999)
stub(dbl, "some_method").when_passed("a").to_return(999)
# Script-level stub (applies to all instances without their own stub)
# Best set in before_each
stub(MyScript, "some_method").to_return(111)
# Fix null default params when calling super
stub(dbl, 'increment').param_defaults([1])
# Add parameters for vararg built-in methods (must be before double())
stub(Node, 'rpc_id').to_do_nothing().param_count(5)
assert_called(dbl, 'method_name')
assert_called(dbl, 'method_name', [arg1, arg2]) # with params
assert_not_called(dbl, 'method_name')
assert_called_count(dbl.foo, 3) # callable form
var params = get_call_parameters(dbl, 'method_name') # last call
var params = get_call_parameters(dbl, 'method_name', 4) # 4th call
var count = get_call_count(dbl, 'method_name')
Only user-defined methods are spied; native Godot methods are not included unless using INCLUDE_NATIVE strategy.
Always use GUT's wait methods — not raw await signal — to prevent test hangs:
await wait_seconds(2.5)
await wait_seconds(0.1, "waiting for timer")
await wait_physics_frames(2) # wait at least 2 for reliability
await wait_idle_frames(10) # alias: wait_process_frames
# Wait for signal OR timeout (returns true if signal fired)
await wait_for_signal(my_obj.some_signal, 5)
assert_true(await wait_for_signal(my_obj.done, 3), "should fire")
# Wait until callable returns true
await wait_until(func(): return is_ready, 5)
# Wait while callable returns true
await wait_while(func(): return is_busy, 10)
wait_for_signal also calls watch_signals internally.
Manually drive _process / _physics_process without running the game loop:
# simulate(obj, times, delta, check_is_processing=false)
var obj = add_child_autofree(MyObject.new())
gut.simulate(obj, 20, 0.1)
assert_eq(obj.a_number, 20)
Add nodes to the tree (add_child_autofree) if using check_is_processing=true. Timers do not fire during simulate — use await for those.
var params = [[1, 2, 3], [4, 5, 9]]
func test_add(p=use_parameters(params)):
assert_eq(p[0] + p[1], p[2])
# Named parameters (more readable)
var named = ParameterFactory.named_parameters(
['a', 'b', 'result'],
[[1, 2, 3], [10, 5, 15]]
)
func test_add_named(p=use_parameters(named)):
assert_eq(p.a + p.b, p.result)
extends GutTest
class TestFeatureA:
extends GutTest
var _subject = null
func before_each():
_subject = Subject.new()
func test_something():
assert_true(_subject.works())
class TestFeatureB:
extends GutTest
# ...
Class name must start with Test. Cannot nest inner test classes.
# res://test/hooks/pre_run.gd
extends GutHookScript
func run():
AudioServer.set_bus_volume_db(0, -INF) # mute audio
register_inner_classes(load('res://some_script.gd'))
# abort() to cancel the test run
Configure in .gutconfig.json:
{
"pre_run_script": "res://test/hooks/pre_run.gd",
"post_run_script": "res://test/hooks/post_run.gd"
}
Tests are run from the command line — no GUI required. Always include -gexit so the process exits after the run. Returns 0 on success, 1 on any failure (suitable for CI).
# Run all tests in a directory
godot -d -s addons/gut/gut_cmdln.gd --path "$PWD" -gdir=res://test -gexit
# Run with subdirectory discovery
godot -d -s addons/gut/gut_cmdln.gd --path "$PWD" -gdir=res://test -ginclude_subdirs -gexit
# Run a specific test file
godot -d -s addons/gut/gut_cmdln.gd --path "$PWD" -gtest=res://test/unit/test_foo.gd -gexit
# Run tests matching a name substring
godot -d -s addons/gut/gut_cmdln.gd --path "$PWD" -gdir=res://test -gunit_test_name=test_my_method -gexit
# Run only scripts whose filename contains a string
godot -d -s addons/gut/gut_cmdln.gd --path "$PWD" -gdir=res://test -gselect=foo -gexit
# Headless / CI (Godot 4)
godot --headless -d -s addons/gut/gut_cmdln.gd --path "$PWD" -gdir=res://test -gexit
.gutconfig.json)Put this at the project root to avoid repeating CLI flags:
{
"dirs": ["res://test/unit/", "res://test/integration/"],
"include_subdirs": true,
"prefix": "test_",
"suffix": ".gd",
"should_exit": true,
"log_level": 1,
"ignore_pause": true
}
With a config file in place, the minimum invocation is:
godot --headless -d -s addons/gut/gut_cmdln.gd --path "$PWD"
Key CLI flags:
| Flag | Purpose |
|---|---|
-gdir=<path> | Directory to search for tests (repeatable) |
-gtest=<path> | Run a specific test file |
-ginclude_subdirs | Recurse into subdirectories |
-gunit_test_name=<str> | Only run tests whose name contains this string |
-gselect=<str> | Only run scripts whose filename contains this string |
-ginner_class=<str> | Only run inner classes whose name contains this string |
-gexit | Exit when done (required for CI) |
-gexit_on_success | Only exit if all tests pass |
-glog=<0-3> | Log verbosity (default 1) |
-gignore_pause | Skip any pause_before_teardown() calls |
-gpre_run_script=<path> | Pre-run hook script |
-gpost_run_script=<path> | Post-run hook script |
gdlint res/scripts/my_script.gd
gdlint src/ # lint a directory
Outputs errors like:
my_script.gd:96: Error: Function argument name "aOrigin" is not valid (function-argument-name)
Always use with version control — formats in-place and can't be undone.
gdformat my_script.gd # format single file
gdformat src/ # format directory
gdformat --check src/ # dry-run: exit 1 if any file would change (use in CI)
gdparse my_script.gd -p # print parse tree
gdradon cc src/my_script.gd # complexity per function/class
Output (A=low, F=high complexity):
src/my_script.gd
F 1:0 foo - B (8)
C 10:0 MyClass - A (2)
- uses: Scony/godot-gdscript-toolkit@master
- run: gdformat --check source/
- run: gdlint source/
var _subject: MyNode
func before_each():
_subject = add_child_autofree(MyNode.new())
func test_something():
assert_eq(_subject.value, 0)
func test_emits_signal():
var obj = autofree(MyClass.new())
watch_signals(obj)
obj.do_thing()
assert_signal_emitted(obj, 'thing_done')
var MyService = load('res://services/my_service.gd')
func test_calls_service():
var svc = double(MyService).new()
stub(svc.fetch).to_return({"id": 1})
var subject = autofree(MyClass.new())
subject.service = svc
subject.run()
assert_called(svc, 'fetch')
npx claudepluginhub rockerboo/gut-skill --plugin gut-skillProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.