# GUT (Godot Unit Testing) - Setup & Usage Guide ## Overview GUT is a unit testing framework for Godot that allows you to write and run automated tests for your GDScript code. It's already installed in your project at `addons/gut/`. **Version:** 9.6.0 **Author:** Butch Wesley **Documentation:** https://gut.readthedocs.io ## Quick Start ### 1. Enable GUT Plugin 1. Open your Godot project 2. Go to **Project → Project Settings → Plugins** 3. Find "Gut" and enable it 4. Restart the editor ### 2. Create Your First Test Create a new GDScript file in a `tests/` folder: ```gdscript # tests/test_example.gd extends GutTest func test_addition(): assert_eq(2 + 2, 4, "2 + 2 should equal 4") func test_string_comparison(): assert_eq("hello", "hello", "Strings should match") ``` ### 3. Run Tests - **In Editor:** Go to **Tools → GUT → Run Tests** - **From Command Line:** `godot --headless -s addons/gut/gut_cmdln.gd` ## GUT Test Structure ### Lifecycle Methods ```gdscript extends GutTest # Runs once before all tests in this script func before_all(): pass # Runs before each test func before_each(): pass # Runs after each test func after_each(): pass # Runs once after all tests in this script func after_all(): pass # Test methods must start with "test_" func test_something(): pass ``` ### Test Naming Convention - Test methods **must** start with `test_` - Use descriptive names: `test_player_takes_damage()`, `test_invalid_input_rejected()` - Each test should test ONE thing ## Common Assertions ### Equality Assertions ```gdscript # Assert equal assert_eq(actual, expected, "Optional message") assert_eq(2 + 2, 4) # Assert not equal assert_ne(actual, expected, "Optional message") assert_ne(1, 2) # Assert true/false assert_true(condition, "Optional message") assert_false(condition, "Optional message") ``` ### Type Assertions ```gdscript # Assert is instance of type assert_is(obj, Node, "Should be a Node") # Assert has method assert_has_method(obj, "method_name") # Assert has property assert_has_property(obj, "property_name") ``` ### Collection Assertions ```gdscript # Assert array contains value assert_has(array, value, "Array should contain value") # Assert array does not contain value assert_does_not_have(array, value) # Assert array is empty assert_is_empty(array) # Assert array is not empty assert_is_not_empty(array) ``` ### String Assertions ```gdscript # Assert string contains substring assert_string_contains(string, substring) # Assert string starts with assert_string_starts_with(string, prefix) # Assert string ends with assert_string_ends_with(string, suffix) ``` ### Signal Assertions ```gdscript # Watch a signal watch_signals(object) # Assert signal was emitted assert_signal_emitted(object, "signal_name") # Assert signal was emitted with arguments assert_signal_emitted_with_arguments(object, "signal_name", [arg1, arg2]) # Assert signal was not emitted assert_signal_not_emitted(object, "signal_name") ``` ## Project Structure for Tests Recommended folder structure: ``` tekton-enet/ ├── addons/ │ └── gut/ ├── scripts/ │ ├── player.gd │ ├── enemy.gd │ └── game_manager.gd ├── tests/ │ ├── test_player.gd │ ├── test_enemy.gd │ ├── test_game_manager.gd │ └── unit/ │ ├── test_math_utils.gd │ └── test_string_utils.gd └── project.godot ``` ## Example Test Files ### Testing a Simple Class ```gdscript # tests/test_player.gd extends GutTest var player: Node func before_each(): # Create a fresh player instance for each test player = preload("res://scripts/player.gd").new() func after_each(): # Clean up if player: player.queue_free() func test_player_starts_with_100_health(): assert_eq(player.health, 100, "Player should start with 100 health") func test_player_takes_damage(): player.take_damage(25) assert_eq(player.health, 75, "Player health should be 75 after taking 25 damage") func test_player_dies_at_zero_health(): player.health = 0 assert_true(player.is_dead(), "Player should be dead at 0 health") func test_player_cannot_have_negative_health(): player.take_damage(200) assert_true(player.health >= 0, "Player health should never be negative") ``` ### Testing with Signals ```gdscript # tests/test_game_manager.gd extends GutTest var game_manager: Node func before_each(): game_manager = preload("res://scripts/game_manager.gd").new() add_child(game_manager) func after_each(): game_manager.queue_free() func test_game_start_emits_signal(): watch_signals(game_manager) game_manager.start_game() assert_signal_emitted(game_manager, "game_started") func test_game_over_emits_with_winner(): watch_signals(game_manager) game_manager.end_game("Player1") assert_signal_emitted_with_arguments(game_manager, "game_ended", ["Player1"]) ``` ### Testing with Mocking/Stubbing ```gdscript # tests/test_enemy.gd extends GutTest var enemy: Node func before_each(): enemy = preload("res://scripts/enemy.gd").new() func test_enemy_calls_attack_on_player(): # Create a mock player var mock_player = double(preload("res://scripts/player.gd")) # Have enemy attack the mock player enemy.attack(mock_player) # Verify the mock player's take_damage was called assert_called(mock_player, "take_damage") ``` ## GUT Configuration ### gutconfig.json Create `gutconfig.json` in your project root to configure GUT: ```json { "dirs": ["res://tests"], "should_exit": true, "log_level": 1, "ignore_pause_before_teardown": false, "should_print_debug": false, "double_strategy": "FULL" } ``` **Configuration Options:** - `dirs`: Directories to search for test files - `should_exit`: Exit after tests complete (useful for CI/CD) - `log_level`: 0 (failures only), 1 (tests + failures), 2 (all asserts) - `ignore_pause_before_teardown`: Ignore pause calls in batch mode - `should_print_debug`: Print debug output - `double_strategy`: "FULL" or "PARTIAL" for mocking ## Running Tests ### In Editor 1. **Tools → GUT → Run Tests** - Runs all tests 2. **Tools → GUT → Run Tests (with options)** - Configure before running 3. **Tools → GUT → Run a specific test** - Select a test file ### Command Line ```bash # Run all tests godot --headless -s addons/gut/gut_cmdln.gd # Run specific test directory godot --headless -s addons/gut/gut_cmdln.gd -d res://tests # Run with specific log level godot --headless -s addons/gut/gut_cmdln.gd -l 2 # Export results to JUnit XML (for CI/CD) godot --headless -s addons/gut/gut_cmdln.gd -o results.xml ``` ### CI/CD Integration For GitHub Actions: ```yaml - name: Run GUT Tests run: | godot --headless -s addons/gut/gut_cmdln.gd \ -d res://tests \ -o test-results.xml ``` ## Best Practices ### 1. One Assertion Per Test (When Possible) ```gdscript # GOOD - Each test tests one thing func test_player_health_decreases(): player.take_damage(10) assert_eq(player.health, 90) func test_player_dies_at_zero(): player.health = 0 assert_true(player.is_dead()) # AVOID - Multiple assertions in one test func test_player_damage(): player.take_damage(10) assert_eq(player.health, 90) assert_false(player.is_dead()) player.take_damage(100) assert_true(player.is_dead()) ``` ### 2. Use Descriptive Test Names ```gdscript # GOOD func test_player_cannot_move_while_stunned(): pass # AVOID func test_stun(): pass ``` ### 3. Clean Up Resources ```gdscript func after_each(): # Always clean up nodes if player: player.queue_free() # Clear any global state GameManager.reset() ``` ### 4. Test Edge Cases ```gdscript func test_negative_damage_heals_player(): player.take_damage(-10) assert_eq(player.health, 110) func test_damage_exceeding_max_health(): player.take_damage(-200) assert_eq(player.health, 100) # Should cap at max ``` ### 5. Use before_each for Setup ```gdscript func before_each(): # Create fresh instances for each test player = preload("res://scripts/player.gd").new() enemy = preload("res://scripts/enemy.gd").new() add_child(player) add_child(enemy) ``` ## Debugging Tests ### Print Debug Info ```gdscript func test_something(): gut.p("This is a debug message") gut.p("Player health: " + str(player.health)) ``` ### Pause Before Teardown ```gdscript func test_something(): assert_eq(1, 1) gut.pause_before_teardown("Pausing to inspect state") ``` ### Run Single Test In the GUT GUI, right-click a test and select "Run" to run just that test. ## Common Issues & Solutions ### Issue: Tests not found **Solution:** Ensure test files are in directories specified in `gutconfig.json` and file names start with `test_`. ### Issue: Signals not being detected **Solution:** Call `watch_signals(object)` before emitting the signal. ### Issue: Nodes not being freed **Solution:** Always call `queue_free()` in `after_each()` or use `add_child()` so GUT can track them. ### Issue: Tests pass individually but fail together **Solution:** Check for shared state. Use `before_each()` to reset state between tests. ## Resources - **Official Docs:** https://gut.readthedocs.io - **GitHub:** https://github.com/bitwes/Gut - **Examples:** Check `addons/gut/examples/` for sample tests ## Next Steps 1. Create `tests/` folder in your project 2. Write your first test file 3. Run tests via **Tools → GUT → Run Tests** 4. Integrate into your development workflow 5. Add tests to CI/CD pipeline