Testing
Running tests
Section titled “Running tests”# Flutter and Dart testsflutter test
# A specific test fileflutter test test/unit/server/disk_test.dart
# Generate a coverage reportflutter test --coverageTest structure
Section titled “Test structure”Dart tests live in test/ and are grouped into unit/, widget/, platform/, and migration/. Shared helpers and release fixtures live in helpers/ and fixtures/. Tests should verify behavior and input/output without depending on an external network or a real server.
unit/ is divided again by feature domain: ssh/, terminal/, file/, server/, store/, ai/, geo/, rootfs/, monitor/, benchmark/, remote_desktop/, and app/. A test belongs to the feature it covers rather than to the layer it reaches — a store, a provider, and a model of the same feature sit together. store/ holds storage infrastructure that belongs to no one feature (the database, the schema, backup and restore, settings), and app/ holds the rest: version, localization, crash reporting, diagnostics, and build checks.
Rust tests
Section titled “Rust tests”# All tests in the Rust workspacecargo test --workspace
# FFI parity test: build the FFI crate firstcargo build -p sbm_ffiflutter test test/unit/app/frb_parser_test.dartcrates/sbm_parser/tests/dart_compat.rs uses the same fixtures as the Dart tests to lock parser behavior on both sides.
Opt-in tests
Section titled “Opt-in tests”The following suites need a real host. They are skipped silently when the required environment variables are not set:
# SSH end to end: upload the generated script, run it remotely,# and compare the parsed result with direct command output.# Set this in the workspace-root .env:# SBM_E2E_SSH_HOST=<SSH destination or ~/.ssh/config alias>cargo test -p sbm_parser --test ssh_e2e
# Monitor terminal against a real sshd# Requires SBM_E2E_TERMINAL_* environment variables.cargo test -p server_box_monitor --test terminal_wsMonitor panel tests
Section titled “Monitor panel tests”The Monitor Svelte frontend has an independent Vitest and Testing Library suite:
cd monitor/frontendnpm run testnpm run test:coveragenpm run checknpm run check performs type checking and is also part of npm run build.
Unit tests
Section titled “Unit tests”Use unit tests for pure business logic, models, and parsers:
test('calculates CPU percentage', () { final cpu = CpuModel(usage: 75.0); expect(cpu.usagePercentage, '75%');});Widget tests
Section titled “Widget tests”Use Widget tests for layout, text, and interaction:
testWidgets('shows the server name', (tester) async { await tester.pumpWidget( ProviderScope( child: MaterialApp( home: ServerCard(server: testServer), ), ), );
expect(find.text('Test Server'), findsOneWidget);});A widget test that writes to a store must call openTestDb() in setUp and closeTestDb() in tearDown. The latter drains pending writes before closing SQLite. Construct the normal store against the isolated database; migration tests use the production setting store name. Do not add production reset methods, test-only constructors, or mutable network factories. Stateful services use their normal instance lifecycle; HTTP and filesystem substitutes belong in test/helpers/.
Do not use pumpAndSettle() on a tree containing a text field or another Widget that continually schedules frames. Count frames explicitly with pump(duration) instead, and use a reasonable test timeout.
Provider tests
Section titled “Provider tests”Use Provider tests to verify state and asynchronous results:
test('returns server status', () async { final container = ProviderContainer(); addTearDown(container.dispose);
final status = await container.read(serverStatusProvider(testServer).future); expect(status, isA<StatusModel>());});External dependencies
Section titled “External dependencies”The default test suite must remain deterministic. Parser, model, command-builder, and ordinary Widget tests must not access a network or a real server. When a feature introduces a service boundary, add a targeted fake or fixture.
The SSH end-to-end and real-sshd suites above are exceptions. They run only when their environment variables are configured, so a default cargo test --workspace still needs no external service.
Storage migration tests
Section titled “Storage migration tests”A storage migration usually gets one chance to process a user’s data. Once it writes its completion marker, the old data is not read again. A migration bug is therefore more likely to silently lose data than to crash.
Every migration must keep a permanent regression test using bytes written by the release being migrated from, not data regenerated by the current adapter:
| File | Purpose |
|---|---|
test/migration/hive_release_migration_test.dart |
Runs Hive import and the registered migrations against each release fixture |
test/fixtures/hive_v{1466,1480,1491}/ |
Boxes written by those releases, plus their generators and documentation |
test/migration/hive_import_test.dart |
Verifies import retry, idempotency, and per-box progress |
test/migration/m0NN_*_test.dart |
One per schema migration, verifying that step’s behavior |
A fixture generated with the current adapter only proves that the current code agrees with itself. It cannot prove that the current decoder still reads the format written by an old release. Once a fixture is used for regression coverage, never regenerate it to make a failing test pass.
Creating a release-authentic fixture
Section titled “Creating a release-authentic fixture”- Check out the target release with
git worktree add /tmp/<tag> <tag>, initialize the submodules required by itspubspec.yaml, and runflutter pub get. - Copy
test/fixtures/hive_v1466/gen_fixture.dart.txtinto a temporary test. Adapt it to that release’s models, adapters, and dependency versions, then generate.hivebytes with the old release’s own code. Cover every optional field, every enum value, every store type, and include non-ASCII text, quotes, and newlines. - Copy the generated files into
test/fixtures/hive_v<tag>/, keep the generator notes and README, then remove the worktree. - Write the current-version reading test through the public store API, and check that the database encoding contains no fields from the old shape.
The generator is checked in as .txt because it targets an old release’s API and is not expected to pass analyze in the current tree.
Integration tests
Section titled “Integration tests”integration_test/ covers questions that flutter test cannot answer. Unit tests run under flutter_tester, which does not load plugins. Code reached through a plugin or FFI therefore does not execute in the real App environment there. Integration tests run on a connected device or simulator:
| File | What it verifies |
|---|---|
local_shell_test.dart |
Whether a shell on the device can actually start |
rootfs_shell_test.dart |
Whether the Alpine userland works through the App’s API |
android_exec_test.dart |
What Android permits the App to execute from its own directory |
android_rootfs_test.dart |
Whether the Android guest mechanism works |
ios_rootfs_test.dart |
The Linux userland on iOS |
ios_bench_test.dart |
Linux guest overhead on real hardware |
ios_load_test.dart |
App overhead while the guest is running |
sandbox_import_test.dart |
Taking over data from the App Store sandbox build |
# Requires a connected device or simulatorflutter test integration_test/local_shell_test.dartmake analyze also analyzes integration_test/.
Wireless iOS 17+ devices
Section titled “Wireless iOS 17+ devices”When Xcode connects to an iOS 17+ device over the network, publish the driver port:
flutter drive --publish-port \ --driver=integration_test/driver.dart \ --target=integration_test/ios_rootfs_test.dartThe device may request local-network permission on the first run. Allow it for the test to connect.
Testing recommendations
Section titled “Testing recommendations”- Organize tests with Arrange–Act–Assert.
- Name tests after the behavior they verify, not the implementation.
- Add enough assertions for important behavior while keeping each test focused.
- Isolate external dependencies with fakes or fixtures.
- Cover empty lists, missing values, invalid input, and permission failures.
Fixtures
Section titled “Fixtures”Signed rootfs fixtures must retain their original bytes: JSON uses LF and signatures are binary in .gitattributes. Never re-sign or regenerate a fixture to make a test pass — a fixture is bytes a release actually wrote, and rewriting it only proves the current code agrees with itself.
Where a test can, it should use the production path rather than a substitute: fixtures go through the real installer, and HTTP tests use a local socket.