Skip to main content
skill CX Codex

test-helpers — Claude Code skill: engine-specific test helpers

Generates a test helper library for your engine: assertion utilities, factory functions, mocks. Useful when the same setup block keeps getting copied between test files.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add Donchitos/Claude-Code-Game-Studios --skill test-helpers -a claude-code
Open on GitHub

What test-helpers does

The skill reads project.yaml to find the engine, language and test framework. Then it scans the tests you already have and pulls out the patterns in use — setup, assertions, object creation, dependency stubbing — so the generated helpers match your style instead of a generic template.

Where the files land depends on the engine. Godot gets tests/helpers/, Unity gets Assets/Tests/EditMode/Helpers/ inside the EditModeTests assembly, Unreal gets Source/<Module>/Private/Tests/Helpers/. That is not arbitrary: those are the folders where the test runner actually compiles helpers. Put them elsewhere and the runner never sees them.

The assertion rule is strict. Helpers must assert through the configured framework's API, never through a bare assert(). A bare assert() aborts the run at the first failure and hides every result after it, and it gets stripped from release builds — so the tests still "pass" with nothing checked. The skill will not guess the call form either: if it cannot confirm the framework's API, it says so and generates no helper rather than inventing one. A configured engine is required up front; without it the skill asks you to run /setup-engine first.

Modes: /test-helpers combat for one system, /test-helpers all for every system with test files, /test-helpers scaffold for the base library only. With no argument it picks scaffold when no helpers exist yet, otherwise all.

What it helps with

  • build an assertion library for a specific engine
  • strip repeated setup boilerplate out of test files
  • create factories for game objects and scenes
  • replace dependencies with mocks in tests
  • match helper style to tests already written

Example prompts for the agent

  • /test-helpers scaffold
  • /test-helpers combat
  • /test-helpers all

How to install test-helpers

1Skills CLI (Vercel)

npx skills add Donchitos/Claude-Code-Game-Studios --skill test-helpers -a claude-code

2Manually: put the files in place

~/.claude/skills/test-helpers/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/test-helpers/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about test-helpers

How do I install test-helpers in Claude?

Run npx skills add Donchitos/Claude-Code-Game-Studios --skill test-helpers -a claude-code. The skill lands in ~/.claude/skills/test-helpers/SKILL.md.

What does test-helpers do?

It generates test helpers for your project's engine: assertion utilities, factory functions and mocks. The files go into the folder where the test runner compiles them.

What do I need before running it?

A configured engine in project.yaml or .claude/docs/technical-preferences.md. Without one, the skill asks you to run /setup-engine first.

Source text

SKILL.md · MIT
---
name: test-helpers
description: "Generate engine-specific test helper libraries — assertion utilities, factory functions, mocks in the engine's test folder. Reduces boilerplate."
argument-hint: "[system-name | all | scaffold]"
user-invocable: true
allowed-tools: Read, Glob, Grep, Write, Bash(bash "*/.claude/skills/test-helpers/../../hooks/yaml-helper.sh" resolve_config *)
model: sonnet
---

!`bash "${CLAUDE_SKILL_DIR}/../../hooks/yaml-helper.sh" resolve_config --keys automation`

**Automation mode**: Resolve `modes.automation` (`project.local.yaml` →
`project.yaml` → default `collaborative`). Every `AskUserQuestion` call and
every file write follows `.claude/docs/automation-modes.md`
(collaborative asks always · guided major-only · autonomous logs and proceeds;
`automation_always_ask` categories always prompt).

# Test Helpers

Writing test cases is faster and more consistent when common setup, teardown,
and assertion patterns are abstracted into helpers. This skill generates a
`tests/helpers/` library tailored to the project's actual engine, language,
and systems — so every developer writes less boilerplate and more assertions.

**Output:** engine-specific helper files in the engine's **helper root** —
the only place its test runner compiles them (`.claude/docs/directory-structure.md`):

