news 2026/10/12 2:18:50

EarlGrey 测试工程改造原理:为什么必须修改 Scheme 并添加 Copy Files Build Phase

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
EarlGrey 测试工程改造原理:为什么必须修改 Scheme 并添加 Copy Files Build Phase
  • 测试

【免费下载链接】EarlGrey

:tea: iOS UI Automation Test Framework

项目地址:https://gitcode.com/gh_mirrors/ea/EarlGrey
点击查看免费下载

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 pass
  • kGREYPendingCAAnimation:等待 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 阶段应设置为:

配置项值
DestinationAbsolute 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

从源码可以看到三个值得注意的细节:

  1. 幂等保护:gem 先检查是否已存在名为 "EarlGrey Copy Files" 的 phase,存在则直接返回,避免重复添加。
  2. 目标类型校验:只有 unit-test 类型的 bundle(com.apple.product-type.bundle.unit-test)才会被添加该 phase,避免误改普通 target。
  3. 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),然后:

  1. 若 Test Action 勾选了 "Use the Run action's arguments and environment variables",先复制 Launch Action 已有的环境变量,避免覆盖用户原有配置;
  2. 检查是否已包含ENVIRONMENT_VALUE,包含则跳过(幂等,输出 "already set up ... ignored");
  3. 否则把DYLD_INSERT_LIBRARIES追加到 Test Action 环境变量中(已有值时用冒号:拼接),并关闭should_use_launch_scheme_args_env,最后保存 scheme。

四、加载时序与"内嵌"的完整链路

综合以上分析,EarlGrey 测试运行时的完整加载链路如下:

  1. 构建期:Copy Files Build Phase 把测试 target 已链接的 EarlGrey.framework 复制到$(TEST_HOST)/..(即MyApp.app/EarlGrey.framework),并完成代码签名;
  2. 启动期:Scheme 中注入的DYLD_INSERT_LIBRARIES = @executable_path/EarlGrey.framework/EarlGrey让 dyld 在main()之前加载 framework;
  3. 初始化期:framework 中的XCTestCase+GREYAdditions分类+load执行 swizzle 并调用prepareOnLoad(安装崩溃处理器、模拟器开启 accessibility);
  4. 测试期:每个测试用例调用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。排查步骤:

  1. 构建 Test Target,检查被测 App 包内是否存在 framework——例如 App 名为MyApp时,应看到MyApp.app/EarlGrey.framework;
  2. 若不存在,确认测试 target 的 Build Phases 中有指向$(TEST_HOST)的 Copy Files 阶段,按 docs/install-and-run.md 重新配置后重建再检查;
  3. 若 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 内嵌进被测 AppDestination = 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

项目地址:https://gitcode.com/gh_mirrors/ea/EarlGrey
点击查看免费下载
上一篇:gogcli 实战指南:用 `gog slides element group` 在终端组合 Google Slides 元素
下一篇:基于 n8n 的多页网站爬取 Agent 实战:从单条 URL 到跨站点信息聚合(oTTomator Live Agent Studio 版)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 2:14:00

基于Python的视频推荐系统的设计与实现-协同过滤 + 基于内容的混合推荐大数据专业毕业设计深度学习项目

1.3 研究主要内容本研究围绕视频推荐系统的设计与实现展开,重点解决个性化推荐算法在中小型平台的应用问题。研究内容主要包含系统架构搭建、混合推荐算法实现和功能模块开发三个方面。在系统设计层面,采用Django框架搭建Web应用,使用MVC模式…

作者头像 李华
网站建设 2026/10/12 2:13:58

基于随机森林的飞机旅客满意度的数据分析与可视化系统大数据专业毕业设计深度学习项目

本系统致力于飞机旅客满意度的数据分析与可视化,融合了随机森林、Django和Vue等先进技术,为用户提供了一个全面、直观的数据分析平台。用户登录前台后,可以便捷地查看训练模型数据、进行系统管理、探索训练数据、执行预测以及查看训练模型结果…

作者头像 李华