From f05289abc2f597d683650de20623e1f66f24c0bb Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sat, 7 Feb 2026 23:47:32 +0000 Subject: [PATCH] Update Writing Tests --- Writing-Tests.-.md => Writing-Tests.md | 674 ++++++++++++------------- 1 file changed, 337 insertions(+), 337 deletions(-) rename Writing-Tests.-.md => Writing-Tests.md (95%) diff --git a/Writing-Tests.-.md b/Writing-Tests.md similarity index 95% rename from Writing-Tests.-.md rename to Writing-Tests.md index cba06d2..0cc4799 100644 --- a/Writing-Tests.-.md +++ b/Writing-Tests.md @@ -1,338 +1,338 @@ -# Writing Tests - -Guide to creating automated tests for McRogueFace using Python and the automation API. - -## Quick Reference - -**Test Location:** `tests/` directory (NOT `build/tests/` - that gets shipped!) - -**Test Types:** -1. **Direct Execution** - No game loop, immediate results -2. **Timer-Based** - Requires rendering/game loop - -**Key Tools:** -- `mcrfpy.automation` - Screenshot and input automation -- `--headless` flag - Run without display -- `--exec` flag - Execute specific script - -**Format:** See CLAUDE.md "Testing Guidelines" section - ---- - -## Test Type 1: Direct Execution - -For tests that don't need rendering or game state. - -### Template - -```python -"""Test description goes here.""" -import mcrfpy -import sys - -def test_feature(): - # Setup - scene = mcrfpy.Scene("test") - frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) - scene.children.append(frame) - - # Test - frame.x = 42 - assert frame.x == 42, f"Expected 42, got {frame.x}" - - return True - -if __name__ == "__main__": - try: - if test_feature(): - print("PASS") - sys.exit(0) - except Exception as e: - print(f"FAIL: {e}") - import traceback - traceback.print_exc() - sys.exit(1) -``` - -### Running - -```bash -cd build -./mcrogueface --headless --exec ../tests/unit/test_myfeature.py -``` - ---- - -## Test Type 2: Timer-Based Tests - -For tests requiring rendering, screenshots, or elapsed time. - -### Template - -```python -"""Test requiring game loop.""" -import mcrfpy -from mcrfpy import automation -import sys - -# Setup scene BEFORE game loop starts -scene = mcrfpy.Scene("test") -ui = scene.children - -frame = mcrfpy.Frame(pos=(100, 100), size=(200, 150), - fill_color=mcrfpy.Color(255, 0, 0)) -ui.append(frame) -mcrfpy.current_scene = scene - -def run_test(timer, runtime): - """Timer callback receives (timer_object, runtime_ms).""" - automation.screenshot("test_output.png") - - # Verify results - assert frame.x == 100 - - print("PASS") - sys.exit(0) # MUST exit! - -# Schedule test to run after game loop starts -test_timer = mcrfpy.Timer("test_runner", run_test, 100) -``` - -**Important:** Timer callbacks receive `(timer, runtime_ms)`. Screenshots only work after rendering starts. - -### Example: Testing Click Events - -```python -import mcrfpy -from mcrfpy import automation -import sys - -clicks_received = [] - -scene = mcrfpy.Scene("test") -frame = mcrfpy.Frame(pos=(100, 100), size=(200, 150), - fill_color=mcrfpy.Color(0, 255, 0)) - -def on_click(pos, button, action): - clicks_received.append((pos, button, action)) - -frame.on_click = on_click -scene.children.append(frame) -mcrfpy.current_scene = scene - -def run_test(timer, runtime): - # Simulate click on frame center - automation.click(200, 175) - # Give it a frame to process - verify_timer = mcrfpy.Timer("verify", verify_results, 32) - -def verify_results(timer, runtime): - assert len(clicks_received) > 0, "No clicks received!" - pos, button, action = clicks_received[0] - print(f"Click received at ({pos.x}, {pos.y})") - print("PASS") - sys.exit(0) - -test_timer = mcrfpy.Timer("test", run_test, 100) -``` - ---- - -## Automation API - -### Screenshots - -```python -from mcrfpy import automation - -# Take screenshot (synchronous in headless mode) -automation.screenshot("output.png") -``` - -### Mouse Input - -```python -from mcrfpy import automation - -automation.click(x, y) # Left click at position -automation.move(x, y) # Move mouse to position -``` - -### Keyboard Input - -```python -from mcrfpy import automation - -automation.keypress(key_code) # Simulate key press -``` - ---- - -## Testing Patterns - -### Pattern 1: Property Round-Trip - -```python -def test_property_roundtrip(): - obj = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) - test_values = [0, 50, 100, 255, 127] - for value in test_values: - obj.x = value - assert obj.x == value, f"Failed for {value}" -``` - -### Pattern 2: Exception Testing - -```python -def test_invalid_input(): - grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(160, 160)) - try: - grid.at(-1, -1) - assert False, "Should have raised exception" - except Exception: - pass # Expected -``` - -### Pattern 3: Grid Operations - -```python -def test_grid_walkable(): - grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(160, 160)) - grid.at(5, 5).walkable = True - assert grid.at(5, 5).walkable == True - - grid.at(5, 5).walkable = False - assert grid.at(5, 5).walkable == False -``` - -### Pattern 4: Entity Lifecycle - -```python -def test_entity_lifecycle(): - grid = mcrfpy.Grid(grid_size=(20, 20), pos=(0, 0), size=(320, 320)) - - # Create and add - entity = mcrfpy.Entity(grid_pos=(5, 5), sprite_index=0) - assert entity.grid is None - grid.entities.append(entity) - assert entity.grid is not None - - # Move - entity.grid_x = 10 - assert entity.grid_x == 10 - - # Remove - entity.die() - assert entity.grid is None -``` - -### Pattern 5: Callback Setup - -```python -def test_click_callbacks(): - frame = mcrfpy.Frame(pos=(100, 100), size=(200, 150)) - clicks = [] - - # on_click receives (pos: Vector, button: MouseButton, action: InputState) - def on_click(pos, button, action): - if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: - clicks.append((pos.x, pos.y)) - - frame.on_click = on_click - # on_enter/on_exit receive (pos: Vector) - frame.on_enter = lambda pos: None - frame.on_exit = lambda pos: None -``` - -### Pattern 6: Timer Usage - -```python -def test_timer(): - fired = [False] - - def on_timer(timer, runtime): - fired[0] = True - - t = mcrfpy.Timer("test_timer", on_timer, 100) - - # In headless mode, use step() to advance time - mcrfpy.step(0.2) # 200ms - assert fired[0], "Timer should have fired" - - t.stop() # Clean up -``` - -### Pattern 7: Animation Testing - -```python -def test_animation(): - frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) - frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT) - - # In headless mode, advance time to test - mcrfpy.step(1.0) # Halfway through - assert frame.x > 0 # Should have moved - - mcrfpy.step(1.0) # Complete - # frame.x should be at or near 500 -``` - ---- - -## Test-Driven Development (TDD) - -### TDD Workflow - -1. **Write failing test** - Demonstrates the bug/missing feature -2. **Run test** - Verify it fails -3. **Implement fix** - Make minimum change to pass -4. **Run test** - Verify it passes -5. **Refactor** - Clean up -6. **Run full suite** - `cd tests && python3 run_tests.py` - ---- - -## Testing Best Practices - -### DO: - -- **Test one thing at a time** - Each test function covers one behavior -- **Use descriptive names** - `test_entity_moves_to_valid_position()` -- **Always exit** - Use `sys.exit(0)` for pass, `sys.exit(1)` for fail -- **Test edge cases** - Boundaries, empty states, invalid input -- **Clean up timers** - Call `timer.stop()` when done - -### DON'T: - -- **Rely on timing** - Use `step()` in headless mode, not `time.sleep()` -- **Forget sys.exit()** in timer tests - Will hang indefinitely -- **Test multiple unrelated things** in one test function -- **Put tests in build/** - They get shipped with the game - ---- - -## Running the Test Suite - -```bash -# Full suite -cd tests && python3 run_tests.py - -# Single test -cd build && ./mcrogueface --headless --exec ../tests/unit/my_test.py - -# Inline test -cd build && ./mcrogueface --headless -c "import mcrfpy; print('OK')" -``` - ---- - -## Related Documentation - -- [[Headless-Mode]] - step() function and headless operation -- [[Input-and-Events]] - Event handler signatures -- [[Animation-System]] - Animation testing -- CLAUDE.md - Testing guidelines section - ---- - +# Writing Tests + +Guide to creating automated tests for McRogueFace using Python and the automation API. + +## Quick Reference + +**Test Location:** `tests/` directory (NOT `build/tests/` - that gets shipped!) + +**Test Types:** +1. **Direct Execution** - No game loop, immediate results +2. **Timer-Based** - Requires rendering/game loop + +**Key Tools:** +- `mcrfpy.automation` - Screenshot and input automation +- `--headless` flag - Run without display +- `--exec` flag - Execute specific script + +**Format:** See CLAUDE.md "Testing Guidelines" section + +--- + +## Test Type 1: Direct Execution + +For tests that don't need rendering or game state. + +### Template + +```python +"""Test description goes here.""" +import mcrfpy +import sys + +def test_feature(): + # Setup + scene = mcrfpy.Scene("test") + frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) + scene.children.append(frame) + + # Test + frame.x = 42 + assert frame.x == 42, f"Expected 42, got {frame.x}" + + return True + +if __name__ == "__main__": + try: + if test_feature(): + print("PASS") + sys.exit(0) + except Exception as e: + print(f"FAIL: {e}") + import traceback + traceback.print_exc() + sys.exit(1) +``` + +### Running + +```bash +cd build +./mcrogueface --headless --exec ../tests/unit/test_myfeature.py +``` + +--- + +## Test Type 2: Timer-Based Tests + +For tests requiring rendering, screenshots, or elapsed time. + +### Template + +```python +"""Test requiring game loop.""" +import mcrfpy +from mcrfpy import automation +import sys + +# Setup scene BEFORE game loop starts +scene = mcrfpy.Scene("test") +ui = scene.children + +frame = mcrfpy.Frame(pos=(100, 100), size=(200, 150), + fill_color=mcrfpy.Color(255, 0, 0)) +ui.append(frame) +mcrfpy.current_scene = scene + +def run_test(timer, runtime): + """Timer callback receives (timer_object, runtime_ms).""" + automation.screenshot("test_output.png") + + # Verify results + assert frame.x == 100 + + print("PASS") + sys.exit(0) # MUST exit! + +# Schedule test to run after game loop starts +test_timer = mcrfpy.Timer("test_runner", run_test, 100) +``` + +**Important:** Timer callbacks receive `(timer, runtime_ms)`. Screenshots only work after rendering starts. + +### Example: Testing Click Events + +```python +import mcrfpy +from mcrfpy import automation +import sys + +clicks_received = [] + +scene = mcrfpy.Scene("test") +frame = mcrfpy.Frame(pos=(100, 100), size=(200, 150), + fill_color=mcrfpy.Color(0, 255, 0)) + +def on_click(pos, button, action): + clicks_received.append((pos, button, action)) + +frame.on_click = on_click +scene.children.append(frame) +mcrfpy.current_scene = scene + +def run_test(timer, runtime): + # Simulate click on frame center + automation.click(200, 175) + # Give it a frame to process + verify_timer = mcrfpy.Timer("verify", verify_results, 32) + +def verify_results(timer, runtime): + assert len(clicks_received) > 0, "No clicks received!" + pos, button, action = clicks_received[0] + print(f"Click received at ({pos.x}, {pos.y})") + print("PASS") + sys.exit(0) + +test_timer = mcrfpy.Timer("test", run_test, 100) +``` + +--- + +## Automation API + +### Screenshots + +```python +from mcrfpy import automation + +# Take screenshot (synchronous in headless mode) +automation.screenshot("output.png") +``` + +### Mouse Input + +```python +from mcrfpy import automation + +automation.click(x, y) # Left click at position +automation.move(x, y) # Move mouse to position +``` + +### Keyboard Input + +```python +from mcrfpy import automation + +automation.keypress(key_code) # Simulate key press +``` + +--- + +## Testing Patterns + +### Pattern 1: Property Round-Trip + +```python +def test_property_roundtrip(): + obj = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) + test_values = [0, 50, 100, 255, 127] + for value in test_values: + obj.x = value + assert obj.x == value, f"Failed for {value}" +``` + +### Pattern 2: Exception Testing + +```python +def test_invalid_input(): + grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(160, 160)) + try: + grid.at(-1, -1) + assert False, "Should have raised exception" + except Exception: + pass # Expected +``` + +### Pattern 3: Grid Operations + +```python +def test_grid_walkable(): + grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(160, 160)) + grid.at(5, 5).walkable = True + assert grid.at(5, 5).walkable == True + + grid.at(5, 5).walkable = False + assert grid.at(5, 5).walkable == False +``` + +### Pattern 4: Entity Lifecycle + +```python +def test_entity_lifecycle(): + grid = mcrfpy.Grid(grid_size=(20, 20), pos=(0, 0), size=(320, 320)) + + # Create and add + entity = mcrfpy.Entity(grid_pos=(5, 5), sprite_index=0) + assert entity.grid is None + grid.entities.append(entity) + assert entity.grid is not None + + # Move + entity.grid_x = 10 + assert entity.grid_x == 10 + + # Remove + entity.die() + assert entity.grid is None +``` + +### Pattern 5: Callback Setup + +```python +def test_click_callbacks(): + frame = mcrfpy.Frame(pos=(100, 100), size=(200, 150)) + clicks = [] + + # on_click receives (pos: Vector, button: MouseButton, action: InputState) + def on_click(pos, button, action): + if button == mcrfpy.MouseButton.LEFT and action == mcrfpy.InputState.PRESSED: + clicks.append((pos.x, pos.y)) + + frame.on_click = on_click + # on_enter/on_exit receive (pos: Vector) + frame.on_enter = lambda pos: None + frame.on_exit = lambda pos: None +``` + +### Pattern 6: Timer Usage + +```python +def test_timer(): + fired = [False] + + def on_timer(timer, runtime): + fired[0] = True + + t = mcrfpy.Timer("test_timer", on_timer, 100) + + # In headless mode, use step() to advance time + mcrfpy.step(0.2) # 200ms + assert fired[0], "Timer should have fired" + + t.stop() # Clean up +``` + +### Pattern 7: Animation Testing + +```python +def test_animation(): + frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) + frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT) + + # In headless mode, advance time to test + mcrfpy.step(1.0) # Halfway through + assert frame.x > 0 # Should have moved + + mcrfpy.step(1.0) # Complete + # frame.x should be at or near 500 +``` + +--- + +## Test-Driven Development (TDD) + +### TDD Workflow + +1. **Write failing test** - Demonstrates the bug/missing feature +2. **Run test** - Verify it fails +3. **Implement fix** - Make minimum change to pass +4. **Run test** - Verify it passes +5. **Refactor** - Clean up +6. **Run full suite** - `cd tests && python3 run_tests.py` + +--- + +## Testing Best Practices + +### DO: + +- **Test one thing at a time** - Each test function covers one behavior +- **Use descriptive names** - `test_entity_moves_to_valid_position()` +- **Always exit** - Use `sys.exit(0)` for pass, `sys.exit(1)` for fail +- **Test edge cases** - Boundaries, empty states, invalid input +- **Clean up timers** - Call `timer.stop()` when done + +### DON'T: + +- **Rely on timing** - Use `step()` in headless mode, not `time.sleep()` +- **Forget sys.exit()** in timer tests - Will hang indefinitely +- **Test multiple unrelated things** in one test function +- **Put tests in build/** - They get shipped with the game + +--- + +## Running the Test Suite + +```bash +# Full suite +cd tests && python3 run_tests.py + +# Single test +cd build && ./mcrogueface --headless --exec ../tests/unit/my_test.py + +# Inline test +cd build && ./mcrogueface --headless -c "import mcrfpy; print('OK')" +``` + +--- + +## Related Documentation + +- [[Headless-Mode]] - step() function and headless operation +- [[Input-and-Events]] - Event handler signatures +- [[Animation-System]] - Animation testing +- CLAUDE.md - Testing guidelines section + +--- + *Last updated: 2026-02-07* \ No newline at end of file