Files
tekton/skills/GUT_SETUP_SKILLS.md
T
adtpdn decdb74ade chore: release version 2.3.5 and refactor lobby
Bump export_presets.cfg version to 2.3.5. Update CHANGELOG_DRAFT.md.
Refactor lobby.gd into LobbyChat, LobbyMainMenu, LobbyRoomList, LobbyRoom.
Move Nakama config to environment variables in nakama_manager.gd.
Derive auth_manager.gd encryption key from OS.get_unique_id().sha256_text().
Remove Steam email auth fallback. Require auth ticket.
Make GachaManager.pull() async in gacha_panel.gd.
Remove dummy wallet seeding. Add store_type to IAP payload.
Validate IAP receipts server-side in economy.lua.
Register gacha module in main.lua.
Clean backend_service.gd stubs.
Fix featured_banners type safety in gacha_manager.gd. Guards non-array responses.
Move tiles_armagedon_a1.res to assets/models/meshes/. Fix import fallback_path.
2026-05-22 12:08:11 +08:00

439 lines
9.5 KiB
Markdown

# 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