添加 claude code game studios 到项目
This commit is contained in:
395
.claude/skills/test-helpers/SKILL.md
Normal file
395
.claude/skills/test-helpers/SKILL.md
Normal file
@@ -0,0 +1,395 @@
|
||||
---
|
||||
name: test-helpers
|
||||
description: "Generate engine-specific test helper libraries for the project's test suite. Reads existing test patterns and produces tests/helpers/ with assertion utilities, factory functions, and mock objects tailored to the project's systems. Reduces boilerplate in new test files."
|
||||
argument-hint: "[system-name | all | scaffold]"
|
||||
user-invocable: true
|
||||
allowed-tools: Read, Glob, Grep, Write
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
---
|
||||
|
||||
## 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 `.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."
|
||||
|
||||
---
|
||||
|
||||
## 3. Load Existing Test Patterns
|
||||
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
## 4. Generate Engine-Specific Helpers
|
||||
|
||||
### Godot 4 (GDUnit4 / GDScript)
|
||||
|
||||
**Base helper** (`tests/helpers/game_assertions.gd`):
|
||||
|
||||
```gdscript
|
||||
## Game-specific assertion utilities for [Project Name] tests.
|
||||
## Extends GdUnitAssertions with domain-specific helpers.
|
||||
##
|
||||
## Usage:
|
||||
## var assert = GameAssertions.new()
|
||||
## assert.health_in_range(entity, 0, entity.max_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:
|
||||
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.
|
||||
## 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)
|
||||
|
||||
## 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)
|
||||
|
||||
## 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
|
||||
## 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.
|
||||
## Wraps GdUnitSceneRunner for common patterns.
|
||||
|
||||
class_name SceneRunnerHelper
|
||||
extends GdUnitTestSuite
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Generate System-Specific Helpers
|
||||
|
||||
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
|
||||
## 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]):
|
||||
- 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\"`"
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user