Test Helpers logo

Test Helpers

CommunityPopular
Donchitos
test-helpers

Generate engine-specific test helper libraries — assertion utilities, factory functions, mocks in the engine's test folder. Reduces boilerplate.

Overview

PublisherDonchitos
RepositoryClaude-Code-Game-Studios
Skill nametest-helpers
Stars
25.8K
Forks
3.7K
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by Donchitos on GitHub. Read the source before you install it.

Installation

Install the Test Helpers AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/Donchitos/Claude-Code-Game-Studios/tree/main/.claude/skills/test-helpers
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/Donchitos/Claude-Code-Game-Studios.git /tmp/Claude-Code-Game-Studios
mkdir -p .claude/skills
cp -r /tmp/Claude-Code-Game-Studios/.claude/skills/test-helpers .claude/skills/test-helpers
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Test Helpers in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Test Helpers on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Test Helpers is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

!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):

EngineHelper root
Godottests/helpers/
UnityAssets/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.
UnrealSource/<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
  • 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.

Frequently asked questions

What does the Test Helpers AI skill do?

Generate engine-specific test helper libraries — assertion utilities, factory functions, mocks in the engine's test folder. Reduces boilerplate.

Why use Test Helpers on TypingMind?

Because you install it once and use it with any model. Test Helpers is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Test Helpers in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/Donchitos/Claude-Code-Game-Studios/tree/main/.claude/skills/test-helpers. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Test Helpers?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Test Helpers?

As many as you like. As long as a model supports skills, you can use Test Helpers with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Test Helpers AI skill free?

Yes. It is published on GitHub by Donchitos under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