| Engine | Helper root |
|---|---|
| Godot | `tests/helpers/` |
| Unity | `Assets/Tests/EditMode/Helpers/` — inside the `EditModeTests` assembly `/test-setup` creates. If PlayMode tests need the same helpers, ask before creating `Assets/Tests/Helpers/TestHelpers.asmdef` and referencing it from both test assemblies. |
| Unreal | `Source/<Module>/Private/Tests/Helpers/` |

`tests/helpers/` below means the helper root for the project's engine.

**When to run:**
- After `/test-setup` scaffolds the framework (first time)
- When multiple test files repeat the same setup boilerplate
- When starting to write tests for a new system

---

## Assertions: use the project's test framework, never bare `assert()`

**Generated helpers must assert through the configured test framework's assertion
API**, resolved from `testing.framework` — not through GDScript's built-in
`assert()`.

Two things go wrong with a bare `assert()` in a helper, and both are silent:

1. **It aborts the run instead of failing the test.** A framework assertion
   records a failure and continues, so one broken expectation yields one red test.
   A bare `assert()` halts execution, so the first failure hides every result
   after it.
2. **It is stripped in release builds.** A helper library built on `assert()`
   stops asserting entirely in exactly the build you most want checked, and does
   so without any error — the tests still "pass".

**Look up the assertion form for the configured framework before generating
code.** Do not copy the forms in this file or in other skills' examples as
authoritative — they are illustrative and have drifted (`assert_eq`,
`assert_true` and `assert_that` all appear across the repo). If you cannot
confirm the correct API for the project's framework, say so and generate no
helper rather than guessing.

**Helpers must not extend the framework's test-suite base class.** A file in
`tests/helpers/` that extends the suite type is discovered by the runner as a
test suite containing zero tests. Helpers are plain classes; only real test files
extend the suite.

---

## 1. Parse Arguments

**Modes:**
- `/test-helpers [system-name]` — generate helpers for a specific system
  (e.g., `/test-helpers combat`)
- `/test-helpers all` — generate helpers for all systems with test files
- `/test-helpers scaffold` — generate only the base helper library (no
  system-specific helpers); use this on first run
- No argument — run `scaffold` if no helpers exist, else `all`

---

## 2. Detect Engine and Language

Read from `project.yaml` first, falling back to `.claude/docs/technical-preferences.md` for any key that is absent or empty:
- `engine.name` (else the `Engine:` value)
- `engine.language` (else the `Language:` value)
- `testing.framework` (else `Framework:` from the Testing section)

If the engine is not configured in either source: "Engine not configured. Run `/setup-engine` first."

---

## 3. Load Existing Test Patterns

Scan the engine's test root for patterns already in use (Godot `tests/`, Unity
`Assets/Tests/`, Unreal `Source/<Module>/Private/Tests/`):

```
Glob pattern="tests/**/*_test.*"                  (Godot)
Glob pattern="Assets/Tests/**/*Tests.cs"          (Unity)
Glob pattern="Source/*/Private/Tests/**/*.cpp"    (Unreal)
```

For a representative sample (up to 5 files), read the test files and extract:
- Setup patterns (how `before_each` / `setUp` / fixtures are written)
- Common assertion patterns (what is being asserted most often)
- Object creation patterns (how game objects or scenes are instantiated in tests)
- Mock/stub patterns (how dependencies are replaced)

This ensures generated helpers match the project's existing style, not a
generic template.

Also read:
- `design/gdd/systems-index.md` — to know which systems exist
- In-scope GDD(s) — to understand what data types and values need testing. Read
  them by section: find the headings you need with the Grep tool (pattern `^#`,
  content mode) and Read those ranges with offset and limit.
- `docs/architecture/tr-registry.yaml` — to map requirements to tested systems

---

## 4. Generate Engine-Specific Helpers

### Godot 4 (GDUnit4 / GDScript)

