☰
EarlGrey iOS UI自动化测试实战:同步机制、元素匹配与工程接入
2026/10/18 10:46:42 网站建设 项目流程

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,添加:

<key>GreyEnableCrashHandling</key> <true/>

如果不加这个配置,有些iOS版本在测试过程中发生了崩溃,EarlGrey无法接管崩溃信息,只会给你一个模糊的"test crashed"提示,排查起来非常痛苦。这个变量我建议从一开始就加上。

2.3 无CocoaPods场景的手动接入方案

如果你的项目因为某些原因不能使用CocoaPods,EarlGrey也提供了手动集成的方式。这个方式我试验过一次,主要步骤是:

  1. 从EarlGrey的官方仓库下载源码包。
  2. 将源码里的EarlGrey.xcodeproj拖入你的Xcode工程,作为子工程。
  3. 在MyAppUITestsTarget的Build Phases里,把EarlGrey相关的framework作为Embedded Binaries加入。
  4. 在Build Settings里设置-ObjC链接标志,否则Swift调用OC代码时会报类方法找不到。
  5. 在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
断言失败文本不符 / 状态未更新检查异步回调时机、加GREYCondition

6. 从Demo到落地:接入CI与团队协作

如果你只是个人体验,写完用例在自己的Xcode里跑通就够了。但要把它用在一个正经的项目里,还需要解决命令行执行、CI集成和团队习惯这三件事。

6.1 命令行运行与报告输出

本地调试用Xcode的Test导航面板没问题,但正式跑回归一定要用命令行。我常用的命令是:

xcodebuild test \ -workspace MyApp.xcworkspace \ -scheme MyApp \ -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.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摸透,应该能感受到同样的体验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询