Replace the broken test suite with working GdUnit4 tests

The previous tests never compiled: they used GUT's assert functions,
Godot 3 APIs and reassigned autoload functions, so no test ran.

- 90 tests: unit tests for the playlist, songs, player scoring, time
  formatting, search and the server client, plus integration tests
  that run MainWindow and SyncWindow against a fake server
- Keybind tests check that Alt+<number> and Ctrl+Alt+<number> keep
  following the player rows while players are added and removed
- run_tests.sh finds Godot and runs the suite headless (~10 seconds)
- CI runs the tests before the build job in gitea-build.yaml; the
  separate test.yml used a GitHub action that does not exist
- GdUnit4 is committed instead of ignored, since the project enables
  it, and .gitea/ is no longer ignored
- Exported builds leave out tests/ and addons/gdUnit4/
This commit is contained in:
Sansan committed 2026-10-06 08:32:56 +02:00
1 parent 56e9fe66d3
commit c985a6ce5b
559 files changed
+32474 -2100

No files matched your search

+45 -325
View File
@@ -1,343 +1,63 @@
# MusicPlayer Testing Guide
# Testing
This document describes the testing setup for the MusicPlayer project using **GdUnit4**.
Tests use [GdUnit4](https://github.com/godot-gdunit-labs/gdUnit4) 6.2.1, committed in
`addons/gdUnit4/`. The whole suite runs headless in about 10 seconds.
## Testing Framework
- **Framework**: [GdUnit4 v6.2.1](https://github.com/godot-gdunit-labs/gdUnit4)
- **Godot Version**: 4.7+
- **Language**: GDScript
## Directory Structure
```
MusicPlayer/
├── addons/gdUnit4/ # GdUnit4 framework
├── tests/
│ ├── unit/ # Unit tests
│ │ ├── test_basic.gd # Basic sanity tests
│ │ ├── test_music_player.gd # MusicPlayer.gd tests
│ │ ├── test_main_window.gd # MainWindow.gd tests
│ │ ├── test_settings.gd # Settings.gd tests
│ │ └── test_playlist.gd # Playlist.gd tests
│ ├── integration/ # Integration tests
│ │ └── test_full_flow.gd # End-to-end flow tests
│ └── mocks/ # Mock utilities
│ ├── mock_http_request.gd # HTTP request mocking
│ └── test_helpers.gd # Test helper functions
├── .gitea/workflows/test.yml # Gitea Actions CI
├── .runsettings # GdUnit4 CLI settings
├── run_tests.sh # Test runner script
└── TESTING.md # This file
```
## Running Tests
### In Godot Editor
1. Open the project in Godot 4.7
2. Enable the GdUnit4 plugin in **Project > Project Settings > Plugins**
3. Open the **GdUnit4 Inspector** panel (should appear automatically)
4. Run tests by:
- Right-clicking on test files in the FileSystem panel
- Using the GdUnit4 Inspector panel
- Right-clicking in the Script Editor
### From Command Line (Headless)
## Running the tests
```bash
# Make the test runner executable (one-time setup)
chmod +x run_tests.sh
# Run all tests in headless mode
./run_tests.sh --headless
# Run with verbose output
./run_tests.sh --headless --verbose
# Or directly with Godot
godot --path . -s addons/gdUnit4/test.gd --gdunit.cli --headless
./run_tests.sh # everything
./run_tests.sh res://tests/unit # only unit tests
./run_tests.sh res://tests/unit/test_search.gd # one suite
```
### Direct Godot Command
The script finds Godot through `$GODOT`, then `godot` on your PATH, then
`/Applications/Godot.app` or `/Applications/Godot_mono.app`. Reports are written to
`reports/` (ignored by git).
```bash
# Run all tests
godot --path /path/to/MusicPlayer -s addons/gdUnit4/test.gd --gdunit.cli
In the editor, open the GdUnit4 panel or right-click the `tests` folder and choose
**Run Tests**.
# Run specific test suite
godot --path . -s addons/gdUnit4/test.gd --gdunit.cli --gdunit.filter=MusicPlayerTest
```
CI runs the suite in the `test` job of `.gitea/workflows/gitea-build.yaml` on every push
and pull request to `main` and `develop`. The `build` job only starts once the tests
pass.
## Test Coverage
## Layout
### Unit Tests
| Folder | Contents |
|---|---|
| `tests/unit/` | Logic without scenes: playlist, songs, player scoring, time formatting, search, the server client |
| `tests/integration/` | Real scenes (`MainWindow.tscn`, `SyncWindow.tscn`) run against a fake server, including the point keybinds |
| `tests/fakes/` | `FakeHttpTransport`, `AutoloadSnapshot` and the `MainWindowTestSuite` base class |
| `tests/data/` | Files the fake server sends, e.g. a short MP3 |
| Test File | Component | Coverage |
|-----------|-----------|----------|
| `test_music_player.gd` | MusicPlayer.gd | Play/Pause, Restart, Seek, Volume |
| `test_main_window.gd` | MainWindow.gd | Buttons, Search, Player List |
| `test_settings.gd` | Settings.gd | HTTP Requests, Utilities |
| `test_playlist.gd` | Playlist.gd | Song Management, Points |
| `test_full_flow.gd` | Integration | Fetch → Play → Next Flow |
## Faking the MusicServer
### Tested Functionality
#### MusicPlayer.gd
- ✅ Play/Pause button state toggling
- ✅ Restart button functionality
- ✅ Pause button functionality
- ✅ Time formatting (format_time)
- ✅ Text formatting with hide options (format_text)
- ✅ Drag started/ended on progress slider
- ✅ Seek functionality
- ✅ Volume change
#### MainWindow.gd
- ✅ All button signal connections
- ✅ Add players functionality
- ✅ Show/hide search view
- ✅ Show/hide settings window
- ✅ Show QR code window
- ✅ Show about window
- ✅ Get statistics
- ✅ Sync soundtracks
- ✅ Reset playlist
- ✅ Reset points
- ✅ Play next song
- ✅ Song object clicked (with download)
- ✅ Player removed
- ✅ Point given/taken
#### Settings.gd
- ✅ HTTP request mocking (make_request2, make_request3)
- ✅ Delete children utilities
#### Playlist.gd
- ✅ Add to playlist
- ✅ Get next song
- ✅ Set latest played song
- ✅ Get preloaded songs count
- ✅ Should preload more songs
- ✅ Song is answered
- ✅ Add/remove points
- ✅ Get song stream
- ✅ Reset playlist
- ✅ Clear song list
#### Integration Tests
- ✅ Fetch song list at start
- ✅ Fetch song flow (rand → info → addQue)
- ✅ Search function
- ✅ Player list updates
- ✅ Point system
- ✅ Winner declaration
- ✅ Show answer flow
- ✅ Update song list
## Mocking
### HTTP Request Mocking
The `MockHTTPRequest` class in `tests/mocks/mock_http_request.gd` allows you to mock HTTP responses:
All server calls go through the `MusicServerClient` autoload, which sends requests with
a swappable `transport`. Tests install a `FakeHttpTransport` that answers from canned
responses and records what was requested:
```gdscript
var mock_http = MockHTTPRequest.new()
mock_http.add_mock_response("https://api.example.com/data", 200, '{"key": "value"}')
var server: FakeHttpTransport = FakeHttpTransport.new().install()
server.respond_json("/music/list", [{"SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Frog's Theme"}])
server.respond_bytes("/music/rand", mp3_bytes)
# ... run the code under test ...
assert_int(server.count("/music/rand")).is_equal(1)
server.uninstall()
```
### Settings Mocking
Paths without a response get a 404, so the code under test sees a failed request.
For testing HTTP requests, temporarily replace `Settings.make_request2`:
## Writing tests
```gdscript
var original_make_request = Settings.make_request2
Settings.make_request2 = func(address: String, func_name: Callable, expect_data: bool):
func_name.call({"SongNo": 1, "Soundtrack": "Test", "Song": "Test Song"})
# Run your test
# Restore original
Settings.make_request2 = original_make_request
```
### Test Helpers
The `TestHelpers` class provides utility functions:
```gdscript
# Create a mock audio stream
var stream = TestHelpers.create_mock_audio_stream()
# Create a test song object
var song = TestHelpers.create_test_song_object(1, "Test Soundtrack", "Test Song")
# Setup test environment
TestHelpers.setup_test_environment()
# Cleanup test environment
TestHelpers.cleanup_test_environment()
```
## Continuous Integration
### Gitea Actions
The project includes a Gitea Actions workflow (`.gitea/workflows/test.yml`) that:
1. Checks out the repository
2. Sets up Godot 4.7
3. Installs GdUnit4 (if not present)
4. Runs all tests in headless mode
5. Uploads test results as artifacts
6. Fails the build if tests fail
### Test Results
After running tests, the following files are generated:
- `test_results.xml` - JUnit XML format (for CI systems)
- `test_results.html` - HTML report (for human reading)
## Writing New Tests
### Basic Test Structure
```gdscript
class_name MyNewTest
extends GdUnitTestSuite
func test_something():
# Arrange
var value = 42
# Act
var result = value * 2
# Assert
assert_eq(result, 84)
```
### Testing with Mocks
```gdscript
func test_with_mock():
var mock_object = mock(Node)
# Verify a method was called
mock_object.some_method()
assert_true(mock_object.some_method.was_called())
# Verify a method was called with specific arguments
mock_object.some_method("arg1", 42)
assert_true(mock_object.some_method.was_called_with(["arg1", 42]))
```
### Async Testing
```gdscript
func test_async_operation():
var completed = false
var callback = func():
completed = true
# Start async operation
some_async_function(callback)
# Wait for completion
await get_tree().create_timer(0.1).timeout
assert_true(completed)
```
## Assertions
GdUnit4 provides a rich set of assertions:
### Basic Assertions
- `assert_true(condition)`
- `assert_false(condition)`
- `assert_eq(actual, expected)`
- `assert_ne(actual, expected)`
- `assert_null(value)`
- `assert_not_null(value)`
### Type Assertions
- `assert_is_type(value, TYPE_INT)`
- `assert_is_not_type(value, TYPE_STRING)`
### String Assertions
- `assert_str("hello").is_equal_to("hello")`
- `assert_str("hello").has_length(5)`
- `assert_str("hello").starts_with("he")`
- `assert_str("hello").ends_with("lo")`
- `assert_str("hello").contains("ell")`
### Array Assertions
- `assert_arr([1, 2, 3]).has_size(3)`
- `assert_arr([1, 2, 3]).contains(2)`
- `assert_arr([1, 2, 3]).does_not_contain(4)`
### Dictionary Assertions
- `assert_dict({"a": 1}).has_size(1)`
- `assert_dict({"a": 1}).contains_key("a")`
- `assert_dict({"a": 1}).contains_value(1)`
### Number Assertions
- `assert_num(42).is_equal_to(42)`
- `assert_num(42).is_greater_than(40)`
- `assert_num(42).is_less_than(50)`
- `assert_num(42).is_between(40, 50)`
### File Assertions
- `assert_file("res://test.txt").exists()`
- `assert_file("res://test.txt").has_size(1024)`
### Signal Assertions
- `assert_signal(signal).was_emitted()`
- `assert_signal(signal).was_emitted_with([arg1, arg2])`
## Best Practices
1. **Test Isolation**: Each test should be independent of others
2. **Setup and Teardown**: Use `_setup()` and `_teardown()` methods for test lifecycle
3. **Mock External Dependencies**: Mock HTTP requests, file I/O, etc.
4. **Test One Thing**: Each test should verify one specific behavior
5. **Use Descriptive Names**: Test method names should describe what they test
6. **Keep Tests Fast**: Tests should run quickly for CI
7. **Test Edge Cases**: Include tests for error conditions and boundary values
## Troubleshooting
### Tests not discovered
- Ensure test classes extend `GdUnitTestSuite`
- Ensure test methods start with `test_`
- Check that the GdUnit4 plugin is enabled
### Tests fail in CI but pass locally
- Check Godot version compatibility
- Ensure headless mode works for your tests
- Verify file paths are correct (use `res://` paths)
### Mocking not working
- Ensure you're using the GdUnit4 `mock()` function
- Verify method names are spelled correctly
- Check that signals are connected properly
## Resources
- [GdUnit4 Documentation](https://godot-gdunit-labs.github.io/gdUnit4/latest/)
- [GdUnit4 GitHub](https://github.com/godot-gdunit-labs/gdUnit4)
- [Godot Documentation](https://docs.godotengine.org/)
## Contributing
When adding new features:
1. Add corresponding unit tests
2. Update integration tests if needed
3. Ensure all tests pass before submitting a PR
4. Add test coverage for edge cases
## License
The testing framework and tests are part of the MusicPlayer project and share the same license.
- Settings and Playlist are global. Tests that change them restore them afterwards, either
by hand or with `AutoloadSnapshot.take([Settings, Playlist])` and `restore()`.
- Main window tests extend `MainWindowTestSuite`, which starts the window against the fake
server and has helpers such as `_add_player()`, `_remove_player()` and
`_give_point_with_keybind()`.
- Player ids are UUID strings that never change. A player's position in
`Settings.player_array` only decides their keybind (Alt+1 for the first row and so on),
so tests check both: that points land on the right player id, and that keybinds follow
the rows when players are added or removed.