> **`FAIL_IF(...)` in the example below is a PLACEHOLDER, not an API.** It marks
> the one line you must resolve from the project's actual test framework before
> emitting any of this, per the rule in §2. Substitute the framework's real
> failure call — the form that **registers a failure with the runner and lets the
> suite continue** — and delete the marker comment.
>
> **It is deliberately not spelled `assert_that`, `assert_eq` or `assert_true`.**
> All three appear somewhere in this repo, they disagree, and **GdUnit4's API is
> not covered by `docs/engine-reference/`** — so writing any of them here would be
> asserting an API this repo cannot source, in the file whose whole job is to stop
> people doing that. If you cannot confirm the correct call for the configured
> framework, §2 already tells you the answer: **generate no helper.** An
> unresolved `FAIL_IF` reaching disk is a bug; a missing helper is a task.
>
> Worked examples must obey §2 too — a bare `assert()` here would be the exact
> form §2 forbids, sixty lines below §2 forbidding it. A rule stated in prose
> does not reach its own worked example unless someone makes it.

**Base helper** (`tests/helpers/game_assertions.gd`):

```gdscript
## Game-specific assertion utilities for [Project Name] tests.
## Domain-specific helpers built on the project's test framework.
##
## Usage (static — no instance needed):
##   GameAssertions.assert_in_range(entity.health, 0, entity.max_health, "health")

class_name GameAssertions
extends RefCounted

## Assert a value is within the inclusive range [min_val, max_val].
## Use for any formula output that has defined bounds in a GDD.
static func assert_in_range(
    value: float,
    min_val: float,
    max_val: float,
    label: String = "value"
) -> void:
    # FRAMEWORK ASSERT — resolve per the rule above; do not emit `assert()`.
    FAIL_IF(
        not (value >= min_val and value <= max_val),
        "%s %.2f is outside expected range [%.2f, %.2f]" % [label, value, min_val, max_val]
    )

## Assert a signal was emitted during a callable block.
## Usage: assert_signal_emitted(entity, "health_changed", func(): entity.take_damage(10))
static func assert_signal_emitted(
    obj: Object,
    signal_name: String,
    action: Callable
) -> void:
    # A lambda captures a local by value: `emitted = true` inside it would set
    # the lambda's own copy. Mutate a Dictionary it shares instead. `...args`
    # (4.5+) accepts a signal with any number of arguments.
    var state := {"emitted": false}
    obj.connect(signal_name, func(...args): state.emitted = true)
    action.call()
    # FRAMEWORK ASSERT — resolve per the rule above; do not emit `assert()`.
    FAIL_IF(not state.emitted, "Expected signal '%s' to be emitted, but it was not." % signal_name)

## Assert that a callable does NOT emit a signal.
static func assert_signal_not_emitted(
    obj: Object,
    signal_name: String,
    action: Callable
) -> void:
    var state := {"emitted": false}  # a Dictionary, as above
    obj.connect(signal_name, func(...args): state.emitted = true)
    action.call()
    # FRAMEWORK ASSERT — resolve per the rule above; do not emit `assert()`.
    FAIL_IF(state.emitted, "Expected signal '%s' NOT to be emitted, but it was." % signal_name)

## Assert a node exists at path within a parent.
static func assert_node_exists(parent: Node, path: NodePath) -> void:
    # FRAMEWORK ASSERT — resolve per the rule above; do not emit `assert()`.
    FAIL_IF(
        not parent.has_node(path),
        "Expected node at path '%s' to exist." % str(path)
    )
```

**Factory helper** (`tests/helpers/game_factory.gd`):

```gdscript
## Factory functions for creating test game objects.
## Returns minimal objects configured for unit testing (no scene tree required).
##
## Usage: var player = GameFactory.make_player(health: 100)

class_name GameFactory
extends RefCounted

## Create a minimal player-like object for testing.
## Override fields as needed.
static func make_player(health: int = 100) -> Node:
    var player = Node.new()
    player.set_meta("health", health)
    player.set_meta("max_health", health)
    return player
```

**Scene helper** (`tests/helpers/scene_runner_helper.gd`):

