- 测试
【免费下载链接】EarlGrey
:tea: iOS UI Automation Test Framework
导读
在基于 EarlGrey 的 iOS UI 自动化测试中,定位元素的核心是编写精确的 matcher(匹配器)。但当 App 内出现属性完全相同的重复元素(如相同的 accessibility ID、相同的文本与相同的层级位置)时,内置 matcher 无法区分它们,测试会因“匹配到多个元素”而失败。本文以仓库文档 docs/examples.md 中的官方示例为主线,讲解如何通过GREYElementMatcherBlock编写一个带“一次性命中”状态的 Swift 自定义 matcher——grey_firstElement(),用于在重复元素场景下选取第一个匹配项,并结合 EarlGrey 源码剖析其底层原理与使用边界。
读完本文你将掌握:EarlGrey matcher 协议与块式自定义 matcher 的完整结构、matches:与describeTo:两个 block 的正确写法、以及自定义 matcher 与grey_allOf()、grey_interactable()等内置 matcher 的组合用法。
一、问题背景:为什么重复元素会让 EarlGrey 测试失败
EarlGrey 的交互式 API 要求一次selectElement(with:)调用最终唯一确定一个元素。从实现上看,交互建立在 matcher 之上:GREYElementInteraction.m 中的grey_uniqueElementInMatchedElements:andError:会对匹配结果计数(见 GREYElementInteraction.m 第 507-540 行):
- 匹配到0 个元素 → 产生“元素未找到”错误;
- 匹配到多于 1 个元素 → 产生
kGREYInteractionMultipleElementsMatchedErrorCode错误,错误信息形如Multiple elements were matched: ... Please use selection matchers to narrow the selection down to single element.(见 GREYElementInteraction.m 第 947-961 行); - 仅当匹配到1 个元素时才直接返回该元素。
因此,当 App 中同时存在两个 accessibility ID 均为some_id的控件,且它们的文本、类、位置等一切属性都相同时,任何内置 matcher 或 matcher 组合都无法将两者区分开,交互必然失败。
官方文档 docs/faq.md 对这类“元素在 App 中重复”的问题给出的建议是:先组合 matcher(如grey_allOf()叠加grey_interactable()或grey_sufficientlyVisible()),尽力缩小候选集。而当所有属性完全相同、组合也无法区分时,examples.md提供的思路是——取第一个匹配的元素,用自定义 matcher 打破僵局。
二、核心方案:grey_firstElement()自定义 Swift Matcher
以下代码完整来自 docs/examples.md 的官方示例(Swift):
// Swift Custom Matcher /** * Example Usage: * * EarlGrey.selectElement(with:grey_allOfMatchers([ * grey_accessibilityID("some_id"), * grey_interactable(), * grey_firstElement()])).assert(grey_notNil()) * * Only intended to be used with selectElementWithMatcher. */ func grey_firstElement() -> GREYMatcher { var firstMatch = true let matches: MatchesBlock = { (element: AnyObject!) -> Bool in if firstMatch { firstMatch = false return true } return false } let description: DescribeToBlock = { (description: GREYDescription!) -> Void in guard let description = description else { return } description.appendText("first match") } return GREYElementMatcherBlock.init(matchesBlock: matches, descriptionBlock: description) }这段代码的核心逻辑非常简洁:闭包通过外部捕获的可变变量firstMatch记录状态——第一次被调用时返回true并把标记置为false,此后所有调用都返回false。于是该 matcher 在整个匹配流程中“只会命中一次”,恰好把候选元素集收敛为唯一的一个元素。
使用方式与官方用法示例
EarlGrey.selectElement(with: grey_allOfMatchers([ grey_accessibilityID("some_id"), grey_interactable(), grey_firstElement() ])).assert(grey_notNil())grey_accessibilityID("some_id"):按 accessibility ID 筛选,先圈定同 ID 的重复元素集合;grey_interactable():过滤掉不可交互的元素(EarlGrey 判定可交互需满足“至少若干像素可见,且 accessibility 激活点或可见区域中心完全可见”,见 GREYMatchers.h 第 146-154 行);grey_firstElement():在剩余候选中取第一个。
grey_allOfMatchers(...)接收 matcher 数组并按顺序执行 AND 逻辑——这是 Swift 下的标准写法。官方文档 docs/api.md 特别说明:为了兼容 Swift,应使用grey_allOfMatchers()/grey_anyOfMatchers()而非 Objective-C 的变长参数版本grey_allOf()/grey_anyOf()(见 docs/api.md 第 134 行)。
三、深入源码:GREYElementMatcherBlock 与 Matcher 协议的工作原理
3.1 GREYMatcher 协议的四要素
所有 EarlGrey matcher 都必须遵循 GREYMatcher 协议,其核心方法包括:
- (BOOL)matches:(id)item:对元素做布尔判定,是匹配逻辑的入口;- (BOOL)matches:describingMismatchTo::匹配失败时向mismatchDescription追加失败原因;- (void)describeTo:(id<GREYDescription>)description:生成 matcher 自身的文本描述,用于失败时的报错信息;- (void)describeMismatchOf:to::描述不匹配的具体原因。
自定义 matcher 只要实现matches:与describeTo:即可(GREYBaseMatcher.h 要求子类必须实现这两个方法),其余方法由基类GREYBaseMatcher提供默认行为。
3.2 GREYElementMatcherBlock:用 Block 实现协议
GREYElementMatcherBlock 是 EarlGrey 专门为“用代码块快速造 matcher”提供的类,它定义了两个 block 类型:
MatchesBlock:签名BOOL (^)(id element),对应GREYBaseMatcher::matches:,参数element是待匹配的 UI 元素或 accessibility 元素;DescribeToBlock:签名void (^)(id<GREYDescription> description),对应describeTo:,负责把 matcher 的描述文字写入GREYDescription。
从实现看(GREYElementMatcherBlock.m),matches:与describeTo:分别转发给_matcherBlock与_descriptionBlock两个属性,构造时还会通过GREYThrowOnNilParameter对两个 block 做非空校验,因此两个 block 都是必填项。
3.3 两个 Block 的职责拆解
对照官方示例逐行分析:
matchesblock 承担“有状态”匹配:借助 Swift 闭包捕获var firstMatch实现“第一次调用放行、之后全部拒绝”。这正是解决重复元素的巧妙之处——它利用 EarlGrey 匹配流程会按 UI 层级顺序逐个调用 matcher 的特性,把“谁先被遍历到”转化为“谁被选中”。descriptionblock 提供可读的描述:description.appendText("first match")让 matcher 在失败报告中呈现为first match。别小看这一步,没有良好的描述,测试失败时你只能看到一串晦涩的表达式。
值得一提的细节:示例中description闭包对参数做了guard let description = description else { return }解包,这是为了防御传入 nil;实际上GREYElementMatcherBlock的初始化器本身会拒绝 nil 参数,这里属于防御性写法。
四、组合原理:grey_allOf 的短路求值与调用顺序
4.1 GREYAllOf 的 AND 逻辑
grey_allOfMatchers返回的是 GREYAllOf 实例。其matches:describingMismatchTo:实现(GREYAllOf.m 第 43-50 行)按传入顺序遍历所有子 matcher,任何一个失败立即短路返回 NO,后续 matcher 不再被调用。
这个行为对grey_firstElement()至关重要:firstMatch状态的推进只发生在所有前置 matcher 都通过的前提下,因此“first”指的是同时满足 accessibility ID 与可交互条件后的第一个元素,而不是整个 UI 层级中的第一个。
4.2 为什么官方建议把精确 matcher 放前面
docs/api.md 在 matcher 章节给出了一个重要提示:grey_allOf的子 matcher 顺序会影响性能——若把grey_sufficientlyVisible()放在最前面,EarlGrey 会对整个 App 的所有元素逐一做可见性计算,开销很大;应把最有选择性的 matcher(如 accessibility ID、label)放在最前,尽快缩小候选集(见 docs/api.md 第 121-123 行)。
因此grey_firstElement()的推荐用法是把grey_accessibilityID(...)放在首位、grey_firstElement()放在最后,与官方示例一致。
五、局限性与替代方案
5.1 使用边界:必须搭配其他 matcher 使用
官方示例的注释明确写着“Only intended to be used with selectElementWithMatcher”,并强调该 matcher 依赖“第一次调用返回 true”的状态语义,因此:
- 它不能独立使用——如果单独对全层级匹配,你得到的只是“UI 树中第一个元素”,没有任何业务意义;
- 它不能用于断言单个元素属性(如
assert阶段),因为匹配阶段已经消费掉了唯一一次的true; - 重复元素问题的治本之策仍是修改 App。
examples.md开篇就指出:当应用存在完全相同的重复元素时,正确的修复方式是更新 App 避免 UI 重复;只有“无法修改”时才用“取第一个匹配”作为 workaround。
5.2 官方 FAQ 中的正统做法
对于重复但并非完全同构的元素(例如文本相同但可见性不同),官方 FAQ 给出的建议是组合 matcher 后追加grey_interactable()或grey_sufficientlyVisible(),让 EarlGrey 自动过滤掉不可交互/不可见的候选,通常就能收敛到唯一元素——这是比grey_firstElement()更优先尝试的方案。
5.3 其他内置的降级手段
即使候选集仍有多个,也不一定非要自定义 matcher:GREYInteraction 的atIndex:允许按索引选取第 N 个匹配元素(索引越界会抛异常)。它的代价是需要预先知道目标元素在匹配列表中的确切位置,而grey_firstElement()的优势是不依赖索引知识、语义自解释。
六、延伸:在仓库示例中观察 matcher 组合的完整写法
仓库自带的 EarlGreyExampleSwiftTests.swift 提供了多个可直接运行的 matcher 组合范例,可作为编写自定义 matcher 时的对照:
- 集合 matcher 消歧:
grey_allOf([grey_accessibilityID("ClickMe"), grey_sufficientlyVisible()])(第 61-67 行),与本文的grey_firstElement()组合属于同一套路; - 自定义 matcher 完整模板:
testWithCustomMatcher(第 77-107 行)演示了与grey_firstElement()完全相同的三段式结构——先写MatchesBlock判定逻辑,再写DescribeToBlock描述,最后GREYElementMatcherBlock.init(matchesBlock:descriptionBlock:)组装并在selectElement中使用; - 单元测试验证:GREYElementMatcherBlockTest.m 中的
testDescriptionOfAllOfMatcher断言grey_allOf(pass1, pass2, nil)的描述为(pass1 && pass2),验证了组合 matcher 描述的输出格式——自定义 matcher 的 description 会以这种括号形式出现在失败报告中。
七、小结
| 要点 | 说明 |
|---|---|
| 问题 | 属性完全相同的重复元素导致Multiple elements were matched失败 |
| 方案 | 用GREYElementMatcherBlock编写带一次性命中状态的grey_firstElement() |
| 组合 | 与grey_allOfMatchers([...])、grey_accessibilityID、grey_interactable配合 |
| 原理 | GREYAllOf按序短路求值,firstMatch状态保证只放行第一个通过前置条件的元素 |
| 边界 | 仅用于selectElement;治本方案是修改 App 去除重复 UI |
| 依据 | docs/examples.md、GREYElementMatcherBlock.m、GREYAllOf.m、GREYElementInteraction.m、docs/faq.md |
写自定义 matcher 是 EarlGrey 测试能力的重要扩展点:无论是“取第一个匹配元素”,还是官方 api.md 中“匹配无子视图元素”的示例,都遵循MatchesBlock+DescribeToBlock的统一范式。掌握GREYElementMatcherBlock之后,你就能针对业务里的任何特殊定位需求,写出清晰、可复用、失败信息友好的自定义 matcher。
- 测试
【免费下载链接】EarlGrey
:tea: iOS UI Automation Test Framework
相关推荐
laravel-soft-cascade vs 原生级联:为什么软删除级联更优?
laravel soft cascade vs 原生级联:为什么软删除级联更优? 在Laravel开发中,数据关联删除是常见需求。原生数据库级联(如 ON DE
3分钟掌握videocr:从视频中智能提取硬编码字幕的终极方案
3分钟掌握videocr:从视频中智能提取硬编码字幕的终极方案 你是否曾经为无法复制视频中的硬编码字幕而烦恼?无论是外语学习、内容创作还是视频分析,手动转录字幕
图像处理视频处理JointJS 自定义元素工具实战:用 dia.ToolView 实现 Port 拖拽重排
JointJS 自定义元素工具实战:用 dia.ToolView 实现 Port 拖拽重排 JointJS 内置的元素工具(Element Tools)覆盖了常
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考