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:
1 parent
87ff3d899b
commit
91adf52cf4
559 files changed
+32474
-2100
No files matched your search
+45
-325
@@ -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.
|
||||
Reference in new issue
Block a user