```gdscript
## Utilities for scene-based integration tests. For input simulation and frame
## stepping, use gdUnit4's own `scene_runner()` from inside the suite.

class_name SceneRunnerHelper
extends RefCounted

## Load a scene under `host` — the calling test suite, which passes `self` —
## and wait one frame for _ready() to complete. A plain class, so the runner
## never discovers this file as a suite with zero tests.
static func load_scene_and_wait(host: Node, scene_path: String) -> Node:
    var scene: Node = load(scene_path).instantiate()
    host.add_child(scene)
    await host.get_tree().process_frame
    return scene
```

---

### Unity (NUnit / C#)

**Base helper** (`Assets/Tests/EditMode/Helpers/GameAssertions.cs`):

```csharp
using NUnit.Framework;
using UnityEngine;

/// <summary>
/// Game-specific assertion utilities for [Project Name] tests.
/// Extends NUnit's Assert with domain-specific helpers.
/// </summary>
public static class GameAssertions
{
    /// <summary>
    /// Assert a value is within an inclusive range [min, max].
    /// Use for any formula output defined in GDD Formulas sections.
    /// </summary>
    public static void AssertInRange(float value, float min, float max, string label = "value")
    {
        Assert.That(value, Is.InRange(min, max),
            $"{label} ({value:F2}) is outside expected range [{min:F2}, {max:F2}]");
    }

    /// <summary>Assert a UnityEvent or C# event was raised during an action.</summary>
    public static void AssertEventRaised(ref bool wasCalled, System.Action action, string eventName)
    {
        wasCalled = false;
        action();
        Assert.IsTrue(wasCalled, $"Expected event '{eventName}' to be raised, but it was not.");
    }

    /// <summary>Assert a component exists on a GameObject.</summary>
    public static void AssertHasComponent<T>(GameObject obj) where T : Component
    {
        var component = obj.GetComponent<T>();
        Assert.IsNotNull(component,
            $"Expected GameObject '{obj.name}' to have component {typeof(T).Name}.");
    }
}
```

**Factory helper** (`Assets/Tests/EditMode/Helpers/GameFactory.cs`):

```csharp
using UnityEngine;

/// <summary>
/// Factory methods for creating minimal test objects without loading scenes.
/// </summary>
public static class GameFactory
{
    /// <summary>Create a minimal GameObject with a named component for testing.</summary>
    public static GameObject MakeGameObject(string name = "TestObject")
    {
        var go = new GameObject(name);
        return go;
    }

    /// <summary>
    /// Create a ScriptableObject of type T for data-driven tests.
    /// Dispose with Object.DestroyImmediate after test.
    /// </summary>
    public static T MakeScriptableObject<T>() where T : ScriptableObject
    {
        return ScriptableObject.CreateInstance<T>();
    }
}
```

---

### Unreal Engine (C++)

**Base helper** (`Source/<Module>/Private/Tests/Helpers/GameTestHelpers.h`):

```cpp
#pragma once

#include "CoreMinimal.h"
#include "Misc/AutomationTest.h"

/**
 * Game-specific assertion macros and helpers for [Project Name] automation tests.
 * Include in any test file that needs domain-specific assertions.
 *
 * Usage:
 *   GAME_TEST_ASSERT_IN_RANGE(TestName, DamageValue, 10.0f, 50.0f, TEXT("Damage"));
 */

// Assert a float value is within inclusive range [Min, Max]
#define GAME_TEST_ASSERT_IN_RANGE(TestName, Value, Min, Max, Label) \
    TestTrue( \
        FString::Printf(TEXT("%s (%.2f) in range [%.2f, %.2f]"), Label, Value, Min, Max), \
        (Value) >= (Min) && (Value) <= (Max) \
    )

// Assert a UObject pointer is valid (not null, not garbage collected)
#define GAME_TEST_ASSERT_VALID(TestName, Ptr, Label) \
    TestTrue( \
        FString::Printf(TEXT("%s is valid"), Label), \
        IsValid(Ptr) \
    )

// Assert an Actor is in the world (spawned successfully)
#define GAME_TEST_ASSERT_SPAWNED(TestName, ActorPtr, ClassName) \
    TestNotNull( \
        FString::Printf(TEXT("Spawned actor of class %s"), TEXT(#ClassName)), \
        ActorPtr \
    )

/**
 * Helper to create a minimal test world.
 * Remember to call World->DestroyWorld(false) in teardown.
 */
namespace GameTestHelpers
{
    inline UWorld* CreateTestWorld(const FString& WorldName = TEXT("TestWorld"))
    {
        UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
        FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
        WorldContext.SetCurrentWorld(World);
        return World;
    }
}
```

