- 测试
【免费下载链接】EarlGrey
:tea: iOS UI Automation Test Framework
EarlGrey 是运行在 iOS 应用进程内部的 UI 自动化测试框架,它通过跟踪 App 的内部状态实现自动同步。为了让框架代码随被测 App 一起加载并精确控制测试执行,EarlGrey 要求对测试工程的 Scheme 与 Build Phases 做两项关键改造:添加 Copy Files Build Phase 将 EarlGrey.framework 内嵌到被测 App,并在 Scheme 中注入DYLD_INSERT_LIBRARIES环境变量。本文将基于仓库源码与示例工程,完整解析这两项改造的原因、自动化实现、手工配置步骤与常见故障排查方法。
一、为什么 EarlGrey 必须内嵌进被测 App?
EarlGrey 是一种白盒(white-box)测试方案,与 Xcode 原生 UI Testing 的黑盒方案不同,它运行在与被测 App同一个进程内,因而可以访问与 App 相同的内存空间。这一架构带来的核心收益是同步能力:EarlGrey 可以等待网络请求完成、跟踪 CA 动画、监听 ViewController 出现/消失、识别键盘输入与滚动状态,从而在 App 处于 idle 状态时才执行交互与断言(详见 docs/features.md 中的 Synchronization 章节)。
这种同步能力依赖 GREYAppStateTracker 对 App 内部状态的全过程跟踪。从源码看,该跟踪器维护了一张细粒度的状态位掩码,例如:
kGREYPendingDrawLayoutPass:View 等待 draw/layout passkGREYPendingCAAnimation:等待 CA 动画完成kGREYPendingNetworkRequest:等待网络请求完成kGREYPendingUIScrollViewScrolling:等待 UIScrollView 滚动结束kGREYPendingGestureRecognition:等待手势识别
状态通过TRACK_STATE_FOR_OBJECT/UNTRACK_STATE_FOR_OBJECT宏(见 GREYAppStateTracker.h)在对象生命周期内登记与释放,且状态位之间可以用按位或(Bitwise-OR)组合。正因为 EarlGrey 需要持续观察 App 内部的这些状态,框架代码必须常驻被测 App 进程内。
同时,EarlGrey 的设计刻意避免两种做法:让用户直接将被测 App 链接 EarlGrey,或为测试单独创建 test rig。前者会让生产代码沾染测试依赖,后者则破坏了"测试真实 App"的原则。因此 EarlGrey 选择由自己完成"嵌入"动作——这就是 IFAQ 中说明的第一项改造的由来:
通过添加一个 Copy Files Build Phase,把链接到测试 target 的 EarlGrey.framework 复制到被测 App 中(位置由
$TEST_HOST变量指定)。
二、改造一:Copy Files Build Phase 与$TEST_HOST
2.1 原理与配置要点
$TEST_HOST是 Xcode 内置的构建设置,指向被测 App 可执行文件的路径(例如$(BUILT_PRODUCTS_DIR)/MyApp.app/MyApp)。EarlGrey 的 Copy Files 阶段利用它定位被测 App 的 .app 包,然后把 EarlGrey.framework 复制进去。
手工配置时(步骤见 docs/install-and-run.md 的 "Final Test Configuration" 一节),Copy Files 阶段应设置为:
| 配置项 | 值 |
|---|---|
| Destination | Absolute Path |
| Path | $(TEST_HOST)/.. |
| Copy files only when installing | 取消勾选 |
| 文件 | EarlGrey.framework,并勾选 "Code Sign on Copy" |
注意 Path 使用$(TEST_HOST)/..,即先定位到 App 可执行文件(在 .app 包内部),再上溯一级到 .app 包的根目录,使 framework 被复制到MyApp.app/EarlGrey.framework的正确位置。
2.2 Demo 工程中的真实配置
仓库内 EarlGreyContribs.xcodeproj/project.pbxproj 的PBXCopyFilesBuildPhase片段印证了这一配置:
FDFCE9BE1D35B19F006D9592 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = "$(TEST_HOST)/.."; dstSubfolderSpec = 0; files = ( FDFCE9BF1D35B1B3006D9592 /* EarlGrey.framework in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; };其中:
dstPath = "$(TEST_HOST)/.."对应 UI 上的 Path 配置;dstSubfolderSpec = 0表示"绝对路径"(Absolute Path)目的地类型;ATTRIBUTES = (CodeSignOnCopy, )(见同文件中的 PBXBuildFile 声明)对应 "Code Sign on Copy" 勾选,确保复制进 App 的 framework 重新签名,避免真机/模拟器上的签名不一致问题。
2.3 gem 的自动化实现
使用 CocoaPods 时,EarlGrey gem 会在pod install后自动完成全部配置(详见 gem/lib/earlgrey/configure_earlgrey.rb)。其中add_earlgrey_copy_files_script方法的实现与手工配置完全对应:
def add_earlgrey_copy_files_script(target, framework_ref) earlgrey_copy_files_phase_name = 'EarlGrey Copy Files' # 若已存在同名 phase 则跳过,保证幂等 return true if target.copy_files_build_phases.any? do |copy_files_phase| copy_files_phase.name == earlgrey_copy_files_phase_name end return false unless target.product_type.eql? UNITTEST_PRODUCTTYPE new_copy_files_phase = target.new_copy_files_build_phase(earlgrey_copy_files_phase_name) new_copy_files_phase.dst_path = '$(TEST_HOST)/../' new_copy_files_phase.dst_subfolder_spec = '0' build_file = new_copy_files_phase.add_file_reference framework_ref, true build_file.settings = { 'ATTRIBUTES' => ['CodeSignOnCopy'] } build_file end从源码可以看到三个值得注意的细节:
- 幂等保护:gem 先检查是否已存在名为 "EarlGrey Copy Files" 的 phase,存在则直接返回,避免重复添加。
- 目标类型校验:只有 unit-test 类型的 bundle(
com.apple.product-type.bundle.unit-test)才会被添加该 phase,避免误改普通 target。 - CodeSignOnCopy:与手工配置一致,保证复制后的 framework 被正确签名。
三、改造二:Scheme 中的DYLD_INSERT_LIBRARIES
3.1 为什么必须在 main() 之前加载 EarlGrey?
IFAQ 文档明确指出第二项改造的目的:
EarlGrey 需要在 App 之前被加载,以确保不会遗漏任何本应被跟踪的状态,同时让 EarlGrey 能够细粒度地控制测试执行。
从源码看,这种"先于 main() 加载"的设计体现在XCTestCase+GREYAdditions的分类实现中(EarlGrey/Additions/XCTestCase+GREYAdditions.m)。该分类通过+load方法在 XCTest 加载时立即执行关键初始化:
+ (void)load { @autoreleasepool { // Swizzle XCTestCase::invokeTest 与 recordFailureWithDescription:..., // 以便接管测试执行流程、报告失败位置 ... // As soon as XCTest is loaded, we setup the EarlGrey crash handlers // so that any issue is tracked at the earliest. Also, we turn on // accessibility for the simulator since it needs to be enabled before // main is called. [[GREYAutomationSetup sharedInstance] prepareOnLoad]; gExecutingTestCaseStack = [[NSMutableArray alloc] init]; } }prepareOnLoad(GREYAutomationSetup.m)会安装崩溃处理器,并在模拟器上提前开启 accessibility——而 accessibility 必须在main()被调用之前开启,这正是"先于 App 加载"诉求的直接证据。此外,EarlGrey 通过 swizzleXCTestCase::invokeTest接管了每个测试的启动流程(grey_invokeTest中执行preparePostLoad并在真机上启用 accessibility、关闭自动纠错等),实现了对测试执行的细粒度控制。
3.2 Scheme 环境变量的具体配置
在测试 target 的 Scheme 中,进入Edit Scheme → Test Action,取消勾选 "Use the Run action's arguments and environment variables",然后添加:
Key: DYLD_INSERT_LIBRARIES Value: @executable_path/EarlGrey.framework/EarlGrey同时确保 "Expand Variables Based On" 指向被测 App(详见 docs/install-and-run.md 的 Scheme 配置小节):
DYLD_INSERT_LIBRARIES是 dyld(动态链接器)提供的预注入机制,会在可执行文件启动、main()尚未执行前把指定动态库插入进程。此处配合 Copy Files 阶段把 framework 放进MyApp.app/EarlGrey.framework,运行时通过@executable_path相对路径定位并加载它。正因为这样,EarlGrey 的+load初始化逻辑能够在 App 的任何业务代码运行之前执行,从而从第一个 App 状态起就开始跟踪。
3.3 gem 的 Scheme 自动化修改
gem 的add_environment_variables_to_test_scheme方法(configure_earlgrey.rb)封装了同样的逻辑。核心常量定义为:
ENVIRONMENT_KEY = 'DYLD_INSERT_LIBRARIES'.freeze ENVIRONMENT_VALUE = '@executable_path/EarlGrey.framework/EarlGrey'.freeze该方法会扫描测试 target 关联的所有 shared/user scheme(schemes_for_native_targets),然后:
- 若 Test Action 勾选了 "Use the Run action's arguments and environment variables",先复制 Launch Action 已有的环境变量,避免覆盖用户原有配置;
- 检查是否已包含
ENVIRONMENT_VALUE,包含则跳过(幂等,输出 "already set up ... ignored"); - 否则把
DYLD_INSERT_LIBRARIES追加到 Test Action 环境变量中(已有值时用冒号:拼接),并关闭should_use_launch_scheme_args_env,最后保存 scheme。
四、加载时序与"内嵌"的完整链路
综合以上分析,EarlGrey 测试运行时的完整加载链路如下:
- 构建期:Copy Files Build Phase 把测试 target 已链接的 EarlGrey.framework 复制到
$(TEST_HOST)/..(即MyApp.app/EarlGrey.framework),并完成代码签名; - 启动期:Scheme 中注入的
DYLD_INSERT_LIBRARIES = @executable_path/EarlGrey.framework/EarlGrey让 dyld 在main()之前加载 framework; - 初始化期:framework 中的
XCTestCase+GREYAdditions分类+load执行 swizzle 并调用prepareOnLoad(安装崩溃处理器、模拟器开启 accessibility); - 测试期:每个测试用例调用
grey_invokeTest,执行preparePostLoad(真机开启 accessibility、禁用自动纠错),同时GREYAppStateTracker全程跟踪 App 状态,App 空闲时才放行交互与断言。
这套设计也解释了为什么文档强调只有测试 target 依赖 EarlGrey.framework——框架只需注入一次、常驻一份。相关 Q&A 可参考 docs/faq.md 中关于重复链接的条目。
五、配置错误时的典型故障与排查
这两项改造如果配置不当,会直接表现为链接或加载类错误,以下是仓库文档(docs/faq.md、docs/ifaq.md)记录的典型场景。
5.1 日志出现 "XXX is implemented in both YYY and ZZZ"
如果运行日志中出现大量"XXX is implemented in both YYY and ZZZ. One of the two will be used. Which one is undefined.",通常意味着 EarlGrey 被链接了多次。排查要点:
- 确认只有Test Target依赖 EarlGrey.framework;
- 确认 EarlGrey.framework 是从测试 target 的构建产物(built products)通过 Copy Files Build Phase 内嵌到被测 App(
$TEST_HOST),而不是直接链接进 App target。
5.2 崩溃 "Could not swizzle …"
EarlGrey/Common/GREYSwizzler.m 负责方法交换,若报"Could not swizzle ..."崩溃,同样是 EarlGrey 被多次链接导致对同一方法重复 swizzle。修复方式与 5.1 相同:确保只有 Test Target 链接框架,且框架经由测试 target 的 Copy Files 阶段内嵌到被测 App。
5.3 "dyld: could not load inserted library '@executable_path/EarlGrey.framework/EarlGrey' because image not found"
此错误表示动态加载器在@executable_path/EarlGrey.framework/EarlGrey处找不到 framework。排查步骤:
- 构建 Test Target,检查被测 App 包内是否存在 framework——例如 App 名为
MyApp时,应看到MyApp.app/EarlGrey.framework; - 若不存在,确认测试 target 的 Build Phases 中有指向
$(TEST_HOST)的 Copy Files 阶段,按 docs/install-and-run.md 重新配置后重建再检查; - 若 framework 仍然缺失,则需要结合工程结构与完整报错信息进一步定位。
5.4 验证 framework 是否正确内嵌
以示例工程为例,运行一次EarlGreyExampleSwiftTeststarget 后,可以在 DerivedData 中找到测试 bundle:
cd ~/Library/Developer/Xcode/DerivedData/EarlGreyExample-*/Build/Products/Debug-iphonesimulator/EarlGreyExampleSwift.app/PlugIns/EarlGreyExampleSwiftTests.xctest/真机构建时把Debug-iphonesimulator替换为Debug-iphoneos(详见 docs/ifaq.md 中 "Where do I find the XCTest bundle" 一节)。
六、小结
EarlGrey 的"Scheme 修改 + Copy Files Build Phase"并非繁琐的工程仪式,而是其白盒架构的必然要求:
| 改造项 | 解决的问题 | 关键值 |
|---|---|---|
| Copy Files Build Phase | 把 EarlGrey.framework 内嵌进被测 App | Destination = Absolute Path、Path = $(TEST_HOST)/..、CodeSignOnCopy |
| Scheme 环境变量 | 在main()之前加载 EarlGrey,不遗漏任何 App 状态 | DYLD_INSERT_LIBRARIES = @executable_path/EarlGrey.framework/EarlGrey |
无论是手工配置还是通过 CocoaPods 让 gem 自动完成,最终目的都是让 EarlGrey 与被测 App 同进程、先启动、全程跟踪。理解了这两项改造背后的加载时序与状态跟踪机制,就可以在遇到重复链接、swizzle 崩溃或 dyld 加载失败等问题时快速定位根因。若需要进一步了解同步机制的配置项(如动画时长阈值、网络请求黑名单),可继续阅读 docs/features.md 与 docs/api.md;完整的安装配置流程见 docs/install-and-run.md。
- 测试
【免费下载链接】EarlGrey
:tea: iOS UI Automation Test Framework
相关推荐
告别调试噩梦:为什么TheOdinProject必须用Jest测试JavaScript?
告别调试噩梦:为什么TheOdinProject必须用Jest测试JavaScript? 你还在靠console.log调试代码?面对"运行正常却提交失败"的练
文档教程教育gevent.wsgi 模块的移除与历史沿革:为什么现在必须改用 gevent.pywsgi
gevent.wsgi 模块的移除与历史沿革:为什么现在必须改用 gevent.pywsgi 导读 本文以 gevent 官方 API 文档中 gevent.w
后端electron-builder v27 配置迁移修复:为什么 GitLab 的 `vPrefixedTagName` 必须被原样保留
electron builder v27 配置迁移修复:为什么 GitLab 的 vPrefixedTagName 必须被原样保留 本篇技术指南围绕 elect
构建工具桌面应用开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考