Page:
Writing Tests
Pages
AI and Pathfinding
Adding Python Bindings
Animation System
Design Proposals
Development Workflow
Entity Management
Grid Interaction Patterns
Grid Rendering Pipeline
Grid System
Grid TCOD Integration
Headless Mode
Home
Input and Events
Issue Roadmap
LLM Agent Testbed Architecture
Performance Optimization Workflow
Performance and Profiling
Procedural-Generation
Proposal: Next Generation Grid & Entity System
Python Binding Layer
Rendering and Visuals
Strategic Direction
UI Component Hierarchy
UI Widget Patterns
Writing Tests
No results
3
Writing Tests
John McCardle edited this page 2026-02-07 23:47:32 +00:00
Table of Contents
- Writing Tests
- Quick Reference
- Test Type 1: Direct Execution
- Test Type 2: Timer-Based Tests
- Automation API
- Testing Patterns
- Pattern 1: Property Round-Trip
- Pattern 2: Exception Testing
- Pattern 3: Grid Operations
- Pattern 4: Entity Lifecycle
- Pattern 5: Callback Setup
- Pattern 6: Timer Usage
- Pattern 7: Animation Testing
- Test-Driven Development (TDD)
- Testing Best Practices
- Running the Test Suite
- Related Documentation
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:
- Direct Execution - No game loop, immediate results
- Timer-Based - Requires rendering/game loop
Key Tools:
mcrfpy.automation- Screenshot and input automation--headlessflag - Run without display--execflag - 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
"""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
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
"""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
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
from mcrfpy import automation
# Take screenshot (synchronous in headless mode)
automation.screenshot("output.png")
Mouse Input
from mcrfpy import automation
automation.click(x, y) # Left click at position
automation.move(x, y) # Move mouse to position
Keyboard Input
from mcrfpy import automation
automation.keypress(key_code) # Simulate key press
Testing Patterns
Pattern 1: Property Round-Trip
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
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
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
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
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
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
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
- Write failing test - Demonstrates the bug/missing feature
- Run test - Verify it fails
- Implement fix - Make minimum change to pass
- Run test - Verify it passes
- Refactor - Clean up
- 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, nottime.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
# 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