feat(bmgd): Add E2E testing methodology and scaffold workflow
- Add comprehensive e2e-testing.md knowledge fragment - Add e2e-scaffold workflow for infrastructure generation - Update qa-index.csv with e2e-testing fragment reference - Update game-qa.agent.yaml with ES trigger - Update test-design and automate instructions with E2E guidance - Update unity-testing.md with E2E section reference
This commit is contained in:
parent
eeebf152af
commit
16190f7a04
|
|
@ -22,6 +22,8 @@ agent:
|
|||
|
||||
critical_actions:
|
||||
- "Consult {project-root}/_bmad/bmgd/gametest/qa-index.csv to select knowledge fragments under knowledge/ and load only the files needed for the current task"
|
||||
- "For E2E testing requests, always load knowledge/e2e-testing.md first"
|
||||
- "When scaffolding tests, distinguish between unit, integration, and E2E test needs"
|
||||
- "Load the referenced fragment(s) from {project-root}/_bmad/bmgd/gametest/knowledge/ before giving recommendations"
|
||||
- "Cross-check recommendations with the current official Unity Test Framework, Unreal Automation, or Godot GUT documentation"
|
||||
- "Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`"
|
||||
|
|
@ -43,6 +45,10 @@ agent:
|
|||
workflow: "{project-root}/_bmad/bmgd/workflows/gametest/automate/workflow.yaml"
|
||||
description: "[TA] Generate automated game tests"
|
||||
|
||||
- trigger: ES or fuzzy match on e2e-scaffold
|
||||
workflow: "{project-root}/_bmad/bmgd/workflows/gametest/e2e-scaffold/workflow.yaml"
|
||||
description: "[ES] Scaffold E2E testing infrastructure"
|
||||
|
||||
- trigger: PP or fuzzy match on playtest-plan
|
||||
workflow: "{project-root}/_bmad/bmgd/workflows/gametest/playtest-plan/workflow.yaml"
|
||||
description: "[PP] Create structured playtesting plan"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,957 @@
|
|||
# End-to-End Testing for Games
|
||||
|
||||
## Overview
|
||||
|
||||
E2E tests validate complete gameplay flows from the player's perspective — the full orchestra, not individual instruments. Unlike integration tests that verify system interactions, E2E tests verify *player journeys* work correctly from start to finish.
|
||||
|
||||
This is the difference between "does the damage calculator work with the inventory system?" (integration) and "can a player actually complete a combat encounter from selection to resolution?" (E2E).
|
||||
|
||||
## E2E vs Integration vs Unit
|
||||
|
||||
| Aspect | Unit | Integration | E2E |
|
||||
|--------|------|-------------|-----|
|
||||
| Scope | Single class | System interaction | Complete flow |
|
||||
| Speed | < 10ms | < 1s | 1-30s |
|
||||
| Stability | Very stable | Stable | Requires care |
|
||||
| Example | DamageCalc math | Combat + Inventory | Full combat encounter |
|
||||
| Dependencies | None/mocked | Some real | All real |
|
||||
| Catches | Logic bugs | Wiring bugs | Journey bugs |
|
||||
|
||||
## The E2E Testing Pyramid Addition
|
||||
|
||||
```
|
||||
/\
|
||||
/ \ Manual Playtesting
|
||||
/----\ (Fun, Feel, Experience)
|
||||
/ \
|
||||
/--------\ E2E Tests
|
||||
/ \ (Player Journeys)
|
||||
/------------\
|
||||
/ \ Integration Tests
|
||||
/----------------\ (System Interactions)
|
||||
/ \ Unit Tests
|
||||
/____________________\ (Pure Logic)
|
||||
```
|
||||
|
||||
E2E tests sit between integration tests and manual playtesting. They automate what *can* be automated about player experience while acknowledging that "is this fun?" still requires human judgment.
|
||||
|
||||
## E2E Infrastructure Requirements
|
||||
|
||||
Before writing E2E tests, scaffold supporting infrastructure. Without this foundation, E2E tests become brittle, flaky nightmares that erode trust faster than they build confidence.
|
||||
|
||||
### 1. Test Fixture Base Class
|
||||
|
||||
Provides scene loading, cleanup, and common utilities. Every E2E test inherits from this.
|
||||
|
||||
**Unity Example:**
|
||||
|
||||
```csharp
|
||||
using System.Collections;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
public abstract class GameE2ETestFixture
|
||||
{
|
||||
protected virtual string SceneName => "GameScene";
|
||||
protected GameStateManager GameState { get; private set; }
|
||||
protected InputSimulator Input { get; private set; }
|
||||
protected ScenarioBuilder Scenario { get; private set; }
|
||||
|
||||
[UnitySetUp]
|
||||
public IEnumerator BaseSetUp()
|
||||
{
|
||||
// Load the game scene
|
||||
yield return SceneManager.LoadSceneAsync(SceneName);
|
||||
yield return null; // Wait one frame for scene initialization
|
||||
|
||||
// Get core references
|
||||
GameState = Object.FindFirstObjectByType<GameStateManager>();
|
||||
Assert.IsNotNull(GameState, $"GameStateManager not found in {SceneName}");
|
||||
|
||||
// Initialize test utilities
|
||||
Input = new InputSimulator();
|
||||
Scenario = new ScenarioBuilder(GameState);
|
||||
|
||||
// Wait for game to be ready
|
||||
yield return WaitForGameReady();
|
||||
}
|
||||
|
||||
[UnityTearDown]
|
||||
public IEnumerator BaseTearDown()
|
||||
{
|
||||
// Clean up any test-spawned objects
|
||||
yield return CleanupTestObjects();
|
||||
|
||||
// Reset input state
|
||||
Input?.Reset();
|
||||
}
|
||||
|
||||
protected IEnumerator WaitForGameReady(float timeout = 10f)
|
||||
{
|
||||
yield return AsyncAssert.WaitUntil(
|
||||
() => GameState != null && GameState.IsReady,
|
||||
"Game ready state",
|
||||
timeout);
|
||||
}
|
||||
|
||||
protected virtual IEnumerator CleanupTestObjects()
|
||||
{
|
||||
// Override in derived classes for game-specific cleanup
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Unreal Example:**
|
||||
|
||||
```cpp
|
||||
// GameE2ETestBase.h
|
||||
UCLASS()
|
||||
class AGameE2ETestBase : public AFunctionalTest
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
UPROPERTY()
|
||||
UGameStateManager* GameState;
|
||||
|
||||
UPROPERTY()
|
||||
UInputSimulator* InputSim;
|
||||
|
||||
UPROPERTY()
|
||||
UScenarioBuilder* Scenario;
|
||||
|
||||
virtual void PrepareTest() override;
|
||||
virtual void StartTest() override;
|
||||
virtual void CleanUp() override;
|
||||
|
||||
void WaitForGameReady(float Timeout = 10.f);
|
||||
};
|
||||
```
|
||||
|
||||
**Godot Example:**
|
||||
|
||||
```gdscript
|
||||
extends GutTest
|
||||
class_name GameE2ETestFixture
|
||||
|
||||
var game_state: GameStateManager
|
||||
var input_sim: InputSimulator
|
||||
var scenario: ScenarioBuilder
|
||||
var _scene_instance: Node
|
||||
|
||||
func before_each():
|
||||
# Load game scene
|
||||
var scene = load("res://scenes/game.tscn")
|
||||
_scene_instance = scene.instantiate()
|
||||
add_child(_scene_instance)
|
||||
|
||||
# Get references
|
||||
game_state = _scene_instance.get_node("GameStateManager")
|
||||
input_sim = InputSimulator.new()
|
||||
scenario = ScenarioBuilder.new(game_state)
|
||||
|
||||
# Wait for ready
|
||||
await wait_for_game_ready()
|
||||
|
||||
func after_each():
|
||||
if _scene_instance:
|
||||
_scene_instance.queue_free()
|
||||
input_sim = null
|
||||
scenario = null
|
||||
|
||||
func wait_for_game_ready(timeout: float = 10.0):
|
||||
var elapsed = 0.0
|
||||
while not game_state.is_ready and elapsed < timeout:
|
||||
await get_tree().process_frame
|
||||
elapsed += get_process_delta_time()
|
||||
assert_true(game_state.is_ready, "Game should be ready")
|
||||
```
|
||||
|
||||
### 2. Scenario Builder (Fluent API)
|
||||
|
||||
Configure game state for test scenarios without manual setup. This is the secret sauce — it lets you express test preconditions in domain language.
|
||||
|
||||
**Unity Example:**
|
||||
|
||||
```csharp
|
||||
public class ScenarioBuilder
|
||||
{
|
||||
private readonly GameStateManager _gameState;
|
||||
private readonly List<Func<IEnumerator>> _setupActions = new();
|
||||
|
||||
public ScenarioBuilder(GameStateManager gameState)
|
||||
{
|
||||
_gameState = gameState;
|
||||
}
|
||||
|
||||
// Domain-specific setup methods
|
||||
public ScenarioBuilder WithUnit(Faction faction, Hex position, int movementPoints = 6)
|
||||
{
|
||||
_setupActions.Add(() => SpawnUnit(faction, position, movementPoints));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ScenarioBuilder WithTerrain(Hex position, TerrainType terrain)
|
||||
{
|
||||
_setupActions.Add(() => SetTerrain(position, terrain));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ScenarioBuilder OnTurn(int turnNumber)
|
||||
{
|
||||
_setupActions.Add(() => SetTurn(turnNumber));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ScenarioBuilder OnPhase(TurnPhase phase)
|
||||
{
|
||||
_setupActions.Add(() => SetPhase(phase));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ScenarioBuilder WithActiveFaction(Faction faction)
|
||||
{
|
||||
_setupActions.Add(() => SetActiveFaction(faction));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ScenarioBuilder FromSaveFile(string saveFileName)
|
||||
{
|
||||
_setupActions.Add(() => LoadSaveFile(saveFileName));
|
||||
return this;
|
||||
}
|
||||
|
||||
// Execute all setup actions
|
||||
public IEnumerator Build()
|
||||
{
|
||||
foreach (var action in _setupActions)
|
||||
{
|
||||
yield return action();
|
||||
yield return null; // Allow state to propagate
|
||||
}
|
||||
_setupActions.Clear();
|
||||
}
|
||||
|
||||
// Private implementation methods
|
||||
private IEnumerator SpawnUnit(Faction faction, Hex position, int mp)
|
||||
{
|
||||
var unit = _gameState.SpawnUnit(faction, position);
|
||||
unit.MovementPoints = mp;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
private IEnumerator SetTerrain(Hex position, TerrainType terrain)
|
||||
{
|
||||
_gameState.Map.SetTerrain(position, terrain);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
private IEnumerator SetTurn(int turn)
|
||||
{
|
||||
_gameState.SetTurnNumber(turn);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
private IEnumerator SetPhase(TurnPhase phase)
|
||||
{
|
||||
_gameState.SetPhase(phase);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
private IEnumerator SetActiveFaction(Faction faction)
|
||||
{
|
||||
_gameState.SetActiveFaction(faction);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
private IEnumerator LoadSaveFile(string fileName)
|
||||
{
|
||||
var path = $"TestData/{fileName}";
|
||||
yield return _gameState.LoadGame(path);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```csharp
|
||||
yield return Scenario
|
||||
.WithUnit(Faction.Player, new Hex(3, 4), movementPoints: 6)
|
||||
.WithUnit(Faction.Enemy, new Hex(5, 4))
|
||||
.WithTerrain(new Hex(4, 4), TerrainType.Forest)
|
||||
.OnTurn(1)
|
||||
.WithActiveFaction(Faction.Player)
|
||||
.Build();
|
||||
```
|
||||
|
||||
### 3. Input Simulator
|
||||
|
||||
Abstract player input for deterministic testing. Don't simulate raw mouse positions — simulate player *intent*.
|
||||
|
||||
**Unity Example (New Input System):**
|
||||
|
||||
```csharp
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
public class InputSimulator
|
||||
{
|
||||
private Mouse _mouse;
|
||||
private Keyboard _keyboard;
|
||||
private Camera _camera;
|
||||
|
||||
public InputSimulator()
|
||||
{
|
||||
_mouse = Mouse.current ?? InputSystem.AddDevice<Mouse>();
|
||||
_keyboard = Keyboard.current ?? InputSystem.AddDevice<Keyboard>();
|
||||
_camera = Camera.main;
|
||||
}
|
||||
|
||||
public IEnumerator ClickWorldPosition(Vector3 worldPos)
|
||||
{
|
||||
var screenPos = _camera.WorldToScreenPoint(worldPos);
|
||||
yield return ClickScreenPosition(screenPos);
|
||||
}
|
||||
|
||||
public IEnumerator ClickHex(Hex hex)
|
||||
{
|
||||
var worldPos = HexUtils.HexToWorld(hex);
|
||||
yield return ClickWorldPosition(worldPos);
|
||||
}
|
||||
|
||||
public IEnumerator ClickScreenPosition(Vector2 screenPos)
|
||||
{
|
||||
// Move mouse
|
||||
InputSystem.QueueStateEvent(_mouse, new MouseState { position = screenPos });
|
||||
yield return null;
|
||||
|
||||
// Press
|
||||
InputSystem.QueueStateEvent(_mouse, new MouseState
|
||||
{
|
||||
position = screenPos,
|
||||
buttons = 1
|
||||
});
|
||||
yield return null;
|
||||
|
||||
// Release
|
||||
InputSystem.QueueStateEvent(_mouse, new MouseState
|
||||
{
|
||||
position = screenPos,
|
||||
buttons = 0
|
||||
});
|
||||
yield return null;
|
||||
}
|
||||
|
||||
public IEnumerator ClickButton(string buttonName)
|
||||
{
|
||||
var button = GameObject.Find(buttonName)?.GetComponent<UnityEngine.UI.Button>();
|
||||
Assert.IsNotNull(button, $"Button '{buttonName}' not found");
|
||||
|
||||
button.onClick.Invoke();
|
||||
yield return null;
|
||||
}
|
||||
|
||||
public IEnumerator DragFromTo(Vector3 from, Vector3 to, float duration = 0.5f)
|
||||
{
|
||||
var fromScreen = _camera.WorldToScreenPoint(from);
|
||||
var toScreen = _camera.WorldToScreenPoint(to);
|
||||
|
||||
// Start drag
|
||||
InputSystem.QueueStateEvent(_mouse, new MouseState
|
||||
{
|
||||
position = fromScreen,
|
||||
buttons = 1
|
||||
});
|
||||
yield return null;
|
||||
|
||||
// Interpolate drag
|
||||
var elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
var t = elapsed / duration;
|
||||
var pos = Vector2.Lerp(fromScreen, toScreen, t);
|
||||
InputSystem.QueueStateEvent(_mouse, new MouseState
|
||||
{
|
||||
position = pos,
|
||||
buttons = 1
|
||||
});
|
||||
yield return null;
|
||||
elapsed += Time.deltaTime;
|
||||
}
|
||||
|
||||
// End drag
|
||||
InputSystem.QueueStateEvent(_mouse, new MouseState
|
||||
{
|
||||
position = toScreen,
|
||||
buttons = 0
|
||||
});
|
||||
yield return null;
|
||||
}
|
||||
|
||||
public IEnumerator PressKey(Key key)
|
||||
{
|
||||
_keyboard.SetKeyDown(key);
|
||||
yield return null;
|
||||
_keyboard.SetKeyUp(key);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
// Reset any held state
|
||||
if (_mouse != null)
|
||||
{
|
||||
InputSystem.QueueStateEvent(_mouse, new MouseState());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Async Assertions
|
||||
|
||||
Wait-for-condition assertions with meaningful failure messages. The timeout and message are critical — when tests fail, you need to know *what* it was waiting for.
|
||||
|
||||
**Unity Example:**
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
using System.Collections;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
public static class AsyncAssert
|
||||
{
|
||||
/// <summary>
|
||||
/// Wait until condition is true, or fail with message after timeout.
|
||||
/// </summary>
|
||||
public static IEnumerator WaitUntil(
|
||||
Func<bool> condition,
|
||||
string description,
|
||||
float timeout = 5f)
|
||||
{
|
||||
var elapsed = 0f;
|
||||
while (!condition() && elapsed < timeout)
|
||||
{
|
||||
yield return null;
|
||||
elapsed += Time.deltaTime;
|
||||
}
|
||||
|
||||
Assert.IsTrue(condition(),
|
||||
$"Timeout after {timeout}s waiting for: {description}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait until condition is true, with periodic logging.
|
||||
/// </summary>
|
||||
public static IEnumerator WaitUntilVerbose(
|
||||
Func<bool> condition,
|
||||
string description,
|
||||
float timeout = 5f,
|
||||
float logInterval = 1f)
|
||||
{
|
||||
var elapsed = 0f;
|
||||
var lastLog = 0f;
|
||||
|
||||
while (!condition() && elapsed < timeout)
|
||||
{
|
||||
if (elapsed - lastLog >= logInterval)
|
||||
{
|
||||
Debug.Log($"[E2E] Still waiting for: {description} ({elapsed:F1}s)");
|
||||
lastLog = elapsed;
|
||||
}
|
||||
yield return null;
|
||||
elapsed += Time.deltaTime;
|
||||
}
|
||||
|
||||
Assert.IsTrue(condition(),
|
||||
$"Timeout after {timeout}s waiting for: {description}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for a specific value, with descriptive failure.
|
||||
/// </summary>
|
||||
public static IEnumerator WaitForValue<T>(
|
||||
Func<T> getter,
|
||||
T expected,
|
||||
string description,
|
||||
float timeout = 5f) where T : IEquatable<T>
|
||||
{
|
||||
yield return WaitUntil(
|
||||
() => expected.Equals(getter()),
|
||||
$"{description} to equal {expected} (current: {getter()})",
|
||||
timeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for an event to fire.
|
||||
/// </summary>
|
||||
public static IEnumerator WaitForEvent<T>(
|
||||
Action<Action<T>> subscribe,
|
||||
Action<Action<T>> unsubscribe,
|
||||
string eventName,
|
||||
float timeout = 5f) where T : class
|
||||
{
|
||||
T received = null;
|
||||
Action<T> handler = e => received = e;
|
||||
|
||||
subscribe(handler);
|
||||
|
||||
yield return WaitUntil(
|
||||
() => received != null,
|
||||
$"Event '{eventName}' to fire",
|
||||
timeout);
|
||||
|
||||
unsubscribe(handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assert that something does NOT happen within a time window.
|
||||
/// </summary>
|
||||
public static IEnumerator WaitAndAssertNot(
|
||||
Func<bool> condition,
|
||||
string description,
|
||||
float duration = 1f)
|
||||
{
|
||||
var elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
Assert.IsFalse(condition(),
|
||||
$"Condition unexpectedly became true: {description}");
|
||||
yield return null;
|
||||
elapsed += Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## E2E Test Patterns
|
||||
|
||||
### Given-When-Then with Async
|
||||
|
||||
The core pattern for E2E tests. Clear structure, readable intent.
|
||||
|
||||
```csharp
|
||||
[UnityTest]
|
||||
public IEnumerator PlayerCanMoveUnitThroughZOC()
|
||||
{
|
||||
// GIVEN: Soviet unit adjacent to German ZOC
|
||||
yield return Scenario
|
||||
.WithUnit(Faction.Soviet, new Hex(3, 4), movementPoints: 6)
|
||||
.WithUnit(Faction.German, new Hex(4, 4)) // Creates ZOC at adjacent hexes
|
||||
.WithActiveFaction(Faction.Soviet)
|
||||
.Build();
|
||||
|
||||
// WHEN: Player selects unit and moves through ZOC
|
||||
yield return Input.ClickHex(new Hex(3, 4)); // Select unit
|
||||
yield return AsyncAssert.WaitUntil(
|
||||
() => GameState.Selection.HasSelectedUnit,
|
||||
"Unit should be selected");
|
||||
|
||||
yield return Input.ClickHex(new Hex(5, 4)); // Click destination (through ZOC)
|
||||
|
||||
// THEN: Unit arrives with reduced movement points (ZOC cost)
|
||||
yield return AsyncAssert.WaitUntil(
|
||||
() => GetUnitAt(new Hex(5, 4)) != null,
|
||||
"Unit should arrive at destination");
|
||||
|
||||
var unit = GetUnitAt(new Hex(5, 4));
|
||||
Assert.Less(unit.MovementPoints, 3,
|
||||
"ZOC passage should cost extra movement points");
|
||||
}
|
||||
```
|
||||
|
||||
### Full Turn Cycle
|
||||
|
||||
Testing the complete turn lifecycle.
|
||||
|
||||
```csharp
|
||||
[UnityTest]
|
||||
public IEnumerator FullTurnCycle_PlayerToAIAndBack()
|
||||
{
|
||||
// GIVEN: Mid-game state with both factions having units
|
||||
yield return Scenario
|
||||
.FromSaveFile("mid_game_scenario.json")
|
||||
.Build();
|
||||
|
||||
var startingTurn = GameState.TurnNumber;
|
||||
|
||||
// WHEN: Player ends their turn
|
||||
yield return Input.ClickButton("EndPhaseButton");
|
||||
yield return AsyncAssert.WaitUntil(
|
||||
() => GameState.CurrentPhase == TurnPhase.EndPhaseConfirmation,
|
||||
"End phase confirmation");
|
||||
|
||||
yield return Input.ClickButton("ConfirmButton");
|
||||
|
||||
// THEN: AI executes its turn
|
||||
yield return AsyncAssert.WaitUntil(
|
||||
() => GameState.CurrentFaction == Faction.AI,
|
||||
"AI turn should begin");
|
||||
|
||||
// AND: Eventually returns to player
|
||||
yield return AsyncAssert.WaitUntil(
|
||||
() => GameState.CurrentFaction == Faction.Player,
|
||||
"Player turn should return",
|
||||
timeout: 30f); // AI might take a while
|
||||
|
||||
Assert.AreEqual(startingTurn + 1, GameState.TurnNumber,
|
||||
"Turn number should increment");
|
||||
}
|
||||
```
|
||||
|
||||
### Save/Load Round-Trip
|
||||
|
||||
Critical for any game with persistence.
|
||||
|
||||
```csharp
|
||||
[UnityTest]
|
||||
public IEnumerator SaveLoad_PreservesGameState()
|
||||
{
|
||||
// GIVEN: Game in specific state
|
||||
yield return Scenario
|
||||
.WithUnit(Faction.Player, new Hex(5, 5), movementPoints: 3)
|
||||
.OnTurn(7)
|
||||
.Build();
|
||||
|
||||
var unitPosition = new Hex(5, 5);
|
||||
var originalMP = GetUnitAt(unitPosition).MovementPoints;
|
||||
var originalTurn = GameState.TurnNumber;
|
||||
|
||||
// WHEN: Save and reload
|
||||
var savePath = "test_save_roundtrip";
|
||||
yield return GameState.SaveGame(savePath);
|
||||
|
||||
// Trash the current state
|
||||
yield return SceneManager.LoadSceneAsync(SceneName);
|
||||
yield return WaitForGameReady();
|
||||
|
||||
// Load the save
|
||||
yield return GameState.LoadGame(savePath);
|
||||
yield return WaitForGameReady();
|
||||
|
||||
// THEN: State is preserved
|
||||
Assert.AreEqual(originalTurn, GameState.TurnNumber,
|
||||
"Turn number should be preserved");
|
||||
|
||||
var loadedUnit = GetUnitAt(unitPosition);
|
||||
Assert.IsNotNull(loadedUnit, "Unit should exist at saved position");
|
||||
Assert.AreEqual(originalMP, loadedUnit.MovementPoints,
|
||||
"Movement points should be preserved");
|
||||
|
||||
// Cleanup
|
||||
System.IO.File.Delete(GameState.GetSavePath(savePath));
|
||||
}
|
||||
```
|
||||
|
||||
### UI Flow Testing
|
||||
|
||||
Testing complete UI journeys.
|
||||
|
||||
```csharp
|
||||
[UnityTest]
|
||||
public IEnumerator MainMenu_NewGame_ReachesGameplay()
|
||||
{
|
||||
// GIVEN: At main menu
|
||||
yield return SceneManager.LoadSceneAsync("MainMenu");
|
||||
yield return null;
|
||||
|
||||
// WHEN: Start new game flow
|
||||
yield return Input.ClickButton("NewGameButton");
|
||||
yield return AsyncAssert.WaitUntil(
|
||||
() => FindPanel("DifficultySelect") != null,
|
||||
"Difficulty selection should appear");
|
||||
|
||||
yield return Input.ClickButton("NormalDifficultyButton");
|
||||
yield return Input.ClickButton("StartButton");
|
||||
|
||||
// THEN: Game scene loads and is playable
|
||||
yield return AsyncAssert.WaitUntil(
|
||||
() => SceneManager.GetActiveScene().name == "GameScene",
|
||||
"Game scene should load",
|
||||
timeout: 10f);
|
||||
|
||||
yield return WaitForGameReady();
|
||||
|
||||
Assert.AreEqual(TurnPhase.PlayerMovement, GameState.CurrentPhase,
|
||||
"Should start in player movement phase");
|
||||
}
|
||||
```
|
||||
|
||||
## What to E2E Test
|
||||
|
||||
### High Priority (Test These)
|
||||
|
||||
| Category | Why | Examples |
|
||||
|----------|-----|----------|
|
||||
| Core gameplay loop | 90% of player time | Select → Move → Attack → End Turn |
|
||||
| Turn/phase transitions | State machine boundaries | Phase changes, turn handoff |
|
||||
| Save → Load → Resume | Data integrity | Full round-trip with verification |
|
||||
| Win/lose conditions | Critical path endpoints | Victory triggers, game over |
|
||||
| Critical UI flows | First impressions | Menu → Game → Pause → Resume |
|
||||
|
||||
### Medium Priority (Test Key Paths)
|
||||
|
||||
| Category | Why | Examples |
|
||||
|----------|-----|----------|
|
||||
| Undo/redo | Easy to break | Action reversal |
|
||||
| Multiplayer sync | Complex state | Turn handoff in MP |
|
||||
| Tutorial flow | First-time experience | Guided sequence |
|
||||
|
||||
### Low Priority (Usually Skip for E2E)
|
||||
|
||||
| Category | Why | Better Tested By |
|
||||
|----------|-----|------------------|
|
||||
| Edge cases | Combinatorial explosion | Unit tests |
|
||||
| Visual correctness | Subjective, changes often | Manual testing |
|
||||
| Performance | Needs dedicated tooling | Performance tests |
|
||||
| Every permutation | Infinite combinations | Unit + integration |
|
||||
| AI decision quality | Subjective | Playtesting |
|
||||
|
||||
## E2E Test Organization
|
||||
|
||||
```
|
||||
Tests/
|
||||
├── EditMode/
|
||||
│ └── ... (existing unit tests)
|
||||
├── PlayMode/
|
||||
│ ├── Integration/
|
||||
│ │ └── ... (existing integration tests)
|
||||
│ └── E2E/
|
||||
│ ├── E2E.asmdef
|
||||
│ ├── Infrastructure/
|
||||
│ │ ├── GameE2ETestFixture.cs
|
||||
│ │ ├── ScenarioBuilder.cs
|
||||
│ │ ├── InputSimulator.cs
|
||||
│ │ └── AsyncAssert.cs
|
||||
│ ├── Scenarios/
|
||||
│ │ ├── TurnCycleE2ETests.cs
|
||||
│ │ ├── MovementE2ETests.cs
|
||||
│ │ ├── CombatE2ETests.cs
|
||||
│ │ ├── SaveLoadE2ETests.cs
|
||||
│ │ └── UIFlowE2ETests.cs
|
||||
│ └── TestData/
|
||||
│ ├── mid_game_scenario.json
|
||||
│ ├── endgame_scenario.json
|
||||
│ └── edge_case_setup.json
|
||||
```
|
||||
|
||||
### Assembly Definition for E2E
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "E2E",
|
||||
"references": [
|
||||
"GameAssembly",
|
||||
"UnityEngine.TestRunner",
|
||||
"UnityEditor.TestRunner"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"defineConstraints": [
|
||||
"UNITY_INCLUDE_TESTS"
|
||||
],
|
||||
"autoReferenced": false
|
||||
}
|
||||
```
|
||||
|
||||
## CI Considerations
|
||||
|
||||
E2E tests are slower and potentially flaky. Handle with care.
|
||||
|
||||
### Separate CI Job
|
||||
|
||||
```yaml
|
||||
# GitHub Actions example
|
||||
e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: game-ci/unity-test-runner@v4
|
||||
with:
|
||||
testMode: PlayMode
|
||||
projectPath: .
|
||||
customParameters: -testCategory E2E
|
||||
```
|
||||
|
||||
### Retry Strategy
|
||||
|
||||
```yaml
|
||||
# Retry flaky tests once before failing
|
||||
- uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 15
|
||||
max_attempts: 2
|
||||
command: |
|
||||
unity-test-runner --category E2E
|
||||
```
|
||||
|
||||
### Failure Artifacts
|
||||
|
||||
Capture screenshots and logs on failure:
|
||||
|
||||
```csharp
|
||||
[UnityTearDown]
|
||||
public IEnumerator CaptureOnFailure()
|
||||
{
|
||||
if (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed)
|
||||
{
|
||||
var screenshot = ScreenCapture.CaptureScreenshotAsTexture();
|
||||
var bytes = screenshot.EncodeToPNG();
|
||||
var path = $"TestResults/Screenshots/{TestContext.CurrentContext.Test.Name}.png";
|
||||
System.IO.File.WriteAllBytes(path, bytes);
|
||||
|
||||
Debug.Log($"[E2E FAILURE] Screenshot saved: {path}");
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
```
|
||||
|
||||
### Execution Frequency
|
||||
|
||||
| Suite | When | Timeout |
|
||||
|-------|------|---------|
|
||||
| Unit tests | Every commit | 5 min |
|
||||
| Integration | Every commit | 10 min |
|
||||
| E2E (smoke) | Every commit | 15 min |
|
||||
| E2E (full) | Nightly | 60 min |
|
||||
|
||||
## Avoiding Flaky Tests
|
||||
|
||||
E2E tests are notorious for flakiness. Fight it proactively.
|
||||
|
||||
### DO
|
||||
|
||||
- Use explicit waits with `AsyncAssert.WaitUntil`
|
||||
- Wait for *game state*, not time
|
||||
- Clean up thoroughly in TearDown
|
||||
- Isolate tests completely
|
||||
- Use deterministic scenarios
|
||||
- Seed random number generators
|
||||
|
||||
### DON'T
|
||||
|
||||
- Use `yield return new WaitForSeconds(x)` as primary sync
|
||||
- Depend on test execution order
|
||||
- Share state between tests
|
||||
- Rely on animation timing
|
||||
- Assume frame-perfect timing
|
||||
- Skip cleanup "because it's slow"
|
||||
|
||||
### Debugging Flaky Tests
|
||||
|
||||
```csharp
|
||||
// Add verbose logging to track down race conditions
|
||||
[UnityTest]
|
||||
public IEnumerator FlakyTest_WithDebugging()
|
||||
{
|
||||
Debug.Log($"[E2E] Test start: {Time.frameCount}");
|
||||
|
||||
yield return Scenario.Build();
|
||||
Debug.Log($"[E2E] Scenario built: {Time.frameCount}");
|
||||
|
||||
yield return Input.ClickHex(targetHex);
|
||||
Debug.Log($"[E2E] Input sent: {Time.frameCount}, Selection: {GameState.Selection}");
|
||||
|
||||
yield return AsyncAssert.WaitUntilVerbose(
|
||||
() => ExpectedCondition(),
|
||||
"Expected condition",
|
||||
timeout: 10f,
|
||||
logInterval: 0.5f);
|
||||
}
|
||||
```
|
||||
|
||||
## Engine-Specific Notes
|
||||
|
||||
### Unity
|
||||
|
||||
- Use `[UnityTest]` attribute for coroutine-based tests
|
||||
- Prefer `WaitUntil` over `WaitForSeconds`
|
||||
- Use `Object.FindFirstObjectByType<T>()` (not the deprecated `FindObjectOfType`)
|
||||
- Consider `InputTestFixture` for Input System simulation
|
||||
- Remember: `yield return null` waits one frame
|
||||
|
||||
### Unreal
|
||||
|
||||
- Use `FFunctionalTest` actors for level-based E2E
|
||||
- `LatentIt` for async test steps in Automation Framework
|
||||
- Gauntlet for extended E2E suites running in isolated processes
|
||||
- `ADD_LATENT_AUTOMATION_COMMAND` for sequenced operations
|
||||
|
||||
### Godot
|
||||
|
||||
- Use `await` for async operations in GUT
|
||||
- `await get_tree().create_timer(x).timeout` for timed waits
|
||||
- Scene instancing provides natural test isolation
|
||||
- Use `queue_free()` for cleanup, not `free()` (avoids errors)
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### The "Test Everything" Trap
|
||||
|
||||
Don't try to E2E test every edge case. That's what unit tests are for.
|
||||
|
||||
```csharp
|
||||
// BAD: Testing edge case via E2E
|
||||
[UnityTest]
|
||||
public IEnumerator Movement_WithExactlyZeroMP_CannotMove() // Unit test this
|
||||
{
|
||||
// 30 seconds of setup for a condition unit tests cover
|
||||
}
|
||||
|
||||
// GOOD: E2E tests the journey, unit tests the edge cases
|
||||
[UnityTest]
|
||||
public IEnumerator Movement_TypicalPlayerJourney_WorksCorrectly()
|
||||
{
|
||||
// Tests the common path players actually experience
|
||||
}
|
||||
```
|
||||
|
||||
### The "Magic Sleep" Pattern
|
||||
|
||||
```csharp
|
||||
// BAD: Hoping 2 seconds is enough
|
||||
yield return new WaitForSeconds(2f);
|
||||
Assert.IsTrue(condition);
|
||||
|
||||
// GOOD: Wait for the actual condition
|
||||
yield return AsyncAssert.WaitUntil(() => condition, "description");
|
||||
```
|
||||
|
||||
### The "Shared State" Trap
|
||||
|
||||
```csharp
|
||||
// BAD: Tests pollute each other
|
||||
private static int testCounter = 0; // Shared between tests!
|
||||
|
||||
// GOOD: Each test is isolated
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
// Fresh state every test
|
||||
}
|
||||
```
|
||||
|
||||
## Measuring E2E Test Value
|
||||
|
||||
### Coverage Metrics That Matter
|
||||
|
||||
- **Journey coverage**: How many critical player paths are tested?
|
||||
- **Failure detection rate**: How many real bugs do E2E tests catch?
|
||||
- **False positive rate**: How often do E2E tests fail spuriously?
|
||||
|
||||
### Warning Signs
|
||||
|
||||
- E2E suite takes > 30 minutes
|
||||
- Flaky test rate > 5%
|
||||
- E2E tests duplicate unit test coverage
|
||||
- Team skips E2E tests because they're "always broken"
|
||||
|
||||
### Health Indicators
|
||||
|
||||
- E2E tests catch integration bugs unit tests miss
|
||||
- New features include E2E tests for happy path
|
||||
- Flaky tests are fixed or removed within a sprint
|
||||
- E2E suite runs on every PR (at least smoke subset)
|
||||
|
|
@ -381,3 +381,17 @@ test:
|
|||
| NullReferenceException | Missing Setup | Ensure [SetUp] initializes all fields |
|
||||
| Tests hang | Infinite coroutine | Add timeout or max iterations |
|
||||
| Flaky physics tests | Timing dependent | Use WaitForFixedUpdate, increase tolerance |
|
||||
|
||||
## End-to-End Testing
|
||||
|
||||
For comprehensive E2E testing patterns, infrastructure scaffolding, and
|
||||
scenario builders, see **knowledge/e2e-testing.md**.
|
||||
|
||||
### Quick E2E Checklist for Unity
|
||||
|
||||
- [ ] Create `GameE2ETestFixture` base class
|
||||
- [ ] Implement `ScenarioBuilder` for your game's domain
|
||||
- [ ] Create `InputSimulator` wrapping Input System
|
||||
- [ ] Add `AsyncAssert` utilities
|
||||
- [ ] Organize E2E tests under `Tests/PlayMode/E2E/`
|
||||
- [ ] Configure separate CI job for E2E suite
|
||||
|
|
|
|||
|
|
@ -15,3 +15,4 @@ localization-testing,Localization Testing,"Text, audio, and cultural validation
|
|||
certification-testing,Platform Certification,"Console TRC/XR requirements and certification testing","certification,console,trc,xr",knowledge/certification-testing.md
|
||||
smoke-testing,Smoke Testing,"Critical path validation for build verification","smoke-tests,bvt,ci",knowledge/smoke-testing.md
|
||||
test-priorities,Test Priorities Matrix,"P0-P3 criteria, coverage targets, execution ordering for games","prioritization,risk,coverage",knowledge/test-priorities.md
|
||||
e2e-testing,End-to-End Testing,"Complete player journey testing with infrastructure patterns and async utilities","e2e,integration,player-journeys,scenarios,infrastructure",knowledge/e2e-testing.md
|
||||
|
|
|
|||
|
|
|
@ -209,6 +209,87 @@ func test_{feature}_integration():
|
|||
# Cleanup
|
||||
scene.queue_free()
|
||||
```
|
||||
### E2E Journey Tests
|
||||
|
||||
**Knowledge Base Reference**: `knowledge/e2e-testing.md`
|
||||
```csharp
|
||||
public class {Feature}E2ETests : GameE2ETestFixture
|
||||
{
|
||||
[UnityTest]
|
||||
public IEnumerator {JourneyName}_Succeeds()
|
||||
{
|
||||
// GIVEN
|
||||
yield return Scenario
|
||||
.{SetupMethod1}()
|
||||
.{SetupMethod2}()
|
||||
.Build();
|
||||
|
||||
// WHEN
|
||||
yield return Input.{Action1}();
|
||||
yield return AsyncAssert.WaitUntil(
|
||||
() => {Condition1}, "{Description1}");
|
||||
yield return Input.{Action2}();
|
||||
|
||||
// THEN
|
||||
yield return AsyncAssert.WaitUntil(
|
||||
() => {FinalCondition}, "{FinalDescription}");
|
||||
Assert.{Assertion}({expected}, {actual});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Step 3.5: Generate E2E Infrastructure
|
||||
|
||||
Before generating E2E tests, scaffold the required infrastructure.
|
||||
|
||||
### Infrastructure Checklist
|
||||
|
||||
1. **Test Fixture Base Class**
|
||||
- Scene loading/unloading
|
||||
- Game ready state waiting
|
||||
- Common service access
|
||||
- Cleanup guarantees
|
||||
|
||||
2. **Scenario Builder**
|
||||
- Fluent API for game state configuration
|
||||
- Domain-specific methods (e.g., `WithUnit`, `OnTurn`)
|
||||
- Yields for state propagation
|
||||
|
||||
3. **Input Simulator**
|
||||
- Click/drag abstractions
|
||||
- Button press simulation
|
||||
- Keyboard input queuing
|
||||
|
||||
4. **Async Assertions**
|
||||
- `WaitUntil` with timeout and message
|
||||
- `WaitForEvent` for event-driven flows
|
||||
- `WaitForState` for state machine transitions
|
||||
|
||||
### Generation Template
|
||||
```csharp
|
||||
// GameE2ETestFixture.cs
|
||||
public abstract class GameE2ETestFixture
|
||||
{
|
||||
protected {GameStateClass} GameState;
|
||||
protected {InputSimulatorClass} Input;
|
||||
protected {ScenarioBuilderClass} Scenario;
|
||||
|
||||
[UnitySetUp]
|
||||
public IEnumerator BaseSetUp()
|
||||
{
|
||||
yield return LoadScene("{main_scene}");
|
||||
GameState = Object.FindFirstObjectByType<{GameStateClass}>();
|
||||
Input = new {InputSimulatorClass}();
|
||||
Scenario = new {ScenarioBuilderClass}(GameState);
|
||||
yield return WaitForReady();
|
||||
}
|
||||
|
||||
// ... (fill from e2e-testing.md patterns)
|
||||
}
|
||||
```
|
||||
|
||||
**After scaffolding infrastructure, proceed to generate actual E2E tests.**
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
# E2E Infrastructure Scaffold Checklist
|
||||
|
||||
## Preflight Validation
|
||||
|
||||
- [ ] Test framework already initialized (`Tests/` directory exists with proper structure)
|
||||
- [ ] Game state manager class identified
|
||||
- [ ] Main gameplay scene identified and loads without errors
|
||||
- [ ] No existing E2E infrastructure conflicts
|
||||
|
||||
## Architecture Analysis
|
||||
|
||||
- [ ] Game engine correctly detected
|
||||
- [ ] Engine version identified
|
||||
- [ ] Input system type determined (New Input System, Legacy, Custom)
|
||||
- [ ] Game state manager class located
|
||||
- [ ] Ready/initialized state property identified
|
||||
- [ ] Key domain entities catalogued for ScenarioBuilder
|
||||
|
||||
## Generated Files
|
||||
|
||||
### Directory Structure
|
||||
- [ ] `Tests/PlayMode/E2E/` directory created
|
||||
- [ ] `Tests/PlayMode/E2E/Infrastructure/` directory created
|
||||
- [ ] `Tests/PlayMode/E2E/Scenarios/` directory created
|
||||
- [ ] `Tests/PlayMode/E2E/TestData/` directory created
|
||||
|
||||
### Infrastructure Files
|
||||
- [ ] `E2E.asmdef` created with correct assembly references
|
||||
- [ ] `GameE2ETestFixture.cs` created with correct class references
|
||||
- [ ] `ScenarioBuilder.cs` created with at least placeholder methods
|
||||
- [ ] `InputSimulator.cs` created matching detected input system
|
||||
- [ ] `AsyncAssert.cs` created with core assertion methods
|
||||
|
||||
### Example and Documentation
|
||||
- [ ] `ExampleE2ETest.cs` created with working infrastructure test
|
||||
- [ ] `README.md` created with usage documentation
|
||||
|
||||
## Code Quality
|
||||
|
||||
### GameE2ETestFixture
|
||||
- [ ] Correct namespace applied
|
||||
- [ ] Correct `GameStateClass` reference
|
||||
- [ ] Correct `SceneName` default
|
||||
- [ ] `WaitForGameReady` uses correct ready property
|
||||
- [ ] `UnitySetUp` and `UnityTearDown` properly structured
|
||||
- [ ] Virtual methods for derived class customization
|
||||
|
||||
### ScenarioBuilder
|
||||
- [ ] Fluent API pattern correctly implemented
|
||||
- [ ] `Build()` executes all queued actions
|
||||
- [ ] At least one domain-specific method added (or clear TODOs)
|
||||
- [ ] `FromSaveFile` method scaffolded
|
||||
|
||||
### InputSimulator
|
||||
- [ ] Matches detected input system (New vs Legacy)
|
||||
- [ ] Mouse click simulation works
|
||||
- [ ] Button click by name works
|
||||
- [ ] Keyboard input scaffolded
|
||||
- [ ] `Reset()` method cleans up state
|
||||
|
||||
### AsyncAssert
|
||||
- [ ] `WaitUntil` includes timeout and descriptive failure
|
||||
- [ ] `WaitForValue` provides current vs expected in failure
|
||||
- [ ] `AssertNeverTrue` for negative assertions
|
||||
- [ ] Frame/physics wait utilities included
|
||||
|
||||
## Assembly Definition
|
||||
|
||||
- [ ] References main game assembly
|
||||
- [ ] References UnityEngine.TestRunner
|
||||
- [ ] References UnityEditor.TestRunner
|
||||
- [ ] References Unity.InputSystem (if applicable)
|
||||
- [ ] `UNITY_INCLUDE_TESTS` define constraint set
|
||||
- [ ] nunit.framework.dll in precompiled references
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] Project compiles without errors after scaffold
|
||||
- [ ] `ExampleE2ETests.Infrastructure_GameLoadsAndReachesReadyState` passes
|
||||
- [ ] Test appears in Test Runner under PlayMode → E2E category
|
||||
|
||||
## Documentation Quality
|
||||
|
||||
- [ ] README explains all infrastructure components
|
||||
- [ ] Quick start example is copy-pasteable
|
||||
- [ ] Extension instructions are clear
|
||||
- [ ] Troubleshooting table addresses common issues
|
||||
|
||||
## Handoff
|
||||
|
||||
- [ ] Summary output provided with all configuration values
|
||||
- [ ] Next steps clearly listed
|
||||
- [ ] Customization requirements highlighted
|
||||
- [ ] Knowledge fragments referenced
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,101 @@
|
|||
# E2E Test Infrastructure Scaffold Workflow
|
||||
|
||||
workflow:
|
||||
id: e2e-scaffold
|
||||
name: E2E Test Infrastructure Scaffold
|
||||
version: 1.0
|
||||
module: bmgd
|
||||
agent: game-qa
|
||||
|
||||
description: |
|
||||
Scaffold complete E2E testing infrastructure for an existing game project.
|
||||
Creates test fixtures, scenario builders, input simulators, and async
|
||||
assertion utilities tailored to the project's architecture.
|
||||
|
||||
triggers:
|
||||
- "ES"
|
||||
- "e2e-scaffold"
|
||||
- "scaffold e2e"
|
||||
- "e2e infrastructure"
|
||||
- "setup e2e"
|
||||
|
||||
preflight:
|
||||
- "Test framework initialized (run `test-framework` workflow first)"
|
||||
- "Game has identifiable state manager"
|
||||
- "Main gameplay scene exists"
|
||||
|
||||
knowledge_fragments:
|
||||
- "knowledge/e2e-testing.md"
|
||||
- "knowledge/unity-testing.md"
|
||||
- "knowledge/unreal-testing.md"
|
||||
- "knowledge/godot-testing.md"
|
||||
|
||||
inputs:
|
||||
game_state_class:
|
||||
description: "Primary game state manager class name"
|
||||
required: true
|
||||
example: "GameStateManager"
|
||||
|
||||
main_scene:
|
||||
description: "Scene name where core gameplay occurs"
|
||||
required: true
|
||||
example: "GameScene"
|
||||
|
||||
input_system:
|
||||
description: "Input system in use"
|
||||
required: false
|
||||
default: "auto-detect"
|
||||
options:
|
||||
- "unity-input-system"
|
||||
- "unity-legacy"
|
||||
- "unreal-enhanced"
|
||||
- "godot-input"
|
||||
- "custom"
|
||||
|
||||
outputs:
|
||||
infrastructure_files:
|
||||
description: "Generated E2E infrastructure classes"
|
||||
files:
|
||||
- "Tests/PlayMode/E2E/Infrastructure/GameE2ETestFixture.cs"
|
||||
- "Tests/PlayMode/E2E/Infrastructure/ScenarioBuilder.cs"
|
||||
- "Tests/PlayMode/E2E/Infrastructure/InputSimulator.cs"
|
||||
- "Tests/PlayMode/E2E/Infrastructure/AsyncAssert.cs"
|
||||
|
||||
assembly_definition:
|
||||
description: "E2E test assembly configuration"
|
||||
files:
|
||||
- "Tests/PlayMode/E2E/E2E.asmdef"
|
||||
|
||||
example_test:
|
||||
description: "Working example E2E test"
|
||||
files:
|
||||
- "Tests/PlayMode/E2E/ExampleE2ETest.cs"
|
||||
|
||||
documentation:
|
||||
description: "E2E testing README"
|
||||
files:
|
||||
- "Tests/PlayMode/E2E/README.md"
|
||||
|
||||
steps:
|
||||
- id: analyze
|
||||
name: "Analyze Game Architecture"
|
||||
instruction_file: "instructions.md#step-1-analyze-game-architecture"
|
||||
|
||||
- id: scaffold
|
||||
name: "Generate Infrastructure"
|
||||
instruction_file: "instructions.md#step-2-generate-infrastructure"
|
||||
|
||||
- id: example
|
||||
name: "Generate Example Test"
|
||||
instruction_file: "instructions.md#step-3-generate-example-test"
|
||||
|
||||
- id: document
|
||||
name: "Generate Documentation"
|
||||
instruction_file: "instructions.md#step-4-generate-documentation"
|
||||
|
||||
- id: complete
|
||||
name: "Output Summary"
|
||||
instruction_file: "instructions.md#step-5-output-summary"
|
||||
|
||||
validation:
|
||||
checklist: "checklist.md"
|
||||
|
|
@ -91,6 +91,18 @@ Create comprehensive test scenarios for game projects, covering gameplay mechani
|
|||
| Performance | FPS, loading times | P1 |
|
||||
| Accessibility | Assist features | P1 |
|
||||
|
||||
### E2E Journey Testing
|
||||
|
||||
**Knowledge Base Reference**: `knowledge/e2e-testing.md`
|
||||
|
||||
| Category | Focus | Priority |
|
||||
|----------|-------|----------|
|
||||
| Core Loop | Complete gameplay cycle | P0 |
|
||||
| Turn Lifecycle | Full turn from start to end | P0 |
|
||||
| Save/Load Round-trip | Save → quit → load → resume | P0 |
|
||||
| Scene Transitions | Menu → Game → Back | P1 |
|
||||
| Win/Lose Paths | Victory and defeat conditions | P1 |
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Create Test Scenarios
|
||||
|
|
@ -153,6 +165,39 @@ SCENARIO: Gameplay Under High Latency
|
|||
CATEGORY: multiplayer
|
||||
```
|
||||
|
||||
### E2E Scenario Format
|
||||
|
||||
For player journey tests, use this extended format:
|
||||
```
|
||||
E2E SCENARIO: [Player Journey Name]
|
||||
GIVEN [Initial game state - use ScenarioBuilder terms]
|
||||
WHEN [Sequence of player actions]
|
||||
THEN [Observable outcomes]
|
||||
TIMEOUT: [Expected max duration in seconds]
|
||||
PRIORITY: P0/P1
|
||||
CATEGORY: e2e
|
||||
INFRASTRUCTURE: [Required fixtures/builders]
|
||||
```
|
||||
|
||||
### Example E2E Scenario
|
||||
```
|
||||
E2E SCENARIO: Complete Combat Encounter
|
||||
GIVEN game loaded with player unit adjacent to enemy
|
||||
AND player unit has full health and actions
|
||||
WHEN player selects unit
|
||||
AND player clicks attack on enemy
|
||||
AND player confirms attack
|
||||
AND attack animation completes
|
||||
AND enemy responds (if alive)
|
||||
THEN enemy health is reduced OR enemy is defeated
|
||||
AND turn state advances appropriately
|
||||
AND UI reflects new state
|
||||
TIMEOUT: 15
|
||||
PRIORITY: P0
|
||||
CATEGORY: e2e
|
||||
INFRASTRUCTURE: ScenarioBuilder, InputSimulator, AsyncAssert
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Prioritize Test Coverage
|
||||
|
|
@ -161,12 +206,12 @@ SCENARIO: Gameplay Under High Latency
|
|||
|
||||
**Knowledge Base Reference**: `knowledge/test-priorities.md`
|
||||
|
||||
| Priority | Criteria | Coverage Target |
|
||||
| -------- | ---------------------------- | --------------- |
|
||||
| P0 | Ship blockers, certification | 100% automated |
|
||||
| P1 | Major features, common paths | 80% automated |
|
||||
| P2 | Secondary features | 60% automated |
|
||||
| P3 | Edge cases, polish | Manual only |
|
||||
| Priority | Criteria | Unit | Integration | E2E | Manual |
|
||||
|----------|----------|------|-------------|-----|--------|
|
||||
| P0 | Ship blockers | 100% | 80% | Core flows | Smoke |
|
||||
| P1 | Major features | 90% | 70% | Happy paths | Full |
|
||||
| P2 | Secondary | 80% | 50% | - | Targeted |
|
||||
| P3 | Edge cases | 60% | - | - | As needed |
|
||||
|
||||
### Risk-Based Ordering
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue