test-helpers

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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:
tests/helpers/
directory with engine-specific helper files
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

编写测试用例时,将常见的设置、清理和断言模式抽象为辅助工具,能让编写过程更快、更一致。本Skill会生成一个适配项目实际引擎、语言和系统的
tests/helpers/
库——这样每位开发者都能减少样板代码的编写,专注于断言逻辑。
输出:
tests/helpers/
目录,包含特定于引擎的辅助工具文件
运行时机:
  • /test-setup
    搭建好测试框架后首次运行
  • 当多个测试文件重复相同的设置样板代码时
  • 开始为新系统编写测试时

1. Parse Arguments

1. 解析参数

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

模式:
  • /test-helpers [system-name]
    — 为特定系统生成辅助工具(例如:
    /test-helpers combat
  • /test-helpers all
    — 为所有包含测试文件的系统生成辅助工具
  • /test-helpers scaffold
    — 仅生成基础辅助工具库(无特定系统的辅助工具);首次运行时使用此模式
  • 无参数 — 若不存在辅助工具则运行
    scaffold
    模式,否则运行
    all
    模式

2. Detect Engine and Language

2. 检测引擎与语言

Read
.claude/docs/technical-preferences.md
and extract:
  • Engine:
    value
  • Language:
    value
  • Framework:
    from the Testing section
If engine is not configured: "Engine not configured. Run
/setup-engine
first."

读取
.claude/docs/technical-preferences.md
并提取:
  • Engine:
    的值
  • Language:
    的值
  • 测试部分的
    Framework:
若未配置引擎:“未配置引擎,请先运行
/setup-engine
。”

3. Load Existing Test Patterns

3. 加载现有测试模式

Scan the test directory for patterns already in use:
Glob pattern="tests/**/*_test.*" (all test files)
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

扫描测试目录,查找已在使用的测试模式:
Glob pattern="tests/**/*_test.*" (所有测试文件)
选取代表性样本(最多5个文件),读取测试文件并提取:
  • 设置模式(
    before_each
    /
    setUp
    /fixtures的编写方式)
  • 常见断言模式(最常被断言的内容)
  • 对象创建模式(测试中游戏对象或场景的实例化方式)
  • 模拟/存根模式(依赖项的替换方式)
这能确保生成的辅助工具匹配项目现有风格,而非通用模板。
同时读取:
  • design/gdd/systems-index.md
    — 了解系统列表
  • 相关GDD文档 — 明确需要测试的数据类型和值
  • docs/architecture/tr-registry.yaml
    — 将需求映射到被测系统

4. Generate Engine-Specific Helpers

4. 生成特定于引擎的辅助工具

Godot 4 (GDUnit4 / GDScript)

Godot 4 (GDUnit4 / GDScript)

Base helper (
tests/helpers/game_assertions.gd
):
gdscript
undefined
基础辅助工具 (
tests/helpers/game_assertions.gd
):
gdscript
undefined

Game-specific assertion utilities for [Project Name] tests.

Game-specific assertion utilities for [Project Name] tests.

Extends GdUnitAssertions with domain-specific helpers.

Extends GdUnitAssertions with domain-specific helpers.

Usage:

Usage:

var assert = GameAssertions.new()

var assert = GameAssertions.new()

assert.health_in_range(entity, 0, entity.max_health)

assert.health_in_range(entity, 0, entity.max_health)

class_name GameAssertions extends RefCounted
class_name GameAssertions extends RefCounted

Assert a value is within the inclusive range [min_val, max_val].

Assert a value is within the inclusive range [min_val, max_val].

Use for any formula output that has defined bounds in a GDD.

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: assert( value >= min_val and value <= max_val, "%s %.2f is outside expected range [%.2f, %.2f]" % [label, value, min_val, max_val] )
static func assert_in_range( value: float, min_val: float, max_val: float, label: String = "value" ) -> void: assert( 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.

Assert a signal was emitted during a callable block.

Usage: assert_signal_emitted(entity, "health_changed", func(): entity.take_damage(10))

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: var emitted := false obj.connect(signal_name, func(_args): emitted = true) action.call() assert(emitted, "Expected signal '%s' to be emitted, but it was not." % signal_name)
static func assert_signal_emitted( obj: Object, signal_name: String, action: Callable ) -> void: var emitted := false obj.connect(signal_name, func(_args): emitted = true) action.call() assert(emitted, "Expected signal '%s' to be emitted, but it was not." % signal_name)

Assert that a callable does NOT emit a signal.

Assert that a callable does NOT emit a signal.

static func assert_signal_not_emitted( obj: Object, signal_name: String, action: Callable ) -> void: var emitted := false obj.connect(signal_name, func(_args): emitted = true) action.call() assert(not emitted, "Expected signal '%s' NOT to be emitted, but it was." % signal_name)
static func assert_signal_not_emitted( obj: Object, signal_name: String, action: Callable ) -> void: var emitted := false obj.connect(signal_name, func(_args): emitted = true) action.call() assert(not emitted, "Expected signal '%s' NOT to be emitted, but it was." % signal_name)

Assert a node exists at path within a parent.

Assert a node exists at path within a parent.

static func assert_node_exists(parent: Node, path: NodePath) -> void: assert( parent.has_node(path), "Expected node at path '%s' to exist." % str(path) )

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

```gdscript
static func assert_node_exists(parent: Node, path: NodePath) -> void: assert( parent.has_node(path), "Expected node at path '%s' to exist." % str(path) )

**工厂辅助工具** (`tests/helpers/game_factory.gd`):

```gdscript

Factory functions for creating test game objects.

Factory functions for creating test game objects.

Returns minimal objects configured for unit testing (no scene tree required).

Returns minimal objects configured for unit testing (no scene tree required).

Usage: var player = GameFactory.make_player(health: 100)

Usage: var player = GameFactory.make_player(health: 100)

class_name GameFactory extends RefCounted
class_name GameFactory extends RefCounted

Create a minimal player-like object for testing.

Create a minimal player-like object for testing.

Override fields as needed.

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
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

**场景辅助工具** (`tests/helpers/scene_runner_helper.gd`):

```gdscript

Utilities for scene-based integration tests.

Utilities for scene-based integration tests.

Wraps GdUnitSceneRunner for common patterns.

Wraps GdUnitSceneRunner for common patterns.

class_name SceneRunnerHelper extends GdUnitTestSuite
class_name SceneRunnerHelper extends GdUnitTestSuite

Load a scene and wait one frame for _ready() to complete.

Load a scene and wait one frame for _ready() to complete.

func load_scene_and_wait(scene_path: String) -> Node: var scene = load(scene_path).instantiate() add_child(scene) await get_tree().process_frame return scene

---
func load_scene_and_wait(scene_path: String) -> Node: var scene = load(scene_path).instantiate() add_child(scene) await get_tree().process_frame return scene

---

Unity (NUnit / C#)

Unity (NUnit / C#)

Base helper (
tests/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 (
tests/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>();
    }
}

基础辅助工具 (
tests/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}.");
    }
}
工厂辅助工具 (
tests/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++)

Unreal Engine (C++)

Base helper (
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;
    }
}

基础辅助工具 (
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

5. 生成特定于系统的辅助工具

For
[system-name]
or
all
modes, generate a helper per system:
Read the system's GDD to 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
undefined
对于
[system-name]
all
模式,为每个系统生成一个辅助工具:
读取系统的GDD文档,提取:
  • 数据类型(实体类型、组件名称)
  • 公式变量及其范围
  • 边缘用例中提到的常见测试场景
生成
tests/helpers/[system]_factory.[ext]
,包含该系统对象专属的工厂函数。
combat
系统的示例模式(Godot/GDScript):
gdscript
undefined

Factory and assertion helpers for Combat system tests.

Factory and assertion helpers for Combat system tests.

Generated by /test-helpers combat on [date].

Generated by /test-helpers combat on [date].

Based on: design/gdd/combat.md

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
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.

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
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.

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
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.

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")

---
static func assert_damage_in_bounds(damage: float) -> void: GameAssertions.assert_in_range(damage, DAMAGE_MIN, DAMAGE_MAX, "damage")

---

6. Write Output

6. 写入输出

Present a summary of what will be created:
undefined
展示即将创建的内容摘要:
undefined

Test Helpers to Create

待创建的测试辅助工具

Base helpers (engine: [engine]):
  • tests/helpers/game_assertions.[ext]
  • tests/helpers/game_factory.[ext] [engine-specific extras]
System helpers ([mode]):
  • tests/helpers/[system]_factory.[ext] ← from [system] GDD

Ask: "May I write these helper files to `tests/helpers/`?"

**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\"`"

---
基础辅助工具(引擎:[engine]):
  • tests/helpers/game_assertions.[ext]
  • tests/helpers/game_factory.[ext] [特定于引擎的额外文件]
系统辅助工具(模式:[mode]):
  • tests/helpers/[system]_factory.[ext] ← 来自[system]的GDD文档

询问:“是否可以将这些辅助工具文件写入`tests/helpers/`?”

**切勿覆盖现有文件**。若文件已存在,提示:
“跳过`[path]`——文件已存在。若需重新生成,请手动删除该文件。”

写入完成后:结果:**完成**——辅助工具文件已创建。

“辅助工具文件已创建。在测试中使用的方式:
- Godot:`class_name`会自动导入——无需显式导入
- Unity:添加`using`指令或引用测试程序集
- Unreal:`#include "tests/helpers/GameTestHelpers.h"`”

---

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/
  • 切勿覆盖现有辅助工具——它们可能包含手写的自定义内容。仅生成不存在的新文件
  • 生成的代码仅作为起点——生成的工厂函数为简化使用了元数据模式;代码结构确定后,可根据实际类结构调整
  • 辅助工具需匹配GDD文档——辅助工具中的范围和常量应源自GDD的公式部分,而非自定义值
  • 写入前需确认——在
    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.
  • 若测试框架尚未搭建,请运行
    /test-setup
  • 使用
    /dev-story
    实现需求——辅助工具能减少新测试文件中的样板代码。
  • 运行
    /skill-test
    验证其他可能需要辅助工具支持的Skill。