测试指南
# Flutter 和 Dart 测试flutter test
# 指定测试文件flutter test test/unit/server/disk_test.dart
# 生成覆盖率报告flutter test --coverageDart 测试位于 test/,按 unit/、widget/、platform/ 和 migration/ 分类。共享 helper 和 release fixture 分别位于 helpers/ 和 fixtures/。测试应验证行为和输入输出,不依赖外部网络或真实服务器。
unit/ 内部再按功能域划分:ssh/、terminal/、file/、server/、store/、ai/、geo/、rootfs/、monitor/、benchmark/、remote_desktop/ 和 app/。测试按所覆盖的功能归类,而不是按所处的层级——同一功能的 store、provider 和 model 放在一起。store/ 存放不属于单个功能的存储设施(数据库、schema、备份恢复、设置),app/ 存放其余部分:版本、本地化、崩溃上报、诊断和构建检查。
Rust 测试
Section titled “Rust 测试”# Rust workspace 的全部测试cargo test --workspace
# FFI parity test:先构建 FFI cratecargo build -p sbm_ffiflutter test test/unit/app/frb_parser_test.dartcrates/sbm_parser/tests/dart_compat.rs 使用与 Dart 相同的 fixture,锁定两侧 parser 的行为。
需要显式开启的测试
Section titled “需要显式开启的测试”以下测试需要真实主机;未设置环境变量时会静默跳过:
# SSH end-to-end:上传生成的脚本,在远端执行并与直接命令输出比较# 在 workspace 根目录的 .env 中设置:# SBM_E2E_SSH_HOST=<SSH 目标或 ~/.ssh/config 别名>cargo test -p sbm_parser --test ssh_e2e
# 使用真实 sshd 测试 Monitor terminal# 需要 SBM_E2E_TERMINAL_* 环境变量cargo test -p server_box_monitor --test terminal_wsMonitor 面板测试
Section titled “Monitor 面板测试”Monitor 的 Svelte 前端有独立的 Vitest 和 Testing Library 测试:
cd monitor/frontendnpm run testnpm run test:coveragenpm run checknpm run check 进行类型检查,也是 npm run build 的一部分。
Unit test
Section titled “Unit test”Unit test 用于验证纯业务逻辑、model 和 parser:
test('calculates CPU percentage', () { final cpu = CpuModel(usage: 75.0); expect(cpu.usagePercentage, '75%');});Widget test
Section titled “Widget test”Widget test 用于验证 Widget 的布局、文本和交互:
testWidgets('shows the server name', (tester) async { await tester.pumpWidget( ProviderScope( child: MaterialApp( home: ServerCard(server: testServer), ), ), );
expect(find.text('Test Server'), findsOneWidget);});会写入 store 的 Widget test 必须在 setUp 使用 openTestDb(),并在 tearDown 调用 closeTestDb(),先排空待写入操作再关闭 SQLite。通过正常构造函数访问隔离数据库,迁移测试使用生产的 setting 存储名。不要向生产代码添加测试专用构造、重置方法或可变网络工厂;有状态服务使用正常实例生命周期,HTTP、文件系统替身放在 test/helpers/。
不要对包含 text field 或其他持续调度 frame 的 Widget 使用 pumpAndSettle()。使用定次数的 pump(duration),并为测试命令设置合理的 timeout。
Provider test
Section titled “Provider test”Provider test 验证状态和异步结果:
test('returns server status', () async { final container = ProviderContainer(); final status = await container.read(serverStatusProvider(testServer).future); expect(status, isA<StatusModel>()); container.dispose();});默认测试必须保持确定性。parser、model、命令构建和普通 Widget test 不应访问网络或真实服务器。引入外部服务边界时,添加专用 fake、fixture 或 mock。
上文的 SSH end-to-end 和真实 sshd 测试是例外;它们只在配置环境变量后运行,因此默认 cargo test --workspace 不需要外部服务。
存储迁移测试
Section titled “存储迁移测试”存储迁移在用户设备上通常只有一次机会。迁移写入完成标记后不会重新读取旧数据,因此错误更可能表现为静默丢数据,而不是崩溃。
每个迁移都必须保留永久 regression test,并使用旧 release 实际写出的 bytes:
| 文件 | 作用 |
|---|---|
test/migration/hive_release_migration_test.dart |
对每个 release fixture 运行 Hive import 和已注册的 migration |
test/fixtures/hive_v{1466,1480,1491}/ |
这些 release 实际写出的 box、生成器和说明 |
test/migration/hive_import_test.dart |
验证 import 的重试、幂等和按 box 进度 |
test/migration/m0NN_*_test.dart |
每个 schema migration 一个,验证该步的迁移行为 |
fixture 一旦进入 regression test,就不能重新生成来绕过失败。使用当前 adapter 生成数据只能证明当前版本与自己一致,不能证明它仍能读取旧 release 的格式。
生成 release-authentic fixture
Section titled “生成 release-authentic fixture”- 使用
git worktree add /tmp/<tag> <tag>检出目标 release,初始化它需要的 submodule,并运行flutter pub get。 - 将
test/fixtures/hive_v1466/gen_fixture.dart.txt复制到临时测试中,按目标 release 的 model、adapter 和依赖版本调整;使用旧 release 自己的代码写出.hivebytes。数据应覆盖可选字段、枚举值、各类 store,以及非 ASCII 字符、引号和换行。 - 将结果复制到
test/fixtures/hive_v<tag>/,保留生成器说明和 README,然后删除 worktree。 - 使用当前代码通过 public store API 编写读取测试,并检查数据库编码没有残留旧字段。
生成器以 .txt 签入,因为它针对旧 release 的 API,在当前 tree 中不一定能通过 analyze。
Integration test
Section titled “Integration test”integration_test/ 用于验证 flutter test 无法回答的问题。Unit test 运行在不加载 plugin 的 flutter_tester 中,因此通过 plugin 或 FFI 调用的代码不会在真实 App 环境中执行。Integration test 则运行在已连接的设备或 simulator 上:
| 文件 | 验证内容 |
|---|---|
local_shell_test.dart |
本机 shell 是否能启动 |
rootfs_shell_test.dart |
Alpine rootfs 是否能通过 App 的 API 运行 |
android_exec_test.dart |
Android App 目录中的进程执行能力 |
android_rootfs_test.dart |
Android guest 机制 |
ios_rootfs_test.dart |
iOS Linux userland |
ios_bench_test.dart |
真实硬件上的 guest 开销 |
ios_load_test.dart |
guest 运行时对 App 的影响 |
sandbox_import_test.dart |
App Store sandbox build 的数据接管 |
# 需要已连接的设备或模拟器flutter test integration_test/local_shell_test.dartmake analyze 也会分析 integration_test/。
iOS 17+ 无线设备
Section titled “iOS 17+ 无线设备”当 Xcode 通过网络连接 iOS 17+ 设备时,需要发布 driver 端口:
flutter drive --publish-port \ --driver=integration_test/driver.dart \ --target=integration_test/ios_rootfs_test.dart首次运行时,设备可能会请求本地网络权限,请允许该权限。
编写测试的建议
Section titled “编写测试的建议”- 使用 Arrange–Act–Assert 组织测试。
- 测试名称描述实际行为,而不是实现细节。
- 对关键行为添加足够断言,同时保持测试专注。
- 使用 fake 或 fixture 隔离外部依赖。
- 覆盖空列表、缺失值、无效输入和权限错误等边界情况。
rootfs 签名 fixture 必须保留原始字节:.gitattributes 为 JSON 固定 LF,将签名标为二进制。永远不要为了让测试通过而重新签名或重新生成 fixture——fixture 是某个发布版本真实写下的字节,改写它只能证明当前代码与自身一致。
测试应尽量走生产路径而非替身:fixture 经由真实安装器,HTTP 测试使用本地 socket。