mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 06:52:18 +02:00
docs(test): describe test layout, contracts and host tools
Add test/README.md (TS-07): file naming, test/support/ and test/fixtures/, the notifier-subclass override pattern, what the source-text contract tests pin, FFmpeg, python3 and SMB requirements, running a subset, and the timing of library_browse_sqlite_paging_test. Derived from test/support/*.dart, test/fixtures/, test/source_contracts_test.dart, test/design_system_test.dart, the FFmpeg and SMB suites, test/library_browse_sqlite_paging_test.dart (timed at 25 s locally) and .github/workflows/ci.yml.
This commit is contained in:
1 parent
c4cf56fb9b
commit
c6f8ac21b5
1 file changed
+160
+160
@@ -0,0 +1,160 @@
|
||||
# Unit and Widget Tests
|
||||
|
||||
Host tests for Dart code. They run with `flutter test` and in CI. Device
|
||||
tests live in `integration_test/` (see `integration_test/README.md`).
|
||||
|
||||
## Layout
|
||||
|
||||
Test files sit directly in `test/`; only shared code has subfolders.
|
||||
|
||||
| Path | Contents |
|
||||
| --- | --- |
|
||||
| `test/*_test.dart` | All test suites |
|
||||
| `test/support/` | Shared fakes, fixtures and frozen "legacy" baselines, imported by unit and device tests |
|
||||
| `test/fixtures/` | Data files: JSON contract fixtures shared with Rust tests, and a TLS certificate for the WebDAV and artwork proxy tests |
|
||||
|
||||
### Naming
|
||||
|
||||
- When a suite tests one module, name it after that file:
|
||||
`lib/services/album_completeness.dart` is tested by
|
||||
`test/album_completeness_test.dart`. Roughly half of the suites follow this.
|
||||
- Otherwise prefix by feature area and describe the behavior, for example
|
||||
`download_queue_persistence_test.dart`, `library_partial_scan_test.dart`,
|
||||
`mornye_bottom_bar_test.dart`, `network_storage_service_test.dart`. The
|
||||
common prefixes are `download_`, `library_`, `mornye_`, `network_`,
|
||||
`track_`, `native_` and `extension_`.
|
||||
- Put a fake used by more than one suite in `test/support/`, not in a copy per
|
||||
file.
|
||||
|
||||
### Shared Support
|
||||
|
||||
| File | Use |
|
||||
| --- | --- |
|
||||
| `test/support/fixed_settings.dart` | `FixedSettingsNotifier`: serves fixed `AppSettings` without reading preferences |
|
||||
| `test/support/sqlite_process_database.dart` | `SqliteProcessDatabase`: a sqflite `Database` backed by real SQLite through a `python3` child process |
|
||||
| `test/support/app_icon_finder.dart` | `findAppIcon`: finds an icon drawn by `Icon` or `AppIcon` |
|
||||
| `test/support/library_paging_fixture.dart`, `test/support/library_native_fixture.dart` | Library database fixtures |
|
||||
| `test/support/re_enrich_settings.dart` | `ReEnrichTestSettings`: default settings that skip the lyrics sync to the backend |
|
||||
| `test/support/virtual_time.dart` | `VirtualTime`: a timer zone that skips idle time while keeping real microtasks |
|
||||
| `test/support/music_player_automix_harness.dart` | Shared AutoMix player fakes used by the `music_player_*_test.dart` suites |
|
||||
| `*_benchmark.dart`, `*_baseline.dart` in `test/support/` | Frozen pre-optimization implementations, compared against production code in unit and device tests |
|
||||
|
||||
### Shared Contract Fixtures
|
||||
|
||||
Some fixtures are read by Dart and Rust tests, so both sides parse the same
|
||||
data: `native_library_contract.json`, `native_metadata_parsers.json`,
|
||||
`native_lyrics_parsers.json`, `native_backup_contract.json` and
|
||||
`collection_restore_contract.json` in `test/fixtures/`. Change them only
|
||||
together with both parsers.
|
||||
|
||||
## Patterns
|
||||
|
||||
### Notifier Subclasses
|
||||
|
||||
Many tests replace a Riverpod notifier with a small subclass that overrides
|
||||
`build()` or one method, then install it with `overrideWith`:
|
||||
|
||||
```dart
|
||||
class _History extends DownloadHistoryNotifier {
|
||||
@override
|
||||
DownloadHistoryState build() => DownloadHistoryState();
|
||||
}
|
||||
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
settingsProvider.overrideWith(() => FixedSettingsNotifier()),
|
||||
downloadHistoryProvider.overrideWith(_History.new),
|
||||
],
|
||||
);
|
||||
```
|
||||
|
||||
This is used for `DownloadQueueNotifier`, `DownloadHistoryNotifier`,
|
||||
`LocalLibraryNotifier`, `LibraryCollectionsNotifier`, `ExtensionNotifier`
|
||||
and `SettingsNotifier`. Keep the public methods these tests override on the
|
||||
notifier class itself when refactoring a provider; moving them to a helper
|
||||
breaks the subclasses.
|
||||
|
||||
### Source Contract Tests
|
||||
|
||||
Several suites read production source text on purpose. Two of them exist
|
||||
only for that:
|
||||
|
||||
- `test/source_contracts_test.dart` pins text across Dart, Kotlin and Swift:
|
||||
- `lib/services/app_state_database.dart`: database version 4 and its
|
||||
playback-session migration.
|
||||
- `ios/Runner/AppDelegate.swift` and
|
||||
`lib/providers/download_queue_provider.dart`: the iOS background download
|
||||
task is acquired before work starts, and expiry cancels downloads before
|
||||
notifying Dart, which pauses and requeues.
|
||||
- `NativeDownloadFinalizer.kt`, `NativeFinalizerHistory.kt`,
|
||||
`NativeHistoryStore.kt` and `DownloadServiceSnapshot.kt` (in
|
||||
`android/app/src/main/kotlin/com/zarz/spotiflac/`), with
|
||||
`lib/services/history_schema.dart` and
|
||||
`lib/providers/download_queue_provider_native_worker.dart`: the native
|
||||
worker contract version, the history schema version, columns and
|
||||
indexes, and the single progress-delta wait.
|
||||
- `android/app/src/test/resources/finalization_quality_cases.tsv`: quality
|
||||
labels shared with the Kotlin tests.
|
||||
- `test/design_system_test.dart` scans every `.dart` file under `lib/`:
|
||||
- every `IconButton` has a tooltip;
|
||||
- only `app_sliver_header.dart` uses `expandedTitleScale`;
|
||||
- only `app_bottom_sheet.dart` draws a drag handle, and only
|
||||
`app_theme.dart` sets a modal sheet shape;
|
||||
- only `selection_bottom_bar.dart` and `app_menu_button.dart` create an
|
||||
`OverlayEntry`;
|
||||
- `album_screen.dart`, `playlist_screen.dart`, `local_album_screen.dart`,
|
||||
`downloaded_album_screen.dart` and `library_tracks_folder_screen.dart`
|
||||
use `CollectionScaffold(`, and `playlist_screen.dart` uses
|
||||
`SelectionModeMixin<PlaylistScreen>`;
|
||||
- `lib/screens/now_playing_screen.dart` uses `showAppBottomSheet` and no
|
||||
`PopupMenuButton`.
|
||||
|
||||
Other suites read a production file alongside their behavior tests:
|
||||
`sqlite_helpers_test.dart` (`lib/services/sqlite_helpers.dart`),
|
||||
`downloaded_embedded_cover_resolver_test.dart`
|
||||
(`lib/screens/track_metadata_screen_cover.dart` and
|
||||
`track_metadata_cards.dart`), `queue_library_refresh_policy_test.dart`
|
||||
(`lib/screens/queue_tab_collection_items.dart`), and the schema tests
|
||||
`library_queue_store_test.dart`, `album_completeness_test.dart`,
|
||||
`album_completeness_query_test.dart` and `replaygain_library_filter_test.dart`
|
||||
(`CREATE TABLE` text from `lib/services/history_schema.dart` and
|
||||
`library_schema.dart`). Find them with `rg -n "File\('(lib|android|ios)/" test`.
|
||||
|
||||
When a refactor moves pinned code, update the path or the assertion in the
|
||||
same commit. Never delete these checks.
|
||||
|
||||
## Host Tools
|
||||
|
||||
| Tool | Needed by | Without it |
|
||||
| --- | --- | --- |
|
||||
| `ffmpeg` on `PATH` | `ffmpeg_decryption_output_test.dart`, `automix_effect_renderer_test.dart` | The FFmpeg cases are skipped (`markTestSkipped`) |
|
||||
| `ffmpeg` and `ffprobe` on `PATH` | `ffmpeg_opus_metadata_test.dart` | The round-trip cases are skipped |
|
||||
| `python3` | Suites using `SqliteProcessDatabase` | Those suites fail |
|
||||
|
||||
`SPOTIFLAC_TEST_FFMPEG` selects another FFmpeg binary for
|
||||
`automix_effect_renderer_test.dart`. The CI workflow
|
||||
(`.github/workflows/ci.yml`) installs `ffmpeg` before running the tests, so
|
||||
these cases are not skipped there.
|
||||
|
||||
Optional SMB cases are skipped unless their environment is set:
|
||||
`TEST_SMB=1` (with `scripts/smb2_fixture_server.py`) and `TEST_SMB_GUEST_PORT`
|
||||
in `network_storage_service_test.dart`; `TEST_SMB_LIBRARY` in
|
||||
`network_smb_transport_test.dart`; `TEST_SMB_UPLOAD=1` in
|
||||
`network_smb_upload_test.dart`. `TEST_SMB_PORT` overrides the fixture port.
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
flutter test # everything
|
||||
flutter test test/album_completeness_test.dart # one file
|
||||
flutter test test/download_queue_persistence_test.dart --plain-name 'restores'
|
||||
flutter test test/source_contracts_test.dart test/design_system_test.dart
|
||||
```
|
||||
|
||||
`--plain-name` filters by a substring of the test name.
|
||||
|
||||
`test/library_browse_sqlite_paging_test.dart` pages 1000 albums through real
|
||||
SQLite. Alone it took about 25 seconds on a development Mac, close to the
|
||||
default 30-second per-test timeout, so it can time out when the machine is
|
||||
busy (for example during a full parallel run). Re-run it alone before treating
|
||||
a failure as real.
|
||||
Reference in new issue
Block a user