---

## 5. Generate System-Specific Helpers

For `[system-name]` or `all` modes, generate a helper per system:

Read only the GDD sections this needs — `Grep pattern="^## ([0-9]+\. )?(Formulas|Edge Cases|Detailed Rules|Detailed Design)" path="design/gdd/[system].md" output_mode="content" -A 30` (the optional number matches a reverse-documented GDD's `## 4. Formulas`) — rather than a full read (every peer skill section-greps GDDs; in `all` mode a full read multiplies across each system's 400+-line GDD). Extract:
- Data types (entity types, component names)
- Formula variables and their bounds
- Common test scenarios mentioned in Edge Cases

Generate `tests/helpers/[system]_factory.[ext]` with factory functions
specific to that system's objects.

Example pattern for a `combat` system (Godot/GDScript):

```gdscript
## Factory and assertion helpers for Combat system tests.
## Generated by /test-helpers combat on [date].
## Based on: design/gdd/combat.md

class_name CombatTestFactory
extends RefCounted

const DAMAGE_MIN := 0
const DAMAGE_MAX := 999  # From GDD: damage formula upper bound

## Create a minimal attacker object for damage formula tests.
static func make_attacker(attack: float = 10.0, crit_chance: float = 0.0) -> Node:
    var attacker = Node.new()
    attacker.set_meta("attack", attack)
    attacker.set_meta("crit_chance", crit_chance)
    return attacker

## Create a minimal target object for damage receive tests.
static func make_target(defense: float = 0.0, health: float = 100.0) -> Node:
    var target = Node.new()
    target.set_meta("defense", defense)
    target.set_meta("health", health)
    target.set_meta("max_health", health)
    return target

## Assert damage output is within GDD-specified bounds.
static func assert_damage_in_bounds(damage: float) -> void:
    GameAssertions.assert_in_range(damage, DAMAGE_MIN, DAMAGE_MAX, "damage")
```

---

## 6. Write Output

Present a summary of what will be created:

```
## Test Helpers to Create

Base helpers (engine: [engine], helper root: [helper root]):
- [helper root]/game_assertions.[ext]
- [helper root]/game_factory.[ext]
[engine-specific extras]

System helpers ([mode]):
- [helper root]/[system]_factory.[ext]  ← from [system] GDD
```

Ask: "May I write these helper files to `[helper root]`?"

**Never overwrite existing files.** If a file already exists, report:
"Skipping `[path]` — already exists. Remove the file manually if you want it
regenerated."

After writing: Verdict: **COMPLETE** — helper files created.

"Helper files created. To use them in a test:
- Godot: `class_name` is auto-imported — no explicit import needed
- Unity: Add `using` directive or reference the test assembly
- Unreal: `#include \"Tests/Helpers/GameTestHelpers.h\"` (from inside the module)"

---

## Collaborative Protocol

- **Never overwrite existing helpers** — they may contain hand-written
  customisations. Only generate new files that don't exist yet
- **Generated code is a starting point** — the generated factory functions use
  metadata patterns for simplicity; adapt to the actual class structure once
  the code exists
- **Helpers should reflect the GDD** — bounds and constants in helpers should
  trace to GDD Formulas sections, not invented values
- **Ask before writing** — always confirm before creating files in `tests/`

## Next Steps

- Run `/test-setup` if the test framework has not been scaffolded yet.
- Use `/dev-story` to implement stories — helpers reduce boilerplate in new test files.
- Run `/skill-test` to validate other skills that may need helper coverage.

© Donchitos · MIT · Donchitos/Claude-Code-Game-Studios

Similar skills

All skills for Testing and QA →

From the same set72

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor