Skills/godot-headless-workflow/references/testing.md

104 lines
3.6 KiB
Markdown

# gdUnit4 Testing Deep Dive
Standardize every Godot project on [gdUnit4](https://github.com/MikeSchulze/gdUnit4) — the
actively maintained test framework with a headless CLI runner built for CI and agents.
## Install
1. AssetLib → "gdUnit4" → Install (or add as git submodule under `addons/gdUnit4`)
2. Enable the addon in Project Settings → Plugins
3. Commit `addons/gdUnit4/` with the project so agents on fresh clones can run tests
## Layout
```
tests/
├── unit/ # pure logic: score, economy, state machines
│ └── test_score_manager.gd
├── integration/ # scenes wired together
└── fixtures/ # test data (.tres)
reports/ # generated HTML/XML — .gitignore this
```
## Suite Conventions
```gdscript
# tests/unit/test_score_manager.gd
class_name TestScoreManager
extends GdUnitTestSuite
const ScoreManager := preload("res://scripts/core/score_manager.gd")
var _sm
func before_test() -> void:
_sm = ScoreManager.new() # auto-freed after each test
func test_add_score_increments() -> void:
_sm.add(10)
assert_int(_sm.score).is_equal(10)
func test_score_never_negative() -> void:
_sm.add(-5)
assert_int(_sm.score).is_equal(0)
func test_addition_is_parameterized(a: int, b: int, expected: int) -> void:
_sm.add(a + b)
assert_int(_sm.score).is_equal(expected) # supply via test-suite args or use fuzzers
```
Assertion helpers to know: `assert_int`, `assert_float`, `assert_str`, `assert_bool`,
`assert_that(expr)`, `assert_array`, `assert_dict`, `assert_object(obj).is_instanceof(...)`,
`assert_signal(spy).is_emitted("name")`. Full list in gdUnit4 docs.
## Async & Scene Tests
Await signals/frames inside tests — essential for scene-dependent logic:
```gdscript
func test_enemy_dies_after_hit() -> void:
var enemy := auto_free(load("res://scenes/entities/enemy.tscn").instantiate())
scene_runner().add_child(enemy) # scene_runner() gives a managed tree context
await get_tree().process_frame
enemy.take_damage(enemy.hp)
await assert_signal(enemy).is_emitted("died")
```
Keep rendering-dependent asserts out of headless runs: no pixel/screenshot checks in CI.
## Running From CLI
```bash
# everything under res://tests
addons/gdUnit4/runtest.cmd -a res://tests --continue # Windows
./addons/gdUnit4/runtest.sh -a res://tests --continue # Linux/macOS
# one suite only
addons/gdUnit4/runtest.cmd -a tests/unit/test_score_manager.gd
# filter by name pattern; stop-on-first-failure for fast loops
addons/gdUnit4/runtest.cmd -a res://tests --include test_score --no-failure
```
- Exit code 0 = all passed; non-zero = failures → CI/agent gate safe
- Reports: `res://reports/` (HTML + XML/JUnit)
- Runner is headless by default when no display is available
## Agent Workflow Integration
1. Write the failing test FIRST when fixing a bug (reproduce via test, then fix)
2. Run only the touched suite while iterating (`--include`), full suite before commit
3. If a test is flaky (async timing), fix the race condition — never delete the assertion
4. New feature ⇒ new suite; refactoring ⇒ tests unchanged, code changes
## Common Pitfalls
| Pitfall | Fix |
|---|---|
| Test class missing `class_name` / wrong base class | Must be `extends GdUnitTestSuite` + `class_name Test*` |
| Asserting inside awaited coroutines after timeout | Use `await assert_signal(...)` helpers, not manual timers |
| Scene loads but `_ready` errors | Run smoke test (`SKILL.md §Smoke run`) — it surfaces these |
| Tests pass alone, fail together | Suites sharing autoload state → reset in `before_test()` |
| Adding tests without enabling addon | `--import` + check plugin enabled in project.godot |