# Tests Headless, deterministic tests that run in CI (`.github/workflows/ci.yml`) and locally via `bash run_tests.sh`. Nothing here needs a GPU — even the UI checks assert computed geometry instead of comparing pixels, so the whole suite runs in plain headless Godot. ## Run ```bash bash run_tests.sh # lint + every headless test (+ gameplay assertions) bash run_tests.sh --no-gameplay # skip the slower full-scene gameplay pass (what CI runs) # a single test: godot --headless --script tests/.gd ``` Set `GODOT` to point at a specific binary: `GODOT=/path/to/godot bash run_tests.sh`. ## The six layers Order is cheapest first. The middle layers are where "don't break interactions/UI by accident" is actually won — none of them need a screenshot. | Layer | What it drives | Example here | |---|---|---| | **Scene smoke** | Instantiate a scene headless, assert no errors | `level_switch_test.gd` (level modules load, no geometry leaks) | | **Unit / logic** | Pure functions, no scene tree | `logic_test.gd`; `harness_selftest.gd` (proves `test_helpers.gd`) | | **Signal contract** | Emit a signal, assert args/count | *to add:* `died` / `health_changed` / `all_defeated` fire correctly | | **Interaction** | Feed synthetic input, step frames, assert state | `touch_controls_test.gd`, `matador_charge_test.gd` | | **Scenario / replay** | Scripted inputs + fixed seed over a whole fight | `difficulty_sim.gd` (a report), `bear_boss_test.gd` | | **UI layout** | Resize the viewport, query `Control` rects | `ui_layout_test.gd` (on-screen, text fits, tap size, scroll) | ## Shared harness `test_helpers.gd` (`class_name TestHelpers`) holds the primitives every test used to re-implement: - `TestHelpers.make_rng(seed)` — a **seeded** `RandomNumberGenerator`. Determinism is the precondition for the interaction/scenario layers; seed the RNG here instead of the global `randf()`/`randi()` so a test replays identically. (Reworking gameplay code to accept an injected RNG is tracked in the refactor plan's Workstream 2.) - `TestHelpers.approx(a, b, eps)` / `vec_approx(a, b, eps)` — float/vector comparisons with tolerance, because computed positions never compare exactly equal. - A pass/fail tally: `t.ok(cond, msg)` and `t.report()`, which prints `Results: X passed, Y failed` and returns the process exit code. New tests follow the existing idiom: `extends SceneTree`, `_init()` → `_run.call_deferred()`, print ` PASS:` / ` FAIL:`, and `quit(t.report())`. ## Next step: GdUnit4 The hand-rolled harness is enough for CI today. The plan's Workstream 5 adopts [GdUnit4](https://github.com/MikeSchulze/gdUnit4) for a fluent assertion API, its **scene runner** (synthetic input + frame stepping — purpose-built for the interaction layer), and HTML/JUnit reports. Adoption is incremental, so the current tests keep running throughout: 1. Add it under `addons/gdUnit4` (AssetLib in-editor, or a git submodule) and enable the plugin in Project Settings → Plugins. 2. Point CI at GdUnit4's own GitHub Action (it publishes the JUnit report to the PR) alongside the existing `run_tests.sh` step. 3. Port suites one at a time — `test_helpers.gd`'s primitives map directly onto GdUnit4 assertions, so a ported test is smaller, not larger. Do **not** vendor GdUnit4 into this repo by hand; install it through AssetLib or a submodule.