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.
This commit is contained in:
@@ -0,0 +1,438 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user