





















核心摘要 (TL;DR)
- 背景:Patrol E2E 测试在 Android 模拟器上全部通过后,需要在 iOS 模拟器上复现同样的测试覆盖。
- 核心问题:
xcodebuild exited with code 70,测试总数始终为 0(Total: 0),模拟器被克隆多份。- 根因链条:
RunnerTests(Unit Test Bundle)→ 需改为RunnerUITests(UI Testing Bundle)→ Patrol CLI 硬编码-destination-timeout 1导致设备匹配失败 →SUPPORTED_PLATFORMS缺少iphonesimulator→ scheme 中parallelizable="YES"导致模拟器克隆 →pressBack()在 iOS 上不可用。- 关键解法:重命名 target 为
RunnerUITests、修补 patrol_cli 源码用device.id替换name+OS匹配、禁用测试并行化、用$.tester.pageBack()替代pressBack()。
基本信息
- 项目背景:Flutter 项目使用 Riverpod + GoRouter + Isar,已在 Android 上完成 E2E 测试。
- 技术栈:Flutter 3.41.1, Patrol 3.20.0, patrol_cli 3.11.0, Xcode 16.x
- 测试目标:将 Dashboard、Inventory、Settings、History 四大模块测试扩展到 iOS 模拟器
- 运行环境:iOS 模拟器 (iPhone 16e)
1 | # 问题 1: xcodebuild 退出码 70,测试数量为 0 |
Android E2E 测试全部通过后,执行 patrol test -d "iPhone 16e" 启动 iOS 测试。预期:像 Android 一样直接跑起来。实际:xcodebuild exited with code 70,Total: 0,测试完全没有被发现。
整个调试过程跨越了多个问题层级,每修复一个问题就暴露下一个:
RunnerUITests,不是 RunnerTestsinherit! :search_paths 不够,需要 :complete-destination-timeout 1 太短 + SUPPORTED_PLATFORMS 不全parallelizable="YES" 导致克隆pressBack() 在 iOS 上不存在Flutter 默认创建的 RunnerTests target 的 productType 是 com.apple.product-type.bundle.unit-test。Patrol 需要的是 com.apple.product-type.bundle.ui-testing。
差异对比:
| 设置 | Unit Test Bundle | UI Testing Bundle (Patrol) |
|---|---|---|
productType | .bundle.unit-test | .bundle.ui-testing |
TEST_HOST | 需要设置 | 不需要 |
BUNDLE_LOADER | 需要设置 | 不需要 |
TEST_TARGET_NAME | 不需要 | 需要 = Runner |
| Podfile 继承 | inherit! :search_paths | inherit! :complete |
修改 project.pbxproj:
1 | // 修改前 |
同时删除 BUNDLE_LOADER 和 TEST_HOST,添加 TEST_TARGET_NAME = Runner。
RunnerUITests,而非 RunnerTests深入 Patrol CLI 源码 (app_options.dart) 发现:
1 |
|
无论你的 Xcode 项目中 target 叫什么名字,Patrol CLI 都会固定查找 RunnerUITests。
解决方案: 全面重命名:
ios/RunnerTests/ → ios/RunnerUITests/RunnerTests.swift → RunnerUITests.m(改用 Objective-C + Patrol 宏)project.pbxproj 中所有引用Podfile 中 target 名称创建 Objective-C 测试入口:
1 | // ios/RunnerUITests/RunnerUITests.m |
1 | Library not loaded: CocoaAsyncSocket.framework/CocoaAsyncSocket |
UI Testing Bundle 运行在独立进程中,需要完整嵌入所有依赖框架。
修改 Podfile:
1 |
|
即使 target 配置正确,xcodebuild test-without-building 仍然报:
1 | Supported platforms for the buildables in the current scheme is empty. |
两层原因:
4a. SUPPORTED_PLATFORMS 不完整
project.pbxproj 中项目级别的 Release/Profile 配置:
1 | // 修改前(只有真机) |
4b. Patrol CLI 硬编码 -destination-timeout 1
patrol_cli 构造的 xcodebuild 命令中 -destination-timeout 只有 1 秒。当 SUPPORTED_PLATFORMS 不完整时,xcodebuild 需要更长时间解析设备。
但更根本的问题是 Patrol CLI 使用 -destination platform=iOS Simulator,OS=latest,name=iPhone 16e 按名称匹配设备,在 “Supported platforms is empty” 场景下完全无法工作。
解决方案:修补本地 patrol_cli 源码,使用设备 UUID 直接匹配:
1 |
|
注意: 修改源码后需要删除 snapshot 并重新激活:
1
2 rm ~/.pub-cache/global_packages/patrol_cli/bin/main.dart-3.11.0.snapshot
dart pub global activate patrol_cli 3.11.0
测试运行时,xcodebuild 为每个 test suite 创建了独立的模拟器克隆(“Clone 1 of iPhone 16e”),导致启动多个模拟器实例。
原因: Runner.xcscheme 中 TestableReference 的 parallelizable 属性:
1 |
|
pressBack() 在 iOS 上不可用Android 测试中广泛使用的 $.native.pressBack() 在 iOS 上返回 400 错误:
1 | pressBack() failed with Invalid response: 400 |
iOS 没有系统级返回按钮,这是 Android 独有的功能。
解决方案: 创建跨平台 goBack() helper:
1 | Future<void> goBack(PatrolIntegrationTester $) async { |
踩坑细节: iOS 上 Flutter AppBar 的返回图标是
Icons.arrow_back_ios_new_rounded(不是arrow_back也不是arrow_back_ios)。直接用 Patrol 的$(Icons.xxx).tap()找到图标后会报 “not hit-testable”,因为图标嵌套在 AppBar 的约束区域内。$.tester.pageBack()是最可靠的方案。
在 project.pbxproj 中:
productType 从 unit-test 改为 ui-testingBUNDLE_LOADER 和 TEST_HOSTTEST_TARGET_NAME = RunnerRunnerTests → RunnerUITestsSUPPORTED_PLATFORMS = "iphoneos iphonesimulator"1 | // ios/RunnerUITests/RunnerUITests.m |
1 | target 'Runner' do |
1 |
|
1 |
|
1 |
|
1 |
|
所有测试文件中的 $.native.pressBack() 替换为 goBack($)。
1 | .PHONY: ios-boot |
RunnerUITests:这是 patrol_cli 硬编码的名称,不是可配置的。新项目请一开始就用这个名字。productType 必须是 ui-testing:Unit Test Bundle 和 UI Testing Bundle 是完全不同的东西,Patrol 需要后者。inherit! :complete 而非 :search_paths:UI Testing Bundle 在独立进程运行,需要完整嵌入所有框架。.snapshot 文件并重新激活,否则修改不会生效。pressBack() 是 Android 独有:iOS 测试中必须用 $.tester.pageBack() 或手动点击 UI 中的返回按钮。xcrun simctl shutdown all 确保只有一个模拟器运行,避免克隆。goBack() 关闭 AlertDialog(没有系统返回键),必须点击对话框中的按钮。| 模块 | 测试内容 | 状态 | 耗时 |
|---|---|---|---|
| Dashboard | 启动、搜索、筛选、添加物品、ChoiceChip 切换、详情页 | ✅ 通过 | 44s |
| Inventory | 添加、验证、详情、消耗、删除、编辑、高级详情、连续添加 | ✅ 通过 | 63s |
| Settings | 数据管理、分类、位置、语言切换、数据统计、反馈对话框 | ✅ 通过 | 43s |
| History | 空状态、消耗后出现、补货流程、多物品历史 | ✅ 通过 | 45s |
1 | xcodebuild exit code 70 (Total: 0) |
1 | make ios-boot |
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。