test-helpers について
test-helpersは、ゲームプロジェクト向けのテスト用ヘルパーコードを生成するスキルです。プロジェクトのエンジン、言語、テストフレームワーク、既存テストのパターンに合わせ、共通およびシステム別のヘルパーを作成し、書き込み前に確認を求めます。
主な機能
- 01エンジンに合わせたヘルパー生成
- 02設定済みアサーションAPIの使用
- 03既存テストからパターンを抽出
- 04システム別ファクトリーヘルパーの作成
- 05既存ファイルの上書き回避
活用シーン
- 01テスト環境の構築後にヘルパーライブラリを作成
- 02繰り返し使うテスト初期化の共通化
- 03新しいゲームシステムのテスト用ヘルパー作成
- 04プロジェクトのフレームワークに合うアサーションヘルパー生成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ donchitos/ claude-code-game-studios --skill test-helpersインストール後、Claude Code で/test-helpersを入力して読み込まれたか確認します。
詳細
!bash "${CLAUDE_SKILL_DIR}/../../hooks/yaml-helper.sh" resolve_config --keys automation
自動化モード:modes.automation を解決します(project.local.yaml →
project.yaml → デフォルトの collaborative)。すべての AskUserQuestion 呼び出しと
すべてのファイル書き込みは .claude/docs/automation-modes.md に従います
(collaborative は常に質問 · guided は主要な項目のみ質問 · autonomous は記録して続行;
automation_always_ask カテゴリーでは常に確認します)。
テストヘルパー
共通のセットアップ、ティアダウン、アサーションのパターンをヘルパーにまとめることで、テストケースをより速く、一貫して作成できます。このスキルは、プロジェクトで実際に使用しているエンジン、言語、システムに合わせた tests/helpers/ ライブラリを生成します。これにより、すべての開発者が定型コードを減らし、アサーションを増やせます。
出力: エンジン固有のヘルパーファイルを、エンジンのヘルパールートに作成します。テストランナーがコンパイルするのはこの場所だけです(.claude/docs/directory-structure.md)。
| エンジン | ヘルパールート |
|---|---|
| Godot | tests/helpers/ |
| Unity | Assets/Tests/EditMode/Helpers/ — /test-setup が作成する EditModeTests アセンブリ内です。PlayMode テストでも同じヘルパーが必要な場合は、Assets/Tests/Helpers/TestHelpers.asmdef を作成し、両方のテストアセンブリから参照する前に確認してください。 |
| Unreal | Source/<Module>/Private/Tests/Helpers/ |
以下の tests/helpers/ は、プロジェクトのエンジンに対応するヘルパールートを指します。
実行するタイミング:
/test-setupでフレームワークを初めて構築した後- 複数のテストファイルで同じセットアップの定型コードが繰り返されている場合
- 新しいシステムのテストを書き始める場合
アサーション:プロジェクトのテストフレームワークを使用し、裸の assert() は使用しない
生成するヘルパーでは、GDScript の組み込み assert() ではなく、設定済みのテストフレームワークのアサーション API を通じてアサーションを実行してください。 API は testing.framework から解決します。
ヘルパーで裸の assert() を使うと、次の2つの問題が起こります。どちらもエラーが表示されません。
- テストを失敗させるのではなく、実行を中断します。 フレームワークのアサーションは失敗を記録して続行するため、期待値が1つ外れても赤くなるテストは1つだけです。裸の
assert()は実行を停止するため、最初の失敗によって、その後の結果がすべて確認できなくなります。 - リリースビルドでは取り除かれます。
assert()に依存するヘルパーライブラリは、最も確認したいビルドでアサーションを完全に実行しなくなり、しかもエラーは表示されません。テストは「成功」したままです。
コードを生成する前に、設定済みフレームワークで使用するアサーションの形式を調べてください。 このファイルや他のスキルの例にある形式を正しいものとしてそのまま使わないでください。これらはあくまで例であり、現在の API と異なる場合があります(リポジトリ内には assert_eq、assert_true、assert_that がすべて登場します)。プロジェクトのフレームワークで正しい API を確認できない場合は、その旨を伝え、推測してヘルパーを生成するのではなく、生成しないでください。
ヘルパーでフレームワークのテストスイート基底クラスを継承しないでください。 tests/helpers/ 内のファイルがスイート型を継承すると、テストランナーによってテストが0件のテストスイートとして検出されます。ヘルパーは通常のクラスにし、実際のテストファイルだけでスイートを継承してください。
1. 引数を解析する
モード:
/test-helpers [system-name]— 特定のシステム用のヘルパーを生成します (例:/test-helpers combat)/test-helpers all— テストファイルがあるすべてのシステム用のヘルパーを生成します/test-helpers scaffold— 基本のヘルパーライブラリのみを生成します(システム固有のヘルパーは含みません)。初回実行時に使用してください- 引数なし — ヘルパーが存在しない場合は
scaffoldを、存在する場合はallを実行します
2. エンジンと言語を検出する
まず project.yaml を読み込み、キーが存在しない、または空の場合は .claude/docs/technical-preferences.md を参照します。
engine.name(なければEngine:の値)engine.language(なければLanguage:の値)testing.framework(なければ Testing セクションのFramework:)
どちらのソースにもエンジンの設定がない場合:"Engine not configured. Run /setup-engine first."
3. 既存のテストパターンを読み込む
エンジンのテストルートをスキャンし、すでに使われているパターンを確認します(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)
代表的なサンプルとして最大 5 ファイルを選び、テストファイルを読み込んで、次の項目を抽出します:
- セットアップのパターン(
before_each/setUp/フィクスチャの記述方法) - 共通のアサーションパターン(最も頻繁にアサートされる内容)
- オブジェクト作成のパターン(テストでゲームオブジェクトやシーンをインスタンス化する方法)
- モック/スタブのパターン(依存関係の置き換え方法)
これにより、生成されるヘルパーが汎用テンプレートではなく、プロジェクト既存のスタイルに合うようになります。
以下も読みます:
design/gdd/systems-index.md— 存在するシステムを把握します- 対象範囲の GDD — テストが必要なデータ型と値を把握します。セクションごとに読みます。Grep ツールで必要な見出し(パターン
^#、content モード)を探し、offset と limit を指定して該当範囲を Read します。 docs/architecture/tr-registry.yaml— 要件をテスト対象のシステムに対応付けます
4. エンジン固有のヘルパーを生成する
Godot 4(GDUnit4/GDScript)
以下の例にある
FAIL_IF(...)は API ではなく、プレースホルダーです。 §2 のルールに従って、これを出力する前に、プロジェクトで実際に使われているテストフレームワークに基づき、この 1 行を確定してください。フレームワークの実際の失敗呼び出しに置き換えます。これは、スイートを続行させながらランナーに失敗を記録する形式です。その後、マーカーコメントを削除します。意図的に
assert_that、assert_eq、assert_trueとは表記していません。 このリポジトリではいずれもどこかで使われていますが、互いに一致しておらず、**GdUnit4 の API はdocs/engine-reference/に記載されていません。**そのため、ここにこれらのいずれかを書くことは、このリポジトリでは根拠を確認できない API を、この作業全体がまさにそうした記述を防ぐためのファイルで断定することになります。設定済みのフレームワークで正しい呼び出しを確認できない場合、§2 に答えがすでに示されています。ヘルパーは生成しないでください。 未解決のFAIL_IFがディスクに書き込まれるのは不具合ですが、ヘルパーがないことはタスクです。記述例も §2 に従う必要があります。ここで単独の
assert()を使うのは、§2 が禁じている形式を、§2 が禁じている箇所から 60 行下に記述することになります。ルールを文章で示しても、誰かが反映しない限り、そのルールは記述例自体には適用されません。
基本ヘルパー(tests/helpers/game_assertions.gd):
## 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)
)
ファクトリーヘルパー(tests/helpers/game_factory.gd):
## 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
シーンヘルパー(tests/helpers/scene_runner_helper.gd):
## 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#)
基本ヘルパー(Assets/Tests/EditMode/Helpers/GameAssertions.cs):
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}.");
}
}
ファクトリーヘルパー(Assets/Tests/EditMode/Helpers/GameFactory.cs):
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++)
基本ヘルパー(Source/<Module>/Private/Tests/Helpers/GameTestHelpers.h):
#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. システム固有のヘルパーを生成する
[system-name] または all モードの場合、システムごとにヘルパーを生成します:
必要な GDD セクションだけを読みます。全体を読み込むのではなく、Grep pattern="^## ([0-9]+\. )?(Formulas|Edge Cases|Detailed Rules|Detailed Design)" path="design/gdd/[system].md" output_mode="content" -A 30 を使用します(番号は省略可能で、逆順にドキュメント化された GDD の ## 4. Formulas にも一致します)。ピアスキルの各セクションは GDD をセクション単位で検索するため、all モードで全体を読み込むと、400 行を超える GDD がシステムごとに読み込まれ、負荷が増大します。以下を抽出します:
- データ型(エンティティ型、コンポーネント名)
- 数式の変数とその範囲
- Edge Cases に記載された一般的なテストシナリオ
そのシステムのオブジェクトに固有のファクトリー関数を含む tests/helpers/[system]_factory.[ext] を生成します。
combat システムのパターン例(Godot/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. 出力の書き込み
作成する内容の概要を提示します。
## 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
「これらのヘルパーファイルを [helper root] に書き込んでもよろしいですか?」と尋ねます。
既存のファイルは絶対に上書きしないでください。 ファイルがすでに存在する場合は、次のように報告します。
「[path] をスキップします — すでに存在します。再生成する場合は、ファイルを手動で削除してください。」
書き込み後:判定:完了 — ヘルパーファイルを作成しました。
「ヘルパーファイルを作成しました。テストで使用するには:
- Godot:
class_nameは自動的にインポートされます — 明示的なインポートは不要です - Unity:
usingディレクティブを追加するか、テストアセンブリを参照してください - Unreal:
#include \"Tests/Helpers/GameTestHelpers.h\"(モジュール内から)」
共同作業プロトコル
- 既存のヘルパーは絶対に上書きしないでください — 手作業によるカスタマイズが含まれている可能性があります。まだ存在しない新しいファイルのみを生成してください
- 生成コードは出発点です — 生成されるファクトリー関数は簡潔さのためにメタデータパターンを使用します。コードができたら、実際のクラス構造に合わせて調整してください
- ヘルパーには GDD を反映してください — ヘルパー内の範囲と定数は、独自に作った値ではなく、GDD の公式セクションに基づいている必要があります
- 書き込み前に確認してください —
tests/にファイルを作成する前に、必ず確認してください
次のステップ
- テストフレームワークがまだスキャフォールディングされていない場合は、
/test-setupを実行します。 /dev-storyを使ってストーリーを実装します — ヘルパーによって、新しいテストファイルの定型コードを減らせます。- ヘルパーによるカバレッジが必要な可能性のある他のスキルを検証するには、
/skill-testを実行します。