# MusicPlayer Testing Guide This document describes the testing setup for the MusicPlayer project using **GdUnit4**. ## 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) ```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 ``` ### Direct Godot Command ```bash # Run all tests godot --path /path/to/MusicPlayer -s addons/gdUnit4/test.gd --gdunit.cli # Run specific test suite godot --path . -s addons/gdUnit4/test.gd --gdunit.cli --gdunit.filter=MusicPlayerTest ``` ## Test Coverage ### Unit Tests | 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 | ### 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: ```gdscript var mock_http = MockHTTPRequest.new() mock_http.add_mock_response("https://api.example.com/data", 200, '{"key": "value"}') ``` ### Settings Mocking For testing HTTP requests, temporarily replace `Settings.make_request2`: ```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.