mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-24 15:38:04 +02:00
feat: security overhaul (proxy ssrf fix, streaming sanitization, docker optimization)
This commit is contained in:
1 parent
6d87ada2b8
commit
8df9a37baa
810 files changed
+131090
-151
No files matched your search
@@ -0,0 +1,376 @@
|
||||
# Godot GUT Testing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
GUT (Godot Unit Test) is the standard unit testing framework for Godot. It provides a full-featured testing framework with assertions, mocking, and CI integration.
|
||||
|
||||
## Installation
|
||||
|
||||
### Via Asset Library
|
||||
|
||||
1. Open AssetLib in Godot
|
||||
2. Search for "GUT"
|
||||
3. Download and install
|
||||
4. Enable the plugin in Project Settings
|
||||
|
||||
### Via Git Submodule
|
||||
|
||||
```bash
|
||||
git submodule add https://github.com/bitwes/Gut.git addons/gut
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
project/
|
||||
├── addons/
|
||||
│ └── gut/
|
||||
├── src/
|
||||
│ ├── player/
|
||||
│ │ └── player.gd
|
||||
│ └── combat/
|
||||
│ └── damage_calculator.gd
|
||||
└── tests/
|
||||
├── unit/
|
||||
│ └── test_damage_calculator.gd
|
||||
└── integration/
|
||||
└── test_player_combat.gd
|
||||
```
|
||||
|
||||
## Basic Test Structure
|
||||
|
||||
### Simple Test Class
|
||||
|
||||
```gdscript
|
||||
# tests/unit/test_damage_calculator.gd
|
||||
extends GutTest
|
||||
|
||||
var calculator: DamageCalculator
|
||||
|
||||
func before_each():
|
||||
calculator = DamageCalculator.new()
|
||||
|
||||
func after_each():
|
||||
calculator.free()
|
||||
|
||||
func test_calculate_base_damage():
|
||||
var result = calculator.calculate(100.0, 1.0)
|
||||
assert_eq(result, 100.0, "Base damage should equal input")
|
||||
|
||||
func test_calculate_critical_hit():
|
||||
var result = calculator.calculate(100.0, 2.0)
|
||||
assert_eq(result, 200.0, "Critical hit should double damage")
|
||||
|
||||
func test_calculate_with_zero_multiplier():
|
||||
var result = calculator.calculate(100.0, 0.0)
|
||||
assert_eq(result, 0.0, "Zero multiplier should result in zero damage")
|
||||
```
|
||||
|
||||
### Parameterized Tests
|
||||
|
||||
```gdscript
|
||||
func test_damage_scenarios():
|
||||
var scenarios = [
|
||||
{"base": 100.0, "mult": 1.0, "expected": 100.0},
|
||||
{"base": 100.0, "mult": 2.0, "expected": 200.0},
|
||||
{"base": 50.0, "mult": 1.5, "expected": 75.0},
|
||||
{"base": 0.0, "mult": 2.0, "expected": 0.0},
|
||||
]
|
||||
|
||||
for scenario in scenarios:
|
||||
var result = calculator.calculate(scenario.base, scenario.mult)
|
||||
assert_eq(
|
||||
result,
|
||||
scenario.expected,
|
||||
"Base %s * %s should equal %s" % [
|
||||
scenario.base, scenario.mult, scenario.expected
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Testing Nodes
|
||||
|
||||
### Scene Testing
|
||||
|
||||
```gdscript
|
||||
# tests/integration/test_player.gd
|
||||
extends GutTest
|
||||
|
||||
var player: Player
|
||||
var player_scene = preload("res://src/player/player.tscn")
|
||||
|
||||
func before_each():
|
||||
player = player_scene.instantiate()
|
||||
add_child(player)
|
||||
|
||||
func after_each():
|
||||
player.queue_free()
|
||||
|
||||
func test_player_initial_health():
|
||||
assert_eq(player.health, 100, "Player should start with 100 health")
|
||||
|
||||
func test_player_takes_damage():
|
||||
player.take_damage(30)
|
||||
assert_eq(player.health, 70, "Health should be reduced by damage")
|
||||
|
||||
func test_player_dies_at_zero_health():
|
||||
player.take_damage(100)
|
||||
assert_true(player.is_dead, "Player should be dead at 0 health")
|
||||
```
|
||||
|
||||
### Testing with Signals
|
||||
|
||||
```gdscript
|
||||
func test_damage_emits_signal():
|
||||
watch_signals(player)
|
||||
|
||||
player.take_damage(10)
|
||||
|
||||
assert_signal_emitted(player, "health_changed")
|
||||
assert_signal_emit_count(player, "health_changed", 1)
|
||||
|
||||
func test_death_emits_signal():
|
||||
watch_signals(player)
|
||||
|
||||
player.take_damage(100)
|
||||
|
||||
assert_signal_emitted(player, "died")
|
||||
```
|
||||
|
||||
### Testing with Await
|
||||
|
||||
```gdscript
|
||||
func test_attack_cooldown():
|
||||
player.attack()
|
||||
assert_true(player.is_attacking)
|
||||
|
||||
# Wait for cooldown
|
||||
await get_tree().create_timer(player.attack_cooldown).timeout
|
||||
|
||||
assert_false(player.is_attacking)
|
||||
assert_true(player.can_attack)
|
||||
```
|
||||
|
||||
## Mocking and Doubles
|
||||
|
||||
### Creating Doubles
|
||||
|
||||
```gdscript
|
||||
func test_enemy_uses_pathfinding():
|
||||
var mock_pathfinding = double(Pathfinding).new()
|
||||
stub(mock_pathfinding, "find_path").to_return([Vector2(0, 0), Vector2(10, 10)])
|
||||
|
||||
var enemy = Enemy.new()
|
||||
enemy.pathfinding = mock_pathfinding
|
||||
|
||||
enemy.move_to(Vector2(10, 10))
|
||||
|
||||
assert_called(mock_pathfinding, "find_path")
|
||||
```
|
||||
|
||||
### Partial Doubles
|
||||
|
||||
```gdscript
|
||||
func test_player_inventory():
|
||||
var player_double = partial_double(Player).new()
|
||||
stub(player_double, "save_to_disk").to_do_nothing()
|
||||
|
||||
player_double.add_item("sword")
|
||||
|
||||
assert_eq(player_double.inventory.size(), 1)
|
||||
assert_called(player_double, "save_to_disk")
|
||||
```
|
||||
|
||||
## Physics Testing
|
||||
|
||||
### Testing Collision
|
||||
|
||||
```gdscript
|
||||
func test_projectile_hits_enemy():
|
||||
var projectile = Projectile.new()
|
||||
var enemy = Enemy.new()
|
||||
|
||||
add_child(projectile)
|
||||
add_child(enemy)
|
||||
|
||||
projectile.global_position = Vector2(0, 0)
|
||||
enemy.global_position = Vector2(100, 0)
|
||||
|
||||
projectile.velocity = Vector2(200, 0)
|
||||
|
||||
# Simulate physics frames
|
||||
for i in range(60):
|
||||
await get_tree().physics_frame
|
||||
|
||||
assert_true(enemy.was_hit, "Enemy should be hit by projectile")
|
||||
|
||||
projectile.queue_free()
|
||||
enemy.queue_free()
|
||||
```
|
||||
|
||||
### Testing Area2D
|
||||
|
||||
```gdscript
|
||||
func test_pickup_collected():
|
||||
var pickup = Pickup.new()
|
||||
var player = player_scene.instantiate()
|
||||
|
||||
add_child(pickup)
|
||||
add_child(player)
|
||||
|
||||
pickup.global_position = Vector2(50, 50)
|
||||
player.global_position = Vector2(50, 50)
|
||||
|
||||
# Wait for physics to process overlap
|
||||
await get_tree().physics_frame
|
||||
await get_tree().physics_frame
|
||||
|
||||
assert_true(pickup.is_queued_for_deletion(), "Pickup should be collected")
|
||||
|
||||
player.queue_free()
|
||||
```
|
||||
|
||||
## Input Testing
|
||||
|
||||
### Simulating Input
|
||||
|
||||
```gdscript
|
||||
func test_jump_on_input():
|
||||
var input_event = InputEventKey.new()
|
||||
input_event.keycode = KEY_SPACE
|
||||
input_event.pressed = true
|
||||
|
||||
Input.parse_input_event(input_event)
|
||||
await get_tree().process_frame
|
||||
|
||||
player._unhandled_input(input_event)
|
||||
|
||||
assert_true(player.is_jumping, "Player should jump on space press")
|
||||
```
|
||||
|
||||
### Testing Input Actions
|
||||
|
||||
```gdscript
|
||||
func test_attack_action():
|
||||
# Simulate action press
|
||||
Input.action_press("attack")
|
||||
await get_tree().process_frame
|
||||
|
||||
player._process(0.016)
|
||||
|
||||
assert_true(player.is_attacking)
|
||||
|
||||
Input.action_release("attack")
|
||||
```
|
||||
|
||||
## Resource Testing
|
||||
|
||||
### Testing Custom Resources
|
||||
|
||||
```gdscript
|
||||
func test_weapon_stats_resource():
|
||||
var weapon = WeaponStats.new()
|
||||
weapon.base_damage = 10.0
|
||||
weapon.attack_speed = 2.0
|
||||
|
||||
assert_eq(weapon.dps, 20.0, "DPS should be damage * speed")
|
||||
|
||||
func test_save_load_resource():
|
||||
var original = PlayerData.new()
|
||||
original.level = 5
|
||||
original.gold = 1000
|
||||
|
||||
ResourceSaver.save(original, "user://test_save.tres")
|
||||
var loaded = ResourceLoader.load("user://test_save.tres")
|
||||
|
||||
assert_eq(loaded.level, 5)
|
||||
assert_eq(loaded.gold, 1000)
|
||||
|
||||
DirAccess.remove_absolute("user://test_save.tres")
|
||||
```
|
||||
|
||||
## GUT Configuration
|
||||
|
||||
### gut_config.json
|
||||
|
||||
```json
|
||||
{
|
||||
"dirs": ["res://tests/"],
|
||||
"include_subdirs": true,
|
||||
"prefix": "test_",
|
||||
"suffix": ".gd",
|
||||
"should_exit": true,
|
||||
"should_exit_on_success": true,
|
||||
"log_level": 1,
|
||||
"junit_xml_file": "results.xml",
|
||||
"font_size": 16
|
||||
}
|
||||
```
|
||||
|
||||
## CI Integration
|
||||
|
||||
### Command Line Execution
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
godot --headless -s addons/gut/gut_cmdln.gd
|
||||
|
||||
# Run specific tests
|
||||
godot --headless -s addons/gut/gut_cmdln.gd \
|
||||
-gdir=res://tests/unit \
|
||||
-gprefix=test_
|
||||
|
||||
# With JUnit output
|
||||
godot --headless -s addons/gut/gut_cmdln.gd \
|
||||
-gjunit_xml_file=results.xml
|
||||
```
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: barichello/godot-ci:4.2
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
godot --headless -s addons/gut/gut_cmdln.gd \
|
||||
-gjunit_xml_file=results.xml
|
||||
|
||||
- name: Publish Results
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
with:
|
||||
report_paths: results.xml
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO
|
||||
|
||||
- Use `before_each`/`after_each` for setup/teardown
|
||||
- Free nodes after tests to prevent leaks
|
||||
- Use meaningful assertion messages
|
||||
- Group related tests in the same file
|
||||
- Use `watch_signals` for signal testing
|
||||
- Await physics frames when testing physics
|
||||
|
||||
### DON'T
|
||||
|
||||
- Don't test Godot's built-in functionality
|
||||
- Don't rely on execution order between test files
|
||||
- Don't leave orphan nodes
|
||||
- Don't use `yield` (use `await` in Godot 4)
|
||||
- Don't test private methods directly
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Cause | Fix |
|
||||
| -------------------- | ------------------ | ------------------------------------ |
|
||||
| Tests not found | Wrong prefix/path | Check gut_config.json |
|
||||
| Orphan nodes warning | Missing cleanup | Add `queue_free()` in `after_each` |
|
||||
| Signal not detected | Signal not watched | Call `watch_signals()` before action |
|
||||
| Physics not working | Missing frames | Await `physics_frame` |
|
||||
| Flaky tests | Timing issues | Use proper await/signals |
|
||||
Reference in new issue
Block a user