Godot Testing
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.
Framework Selection
| 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.
TDD Workflow: RED-GREEN-REFACTOR
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.
Test Directory Structure
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)
Naming conventions
| Framework | GDScript file | C# file | Test method prefix/attribute |
|---|---|---|---|
| GUT | test_*.gd |
N/A | func test_*() |
| gdUnit4 | test_*.gd |
*Test.cs |
func test_*() / [TestCase] |
Running Tests
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.
Testing Patterns
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).
Common Assertions
GUT assertions
| 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 |
gdUnit4 assertions (GDScript + C#)
| 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 |
What NOT to Test
Avoid testing things that add noise without catching real bugs:
- Godot engine internals — do not assert that
Node.add_child()works or that@exportvariables show up in the editor - Private implementation details — test behavior through the public API; if a refactor breaks a test that covers only private state, the test is wrong
- Visual/rendering output — pixel-level rendering results are brittle; test the data driving the visuals instead
- Timing-sensitive floats without margins — use
assert_almost_eq/IsApproximatelyfor physics values - One-liners that wrap a built-in — a property getter that just returns a field needs no test
- Every possible invalid input — test the documented contract, not every imaginable misuse
Checklist
- Each test file matches the naming convention for the chosen framework (
test_*.gd/*Test.cs) - Tests extend the correct base class (
GutTest/GdUnit4.GdUnitTestSuite) - Nodes added to the scene tree use
add_child_autofreeorauto_free— never manualqueue_free() - Signals are watched before the action that triggers them
- Mocks/doubles are used for external dependencies, not for the unit under test
- Each test covers exactly one behavior (one logical assertion per test)
- CI workflow runs tests headlessly on every push and PR
- Flaky async tests use explicit timeouts, not arbitrary sleep durations
- Tests pass before merging (RED is only acceptable while actively implementing)