# Testing Workflow Guide for YeetGeese This document provides a structured approach to testing Godot 4.x games, with specific focus on the tower defense mechanics of YeetGeese. It covers what to test, how to structure tests, and debugging failed tests. --- ## Table of Contents 1. [Testing Philosophy](#testing-philosophy) 2. [Test Organization](#test-organization) 3. [Test Categories](#test-categories) 4. [Writing Tests](#writing-tests) 5. [Debugging Failed Tests](#debugging-failed-tests) 6. [CI Integration](#ci-integration) --- ## Testing Philosophy ### Why Test? - **Regression prevention:** Catch unintended side effects from changes - **Documentation by example:** Tests serve as usage examples - **Design validation:** Ensure architecture matches intent - **Confidence for refactoring:** Safe to reorganize code when tested ### What AI Can Help With AI can: - Generate boilerplate test scaffolding - Suggest edge cases and boundary conditions - Write mock implementations for complex systems - Explain Godot testing framework capabilities ### What AI Cannot Handle AI should NOT: - Design game mechanics or balance (human domain) - Make subjective quality-of-life judgments - Replace manual playtesting of actual gameplay feel - Generate test data for art assets (no AI-generated art rule applies) --- ## Test Organization ### Directory Structure ``` tests/ ├── core/ # Autoloads and global state tests │ ├── wave_progression_test.gd │ └── currency_manager_test.gd ├── towers/ # Tower behavior tests │ ├── tower_base_test.gd │ ├── projectile_spawner_test.gd │ └── target_finder_test.gd ├── enemies/ # Enemy system tests │ ├── enemy_mob_test.gd │ ├── navigation_system_test.gd │ └── spawner_test.gd ├── projectiles/ # Projectile handling tests │ ├── goose_projectile_test.gd │ └── impact_handler_test.gd ├── ui/ # HUD and menu tests │ ├── health_bar_test.gd │ └── currency_display_test.gd ├── utils/ # Utility functions │ └── math_utils_test.gd └── integration/ # Cross-system tests ├── wave_complete_sequence_test.gd └── tower_placement_integration_test.gd ``` ### Naming Conventions - Files: `_test.gd` (e.g., `tower_base_test.gd`) - Functions: `func test_()` (e.g., `func test_fire_rate_scales_with_level()`) - Assertions: Use descriptive names like `assert_equal`, `assert_signal_emitted`, `assert_no_error` --- ## Test Categories ### Unit Tests Test individual functions or methods in isolation. **Example:** Testing a tower's fire rate calculation ```gdscript @tool extends Node func test_fire_rate_calculation(tower: Node) -> void: assert_equal(tower.fire_rate, 1.0) # Modify tower level tower.tower_level = 2 # Verify fire rate changed appropriately assert_greater(tower.fire_rate, 1.0) assert_less(tower.fire_rate, 2.0) func test_damage_scaling_by_tower_level(tower: Node) -> void: tower.tower_level = 3 var initial_damage = tower.get_damage_at_current_level() tower.tower_level = 5 # Verify damage increased with level assert_greater(tower.get_damage_at_current_level(), initial_damage) ``` **When to use:** - Testing pure logic (math, state transitions) - Testing Resource loading and property access - Testing signal emission patterns --- ### Integration Tests Test interactions between multiple systems. **Example:** Testing the full projectile launch sequence ```gdscript @tool extends Node func test_projectile_launch_sequence() -> void: # Arrange: Create mock tower and target var tower := _create_mock_tower() var target := _create_mock_target() # Act: Fire at target tower.fire_at(target) # Assert: Verify projectile spawned in correct state assert_true(tower.projectile_count == 1) assert_equal(tower.projectiles[0].source_node, tower.$ProjectileSpawner) ``` **When to use:** - Testing scene hierarchies with multiple nodes - Verifying signal connections between systems - Validating Godot's built-in component behavior (e.g., RigidBody2D movement) --- ### Regression Tests Tests added when a bug is fixed to ensure it doesn't return. **Example:** Preventing physics tunneling after a collision fix ```gdscript @tool extends Node func test_projectile_collision_prevents_tunneling() -> void: # Scenario: Goose projectile moving at high speed var projectile := _create_goose_with_velocity(100.0) var enemy := _create_enemy_with_small_hitbox() # Act: Move projectile toward enemy's hitbox projectile.yeet_from(projectile.position, enemy.position) # Assert: Projectile detected collision on this frame assert_signal_emitted(projectile, 'collision_detected') ``` **When to use:** - After fixing a critical bug - When performance regression occurs - To validate edge case handling --- ## Writing Tests ### Test Function Template ```gdscript @tool extends Node func test_() -> void: # Arrange: Setup the scenario var := _create_mock_() # Act: Execute the action being tested .() # Assert: Verify expected behavior assert_true() assert_false() assert_signal_emitted(, '') ``` ### Available Assertions | Assertion | Purpose | Example | |-----------|---------|---------| | `assert_equal(a, b)` | Check exact equality | `assert_equal(tower.level, 3)` | | `assert_not_equal(a, b)` | Check inequality | `assert_not_equal(projectile.speed, 0.0)` | | `assert_true(condition)` | Verify boolean is true | `assert_true(tower.is_ready())` | | `assert_false(condition)` | Verify boolean is false | `assert_false(projectile.is_removed())` | | `assert_greater(a, b)` | Check a > b | `assert_greater(damage_dealt, 10)` | | `assert_less(a, b)` | Check a < b | `assert_less(enemy.health, 50.0)` | | `assert_null(obj)` | Verify object is null | `assert_null(projectile.get_target())` | | `assert_not_null(obj)` | Verify object exists | `assert_not_null(projectile)` | | `assert_array_size(arr, size)` | Check array length | `assert_array_size(tower.projectiles, 4)` | ### Signal Testing Pattern ```gdscript var _signal_verified = false func test_health_changed_signal_emits_correct_values() -> void: var enemy := EnemyMob.new() enemy.health_changed.connect(func(new_health) { assert_true(_signal_verified == false) # Prevent double-firing assert_equal(new_health, 75.0) _signal_verified = true }) enemy.take_damage(25.0) assert_true(_signal_verified) ``` ### Mock Object Helpers Use these helper functions to create mock objects for tests: ```gdscript # Create a mock tower with minimal setup func _create_mock_tower() -> Node: var tower := TowerBase.new() tower.tower_level = 1 return tower # Create a mock enemy for collision testing func _create_mock_enemy() -> EnemyMob: var enemy := EnemyMob.new() enemy.health = 50.0 enemy.damage_taken.connect(func() { pass }) # Prevent death on damage return enemy # Verify no error occurred during test func _assert_no_error(condition: bool) -> void: assert_true(condition, "Test completed with errors") ``` --- ## Debugging Failed Tests ### Common Failure Patterns #### 1. Test Completes Before Signal Fires **Problem:** `signal_emitted` verification fails because the test ends too early. **Solution:** Add a yield or use Godot's built-in coroutine system: ```gdscript func test_signal_fires_after_delay() -> void: var subject := _create_subject() subject.event.triggered.connect(_on_event_triggered) subject.do_something() # Ensure signal fires before test ends get_tree().process_frame.connect(_on_frame_processed) yield(get_tree(), "process_frame") # Wait for signal assert_true(_signal_received) func _on_frame_processed() -> void: _on_event_triggered.call_deferred() ``` #### 2. Scene Loading in Tests Fails **Problem:** Godot scenes loaded via `load("res://...")` may not find their resources. **Solution:** Use absolute paths or ensure test assets are in proper location: ```gdscript # Bad: Relative path that breaks in tests var mob_scene := load("enemy_mob.tscn").instantiate() # Good: Absolute resource path var mob_scene := load("res://src/enemies/enemy_mob.tscn").instantiate() ``` #### 3. Random Behavior Causes Flaky Tests **Problem:** Tests pass sometimes but fail others due to randomness (e.g., particle emission). **Solution:** Disable randomness in test environment: ```gdscript # At start of test, seed RNG for reproducibility Rand.randi = func() -> int: return 42 # Fixed value for testing # Or disable randomness for specific systems var projectile := Projectile.new() projectile.random_variation_enabled = false ``` #### 4. Physics Tunneling in Collision Tests **Problem:** Fast-moving projectiles miss collisions when tested with static enemies. **Solution:** Use multiple physics frames or smaller time steps: ```gdscript func test_high_velocity_collision_detection() -> void: var projectile := _create_goose_with_velocity(200.0) # Run multiple physics frames for collision detection for _ in range(5): get_tree().physics_ticks_processed = 1 get_tree().process_frame() assert_true(_collision_detected) ``` #### 5. Signal Connections Leak Between Tests **Problem:** Test A's signal handlers remain active when Test B runs. **Solution:** Disconnect signals at test end: ```gdscript func cleanup_test(subject: Node, signal_name: String) -> void: subject.signal_removed.connect(_disconnect_signals) # Clear all signals from subject var signals = subject.get_signal_list() for s in signals: if s.is_connected(func): s.disconnect(func) # Call cleanup at end of each test function ``` --- ## CI Integration (Optional for Indie Teams) ### GitHub Actions / GitLab CI Setup If using a continuous integration server, add tests to build pipeline. **Example workflow:** ```yaml name: YeetGeese Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Godot run: | # Download and install Godot 4.x stable - name: Run Tests run: | godot --path . --headless tests/run_all_tests.gd ``` ### Local Test Runner Script Create a script to run all tests from command line: ```gdscript # tests/run_all_tests.gd extends Node func _ready(): var result := 0 # Run unit tests for file in DirAccess.get_directories() if dir_name == "core" or dir_name == "towers": pass # Implementation depends on test runner framework if result == 0: print("All tests passed!") else: error("Some tests failed") func _exit_tree(): quit(0) ``` --- ## Testing Checklist ### Before Writing Tests for New Feature - [ ] Identify the system(s) being tested - [ ] Determine unit vs. integration testing needs - [ ] List expected inputs and outputs - [ ] Identify edge cases (empty arrays, null objects, etc.) ### Test Coverage Goals | System Type | Recommended Test Coverage | |-------------|---------------------------| | Core game loop | 100% (critical path) | | Tower logic | 90%+ | | Enemy AI states | 85%+ | | UI displays | 80%+ | | Utility functions | 95%+ | ### Review Checklist for AI-Generated Tests When asking AI to generate tests, verify: - [ ] Uses `@tool` directive for editor-only execution - [ ] Sets up proper mock objects for isolation - [ ] Disconnects signals at test end to prevent leaks - [ ] Uses absolute paths for resource loading - [ ] Handles expected error conditions gracefully - [ ] Includes assertions for all critical behaviors --- ## Summary **AI can help with:** - Generating boilerplate test scaffolding - Suggesting edge cases and boundary conditions - Writing mock implementations for complex systems - Explaining Godot testing framework capabilities **Remember:** - Tests should be runnable in editor (`@tool` functions) - Keep tests focused on one behavior per function - Disconnect signals to prevent cross-test contamination - Use absolute paths for resource loading in tests - Document test intent with clear, descriptive names For more details, see `AI_HELP.md` for prompt templates when working with AI assistants on testing tasks.