Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when writing tests for Godot projects — TDD workflow with GUT and gdUnit4, covers both GDScript and C#
.claude/skills/jame581-godot-testing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 42% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 46% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 45% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 91% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 192% | 0% |
This skill covers test-driven development (TDD) for Godot 4.3+ projects using GUT (Godot Unit Testing) and gdUnit4. It includes framework selection, full RED-GREEN-REFACTOR examples, test structure, running tests in CI, and common testing patterns.
> Related skills: godot-code-review for review checklists, dependency-injection for test-friendly architecture, export-pipeline for CI/CD test automation.
| Feature | GUT | gdUnit4 | |-----------------------|----------------------------------|-----------------------------------| | Language | GDScript-first, limited C# | GDScript + C# (first-class) | | Install | AssetLib or git submodule | AssetLib or git submodule | | Editor integration | Built-in GUT panel | Built-in inspector + panel | | Mocking | double() / stub() API | mock() / spy() API | | Scene testing | add_child_autofree() | auto_free() + scene runner | | CI support | gut_cmdln.gd CLI script | gdunit4_runner CLI script | | C# support | Minimal (GDScript wrappers only) | Native C# assertions + lifecycle | | Maturity | Established (Godot 3 + 4) | Godot 4 focused, actively updated | | Best for | Pure GDScript projects | Mixed GDScript/C# or C#-only |
Rule of thumb: Use GUT for GDScript-only projects. Use gdUnit4 for C# projects or when you need first-class C# support and scene runner utilities.
The standard Test-Driven Development cycle: write a failing test (RED), write minimal code to pass (GREEN), then refactor without breaking the test. Each step has its own discipline — don't skip RED (you'll write tests that pass trivially), and don't skip REFACTOR (technical debt compounds).
> See references/tdd-workflow.md for a worked GDScript + C# example walking through all three steps on a HealthComponent.
res://
├── src/
│ └── components/
│ ├── health_component.gd
│ └── HealthComponent.cs
└── tests/
├── unit/
│ ├── test_health_component.gd # GUT: test_ prefix required
│ └── HealthComponentTest.cs # gdUnit4 C#: [TestSuite] attribute
├── integration/
│ ├── test_player_scene.gd
│ └── PlayerSceneTest.cs
└── gut_config.json # GUT configuration (optional)| Framework | GDScript file | C# file | Test method prefix/attribute | |-----------|---------------------|----------------------|------------------------------| | GUT | test_*.gd | N/A | func test_*() | | gdUnit4 | test_*.gd | *Test.cs | func test_*() / [TestCase] |
Both frameworks ship a CLI runner. GUT: addons/gut/gut_cmdln.gd invoked via godot --headless --path . -s addons/gut/gut_cmdln.gd. gdUnit4: --add-gdunit-test-runner argument, or via the editor "GdUnit Tests" dock. CI: tag-triggered or PR-triggered GitHub Action that installs Godot, runs the suite, exits non-zero on failure.
> See references/running-tests.md for full GUT and gdUnit4 CLI invocations + a copy-pasteable GitHub Actions workflow.
Four common patterns: scenes with nodes (instantiate via add_child in before_each, free in after_each), signal testing (assert that emitting works and connect-then-emit fires), mocking/doubling (gdUnit4 Mock<T> or hand-rolled fakes via @export injection), async (await yields, signals, frames in tests).
> See references/testing-patterns.md for full code on each pattern (GDScript + C# where applicable).
| Assertion | Description | |----------------------------------------------|------------------------------------| | assert_eq(actual, expected) | Equality | | assert_ne(actual, expected) | Not equal | | assert_true(value) | Is truthy | | assert_false(value) | Is falsy | | assert_null(value) | Is null | | assert_not_null(value) | Is not null | | assert_gt(actual, expected) | Greater than | | assert_lt(actual, expected) | Less than | | assert_gte(actual, expected) | Greater than or equal | | assert_lte(actual, expected) | Less than or equal | | assert_has(collection, item) | Collection contains item | | assert_does_not_have(collection, item) | Collection does not contain item | | assert_string_contains(str, sub) | String contains substring | | assert_almost_eq(actual, expected, margin) | Float equality within margin | | assert_signal_emitted(obj, signal_name) | Signal was emitted | | assert_signal_not_emitted(obj, signal_name)| Signal was not emitted |
| GDScript | C# | Description | |----------------------------------------------------|-------------------------------------------------|---------------------------------| | assert_that(val).is_equal(exp) | AssertThat(val).IsEqual(exp) | Equality | | assert_that(val).is_not_equal(exp) | AssertThat(val).IsNotEqual(exp) | Not equal | | assert_that(val).is_true() | AssertThat(val).IsTrue() | Is true | | assert_that(val).is_false() | AssertThat(val).IsFalse() | Is false | | assert_that(val).is_null() | AssertThat(val).IsNull() | Is null | | assert_that(val).is_not_null() | AssertThat(val).IsNotNull() | Is not null | | assert_that(val).is_greater(exp) | AssertThat(val).IsGreater(exp) | Greater than | | assert_that(val).is_less(exp) | AssertThat(val).IsLess(exp) | Less than | | assert_that(val).is_between(min, max) | AssertThat(val).IsBetween(min, max) | In range (inclusive) | | assert_that(arr).contains([a, b]) | AssertThat(arr).Contains(a, b) | Array contains elements | | assert_that(str).contains("sub") | AssertThat(str).Contains("sub") | String contains substring | | assert_that(val).is_approximately(exp, margin) | AssertThat(val).IsApproximately(exp, margin) | Float within margin | | assert_signal(mon).is_emitted("name") | AssertSignal(mon).IsEmitted("name") | Signal emitted |
Avoid testing things that add noise without catching real bugs:
Node.add_child() works or that @export variables show up in the editorassert_almost_eq / IsApproximately for physics valuestest_*.gd / *Test.cs)GutTest / GdUnit4.GdUnitTestSuite)add_child_autofree or auto_free — never manual queue_free()| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-17 | pass→pass | 4,573 | 2,351 | -49% | 1 | 1 | 0% | 819 | 2,535 | +210% | 0 | 0 | — |
case-01 | fail→pass | 15,393 | 8,829 | -43% | 1 | 1 | 0% | 2,603 | 3,692 | +42% | 0 | 0 | — |
case-02 | fail→pass | 15,340 | 7,281 | -53% | 1 | 1 | 0% | 2,462 | 3,587 | +46% | 0 | 0 | — |
case-03 | pass→pass | 10,111 | 4,484 | -56% | 1 | 1 | 0% | 1,737 | 2,963 | +71% | 0 | 0 | — |
case-04 | pass→pass | 10,157 | 5,793 | -43% | 1 | 1 | 0% | 1,598 | 3,192 | +100% | 0 | 0 | — |
case-05 | pass→pass | 8,435 | 6,193 | -27% | 1 | 1 | 0% | 1,541 | 3,322 | +116% | 0 | 0 | — |
case-06 | pass→pass | 10,055 | 5,538 | -45% | 1 | 1 | 0% | 1,835 | 3,216 | +75% | 0 | 0 | — |
case-07 | pass→pass | 9,367 | 4,765 | -49% | 1 | 1 | 0% | 1,631 | 3,050 | +87% | 0 | 0 | — |
case-08 | pass→pass | 12,734 | 6,715 | -47% | 1 | 1 | 0% | 1,892 | 3,210 | +70% | 0 | 0 | — |
case-09 | pass→pass | 14,997 | 9,744 | -35% | 1 | 1 | 0% | 2,088 | 3,538 | +69% | 0 | 0 | — |
case-10 | pass→pass | 12,381 | 7,095 | -43% | 1 | 1 | 0% | 1,811 | 3,248 | +79% | 0 | 0 | — |
case-11 | pass→pass | 11,057 | 8,494 | -23% | 1 | 1 | 0% | 1,532 | 3,366 | +120% | 0 | 0 | — |
case-12 | pass→pass | 15,008 | 12,717 | -15% | 1 | 1 | 0% | 2,343 | 4,149 | +77% | 0 | 0 | — |
case-13 | pass→pass | 5,012 | 2,730 | -46% | 1 | 1 | 0% | 840 | 2,689 | +220% | 0 | 0 | — |
case-14 | fail→pass | 11,822 | 5,717 | -52% | 1 | 1 | 0% | 2,189 | 3,164 | +45% | 0 | 0 | — |
case-15 | pass→pass | 12,526 | 13,522 | +8% | 1 | 1 | 0% | 2,124 | 4,583 | +116% | 0 | 0 | — |
case-16 | pass→pass | 7,991 | 3,896 | -51% | 1 | 1 | 0% | 1,333 | 2,746 | +106% | 0 | 0 | — |
case-18 | pass→pass | 12,311 | 6,086 | -51% | 1 | 1 | 0% | 1,705 | 3,124 | +83% | 0 | 0 | — |
case-19 | fail→pass | 12,072 | 8,944 | -26% | 1 | 1 | 0% | 1,858 | 3,556 | +91% | 0 | 0 | — |
case-20 | fail→pass | 5,512 | 2,514 | -54% | 1 | 1 | 0% | 892 | 2,607 | +192% | 0 | 0 | — |
case-21 | fail→fail | 7,545 | 5,343 | -29% | 1 | 1 | 0% | 1,294 | 3,090 | +139% | 0 | 0 | — |
case-22 | pass→pass | 3,384 | 2,760 | -18% | 1 | 1 | 0% | 473 | 2,579 | +445% | 0 | 0 | — |
case-23 | pass→pass | 8,024 | 4,849 | -40% | 1 | 1 | 0% | 1,266 | 2,983 | +136% | 0 | 0 | — |
case-24 | pass→pass | 18,505 | 18,513 | +0% | 1 | 1 | 0% | 2,971 | 5,044 | +70% | 0 | 0 | — |
case-25 | pass→pass | 20,134 | 17,699 | -12% | 1 | 1 | 0% | 3,557 | 5,306 | +49% | 0 | 0 | — |
case-26 | pass→pass | 21,809 | 15,410 | -29% | 1 | 1 | 0% | 3,429 | 4,593 | +34% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 26 cases were attempted. The headline lift of +19 percentage points is the difference between those two pass rates over the 26 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.