1. EarlGrey是什么先搞清楚它解决什么问题做iOS测试的同学应该都有同感UI自动化这块选择一直很纠结。Appium、XCUITest、KIF各有一批拥趸但对原生iOS项目来说EarlGrey是一个绕不开的名字。它由一家大型科技公司的原厂团队开源维护底层基于XCTest但又不像XCUITest那样通过外部进程驱动App而是直接注入到被测App的进程里运行。这个差异决定了它的稳定性、速度和排查方式都跟XCUITest完全不一样。在介绍安装步骤之前我先说清楚EarlGrey的定位它不是一个录制回放工具也不是一个跨平台的框架它就是为iOS原生工程设计的一套白盒UI测试框架。白盒意味着测试代码能直接访问App内部的对象、状态、网络层这在做复杂业务断言时非常有用。比如你需要验证一个页面在某个网络响应到达后是否刷新了列表用XCUITest只能靠肉眼或者等待元素出现而EarlGrey可以结合它的同步机制精准控制执行时机。那它适合谁如果你是一个iOS开发或者测试开发日常需要给App写UI测试用例而且项目是用Swift或者Objective-C写的原生工程那EarlGrey值得认真学习。如果你做的是跨平台H5混合App或者需要一套脚本同时跑iOS和Android那Appium那套可能更适合你EarlGrey就不用考虑了。说白了它的理念是专注于一个平台做深做透。1.1 三大主流iOS UI测试方案对比我接触过的iOS UI自动化方案大致可以分成三类XCUITest、KIF、EarlGrey。XCUITest是苹果官方给的方案测试代码跑在独立的测试进程里通过XCTest框架和App之间建立连接用模拟用户点击的方式来操作界面。它最大的优势是生态原生、Xcode支持好、稳定性也不错但有个硬伤它只能看到UI层的东西访问不到App内部的状态而且如果你需要等待某个网络回调、某个动画结束后再做下一步操作XCUITest提供的等待接口写起来很别扭很容易写出又慢又脆的用例。KIF是另一个老牌框架也是注入式运行的它在测试代码里通过Accessibility找元素并模拟触摸事件。KIF的问题主要是它本身不包含完整的同步机制很多时候需要自己写等待测试多了之后维护成本很高。EarlGrey同样是注入式运行但它把同步机制做成了框架的核心能力。它会在执行每个交互之前自动等待App主线程空闲下来等动画结束、网络请求返回、RunLoop稳定之后才继续下一步。这一点用起来是真的省心也是我最终在项目里选择它的主要原因。1.2 为什么我会选择EarlGrey之前参与的一个电商类App项目UI自动化用例跑一圈要将近两个小时而且经常因为某个弹窗动画还没结束、某个接口响应慢了几百毫秒就随机失败。当时团队用的方案就是XCUITest每次排查失败都要反复看截图和日志后来我们把核心流程逐步迁移到EarlGrey上一个最直观的变化是用例的等待时间不再靠sleep硬凑了执行速度变快失败率也明显下降。另一个让我比较看重的是它的断言能力。EarlGrey的匹配器Matcher体系非常灵活你可以用grey_allOf、grey_anyOf、grey_not这样的逻辑组合把“某个父容器里符合条件的一个元素”精确地找出来。对于复杂页面这个能力比XCUITest里单纯靠identifier找元素的容错率高太多了。2. 安装之前的准备环境与工程接入EarlGrey的安装不像普通三方库那样简单它需要把测试依赖和被测App配置都处理好否则很容易出现启动崩溃或者找不到符号的问题。我先列一个环境清单再分两种方式讲接入过程。2.1 环境要求清单macOS系统Xcode版本建议12.0以上我实测在Xcode 14和15上都没有遇到兼容性问题。项目最低部署版本建议iOS 11以上太低的版本会有部分API不可用。你需要有CocoaPods或者Carthage二选一就行。个人强烈建议用CocoaPods因为它对EarlGrey的配置封装最完整手动接入容易漏掉一些链接参数。测试工程必须开启XCTest的UI Testing Target配置EarlGrey虽然跑在App进程内但对外仍然表现为一个XCTest测试Target。2.2 基于CocoaPods的接入步骤第一步是在工程根目录下编辑Podfile。假设你的App Target叫做MyApp需要新建一个UITests的Target名字叫做MyAppUITests那么在Podfile里这样写platform :ios, 12.0 target MyAppUITests do use_frameworks! pod EarlGrey end注意这里有个关键点EarlGrey这个pod不应该打进App Target里它只需要存在于UITests Target。因为EarlGrey的代码是要跟随测试包一起注入到App进程的如果错误地加进了App Target会导致App在启动时加载测试代码这在发布包是不可接受的。第二步是执行安装pod install执行完成后你会发现MyAppUITests这个Target里自动多了一个EarlGrey的framework引用。这里有一个我踩过的坑CocoaPods安装之后如果Podfile里的use_frameworks!没有开启EarlGrey可能会出现链接报错。它的源码是OC和Swift混编的静态库方式链接时经常会遇到符号找不到的问题所以一定保持use_frameworks!为开启状态。第三步也是最容易忽略的一步需要在MyAppUITests的Info.plist里配置一个环境变量让EarlGrey在加载时能做参数校验。打开MyAppUITests/Info.plist添加keyGreyEnableCrashHandling/key true/如果不加这个配置有些iOS版本在测试过程中发生了崩溃EarlGrey无法接管崩溃信息只会给你一个模糊的test crashed提示排查起来非常痛苦。这个变量我建议从一开始就加上。2.3 无CocoaPods场景的手动接入方案如果你的项目因为某些原因不能使用CocoaPodsEarlGrey也提供了手动集成的方式。这个方式我试验过一次主要步骤是从EarlGrey的官方仓库下载源码包。将源码里的EarlGrey.xcodeproj拖入你的Xcode工程作为子工程。在MyAppUITestsTarget的Build Phases里把EarlGrey相关的framework作为Embedded Binaries加入。在Build Settings里设置-ObjC链接标志否则Swift调用OC代码时会报类方法找不到。在MyAppUITests的Header Search Paths里添加EarlGrey源码的头文件路径。手动接入最容易出错的就是-ObjC这个标志和头文件路径如果漏了编译能过但一运行就崩在Class not found。我的建议是除非项目有硬性限制否则尽量使用CocoaPods接入省时省心。3. 第一个测试用例从选中元素到断言安装完成之后就可以写用例了。这一节我会从核心API讲起以一个登录流程的用例为例子把元素选择、交互、断言这三板斧过一遍同时分享一些我在写用例时总结的写法规范。3.1 EarlGrey核心API速览EarlGrey的API设计其实就是三段式选择Select → 交互Perform → 断言Assert。选择用EarlGrey().selectElement(with:)参数是一个匹配器。交互用.perform(...)参数是一个Action比如点击、滑动、输入。断言用.assert(...)参数是一个断言比较器比如断言可见、断言存在、断言文本一致。匹配器这块是EarlGrey的精华。grey_text(登录)匹配文本内容grey_accessibilityID(login_button)匹配Accessibility标识grey_kindOfClass(UITableViewCell.self)匹配元素类型grey_sufficientlyVisible()匹配可见性。它们之间可以用grey_allOf、grey_anyOf做组合用grey_not取反。我经常用这样的组合来精确匹配一个在Cell里面的按钮grey_allOf([ grey_accessibilityID(sku_add_button), grey_ancestor(grey_kindOfClass(NSClassFromString(ProductCell)!)) ])这个意思是在ProductCell类型的祖先元素里去找一个ID为sku_add_button的控件。为什么要这样写因为一个列表页里可能有很多个“加入购物车”按钮它们的ID都一样如果不限定祖先范围EarlGrey会报匹配到多个元素测试直接失败。这个匹配器组合几乎贯穿了我在复杂页面里写的每一个用例。3.2 元素选择、交互与断言的标准写法下面是一个完整的登录测试用例我把关键步骤都注释出来了import XCTest import EarlGrey final class LoginFlowTests: XCTestCase { override func setUp() { super.setUp() // 一个用例失败后继续执行后续用例方便看全量失败情况 continueAfterFailure true // 重启App到初始状态 let app XCUIApplication() app.launch() } func testLoginWithValidAccount() { // 输入用户名 EarlGrey() .selectElement(with: grey_accessibilityID(login_username_input)) .perform(grey_typeText(tester01)) // 输入密码 EarlGrey() .selectElement(with: grey_accessibilityID(login_password_input)) .perform(grey_typeText(Passw0rd!)) // 点击登录按钮 EarlGrey() .selectElement(with: grey_accessibilityID(login_submit_btn)) .perform(grey_tap()) // 断言首页出现欢迎语 EarlGrey() .selectElement(with: grey_text(欢迎回来tester01)) .assert(grey_sufficientlyVisible()) } }这里有几个细节值得注意。grey_typeText这个操作要求目标元素是文本框而且EarlGrey要求文本框处于可输入状态如果键盘弹不出来或者元素被遮挡这个操作会直接失败。所以在写用例之前先确认输入框在界面上是可见且可交互的。如果你想要清空一个输入框的默认值再输入使用grey_replaceText(新值)会更可靠它的行为是直接替换整段文字不经过键盘逐字输入速度更快也不受输入法影响。代价是它只做赋值不会触发真实的用户输入事件所以依赖文本变化回调的输入框慎用。3.3 测试文件命名与调试技巧EarlGrey测试文件会被Xcode标记为灰色图标这个“灰色”是框架命名的由来之一。团队协作时我建议测试文件按照业务模块命名例如LoginFlowTests、CheckoutFlowTests一个文件对应一个核心流程不要把所有用例堆在同一个文件里。EarlGrey的用例不像单元测试那样能很好地并行执行所以文件粒度适中就好避免单文件过大导致跑完一个流程要等很久。调试时还有一个特别好用的技巧在测试方法里加断点然后用po EarlGrey().selectElement(with: grey_text(xxx))查看匹配结果。在LLDB控制台直接调用EarlGrey API可以快速确认某个元素是否真的存在、匹配器是否写对了比跑完整个用例再复盘快得多。我在复杂页面写匹配器的时候经常这样干。4. 同步机制EarlGrey最值得依赖的特性EarlGrey的同步机制是它区别于其他框架的核心特性也是很多人在用的时候“知其然不知其所以然”的地方。这一节我会讲清楚同步是怎么工作的以及在哪些场景下它的同步会失效需要我们自己兜底。4.1 为什么同步机制重要你回想一下用XCUITest写用例的经历点击一个按钮后要不要sleep两秒网络请求还没回来列表还没渲染但你已经开始找元素了于是用例失败。为了解决这种稳定性问题很多团队只能疯狂加等待用例越写越慢。EarlGrey的同步机制解决的就是这个问题。框架在执行任何交互之前会等待以下条件同时满足App的主线程处于空闲状态RunLoop没有在忙。没有正在执行的动画。没有等待中的网络请求。没有正在调度的Dispatcher Source。UI事件队列清空。只有当系统“闲下来”了EarlGrey才会真正执行点击。这个机制让测试流程贴近真实用户的行为逻辑用户本来也不可能在页面上疯狂滑动的同时点按钮。所以EarlGrey的用例天然就有一种“真实操作感”。这个机制的原理不复杂它就类似一个看门狗注册为App主线程的Observer监控RunLoop的状态。一旦发现有线程或者动画还在忙就进入等待循环直到超时失败。这个“等多久”是通过GREYConfiguration配置的默认是10秒。4.2 常见同步失效场景与自定义等待同步机制虽然强大但它不是万能的有几个场景它会“瞎掉”。第一个是无限动画。比如加载中的菊花转圈动画理论上它会一直转EarlGrey会认为App永远不空闲直到超时抛出异常。应对方法是在测试用例中提前把动画关掉或者给动画操作配置成“不跟踪”。我用的比较多的做法是在App的启动参数里加一个开关测试环境直接禁用加载动画。第二个是外部网络请求。EarlGrey默认会自动跟踪URLSession发起的网络请求但它只能等请求完成回调如果你用的是第三方网络库底层开启了一个没有结束的常驻长连接EarlGrey会误判为“有请求没完成”。遇到这种情况可以手动把长连接从idling resources里移除具体做法是给网络库添加EarlGrey的idling resource适配或者简单粗暴地在测试配置里关闭对网络请求的等待。第三个是我们自己的多线程逻辑。如果App里有一个后台线程一直在循环执行任务EarlGrey无法感知这个线程什么时候算“空闲”。这时候需要用EarlGrey提供的GREYCondition来手动等待某个条件成立不要再依赖框架的自动等待。示例let condition GREYCondition(name: 等待数据加载) { () - Bool in return AppState.shared.isDataReady } let success condition!.wait(withTimeout: 5) XCTAssertTrue(success, 数据在5秒内未加载完成)这种手动等待和线程循环相结合的方式是我在实际项目中处理复杂异步逻辑的标准方案。记住一个原则框架的自动同步负责常规场景特殊场景一定要主动接管不要对抗它的判断逻辑。5. 常见问题排查与避坑实录下面这些问题基本是每个初学者都会遇到的我把它们的现象和排查思路整理一下。5.1 启动崩溃与依赖冲突现象一测试Target启动后立刻崩溃日志里有dyld: Library not loaded或者Class not found。这个大概率是链接配置问题。优先检查Podfile里use_frameworks!有没有开其次检查是否误把EarlGrey加进了App Target。还有一种情况是项目里同时集成了其他测试框架比如KIF或者OCMock它们在加载时会互相覆盖方法导致EarlGrey初始化失败。我的建议是一个UITests Target里只保留EarlGrey其他mock框架放到单元测试Target里。现象二编译报cannot find EarlGrey in scope。这个通常是import路径没找到。检查Build Settings里的Framework Search Paths是否包含Pod产物路径确认Pod安装后已经重新打开了.xcworkspace而不是.xcodeproj。我有一次就是一直开着旧工程文件编译白白浪费了半个小时。5.2 元素不可见、点击无效现象元素明明在屏幕上但EarlGrey说它not visible或者not hittable。EarlGrey判断一个元素是否可以点击不是看它有没有被渲染出来而是看它那个位置能不能接受点击事件。常见原因是元素被另一个透明视图盖住了或者父视图的alpha非常低或者元素的frame有一部分落在屏幕外。排查方法我很推荐一个在失败日志里打开EarlGrey自动生成的截图和层级树。当你用一个匹配器匹配到多个元素时它会打印出Element Snapshot能看到每个候选元素的frame、accessibility属性、父链关系。我通常先看层级树里有没有一个全屏的透明容器盖在按钮上面如果有把匹配器加上grey_interactable()条件去过滤掉不可交互的匹配项。5.3 网络请求导致的超时现象用例在某个页面执行时频繁超时报错信息指向WAIT_FOR_IDLE。如果页面本身没有无限动画那大概率是网络层有请求一直挂在Pending状态。遇到这种情况我先打开App运行日志确认是否有请求发出了但没有收到响应。很多测试环境里接口会返回很慢或者直接超时EarlGrey的自动等待会一直挂着直到网络请求超时才算结束用例自然就慢了。一种处理方式是测试环境中让接口返回静态本地数据既快又可控这属于接口Mock的范畴不细展开。另一种方式是主动降低超时等待在setUp里用GREYConfiguration.sharedInstance().setValue(5, forKey: kGREYConfigKeyInteractionTimeout)把单次交互超时从10秒缩到5秒不给慢接口留太多拖时间的机会。不过要注意这只是让失败更快暴露并不能根治问题。我整理了一个常用问题速查表供参考现象可能原因排查顺序启动即崩溃链接配置错误 / 框架冲突查Podfile、查依赖列表、查启动日志找不到元素匹配器写错 / 元素未渲染用LLDB手动匹配、看层级树元素不可点击被遮挡 / 不可交互状态看Element Snapshot、加interactable过滤无尽头等待无限动画 / 长连接关动画、移除idling resource断言失败文本不符 / 状态未更新检查异步回调时机、加GREYCondition6. 从Demo到落地接入CI与团队协作如果你只是个人体验写完用例在自己的Xcode里跑通就够了。但要把它用在一个正经的项目里还需要解决命令行执行、CI集成和团队习惯这三件事。6.1 命令行运行与报告输出本地调试用Xcode的Test导航面板没问题但正式跑回归一定要用命令行。我常用的命令是xcodebuild test \ -workspace MyApp.xcworkspace \ -scheme MyApp \ -destination platformiOS Simulator,nameiPhone 15,OS17.0 \ -only-testing:MyAppUITests/LoginFlowTests注意-only-testing参数后面跟的是测试类名如果只想跑某一个用例可以写成MyAppUITests/LoginFlowTests/testLoginWithValidAccount。如果用-skip-testing排除掉一些不稳定的用例在CI上也能省不少时间。命令行跑完之后结果会在xcresult包里。用xcrun xcresulttool可以导出测试报告但Xcode 15以后我更喜欢直接在CI上集成能够解析xclog的插件可以直接生成带失败截图和层级树的网页报告。如果不想引第三方插件至少要把xcresult包作为CI产物保留下来排查问题的时候没有它你连失败现场都看不到。6.2 团队落地建议最后聊聊团队落地。EarlGrey对团队成员的要求比普通测试框架要高一点因为它本质上是“站在开发者视角”的框架写的人得理解iOS的事件传递、RunLoop、Accessibility这些概念。我建议在项目组里先指定一两个人把框架吃透再给团队做一次分享不要直接撒给所有人写用例。用例的评审标准我也定了几条不允许出现sleep等待每个用例必须保证可重复执行匹配器必须写在辅助方法里复用而不是每个用例各写一遍。这些规范听着简单但一旦没有约束用例库很快就会变成一团乱麻。EarlGrey不是能自动让你的UI测试变好的魔法它只是给了你一个更好的基础设施真正让它发挥价值的是用它对的人和方法。在我自己项目的实践里EarlGrey最大的惊喜是它让我愿意去写UI测试了因为写起来不痛苦、跑起来不焦虑。如果你正在为XCUITest的脆弱性头疼认真花两周时间把EarlGrey摸透应该能感受到同样的体验。