Skip to content

Selenium 元素定位与显式等待 ​

T2-009必学进阶约 90 分钟JDK 21测试专项 B:接口·UI·性能 · UI 自动化

1. 本节定位 ​

《Selenium UI 自动化基础》解决了"跑起来";本节解决 UI 测试里最高频的一类失败:

text
NoSuchElementException: Unable to locate element    ← 十有八九是"时机",不是"选择器"

本节的核心命题:

"找不到元素"里,真正"元素不存在"的只是少数 ✗

大多数是"元素还没出现"、"元素在 iframe 里"、"选择器定位到了别的地方" ✓✓

2. 学习目标 ​

  • 说出六种定位方式与优先级,知道为什么 data-testid 最稳 ★★★;
  • 知道"全局查找"为什么会出错,会用相对定位 ★★★;
  • 说清三种等待的差别(隐式 / 显式 / Thread.sleep)★★★;
  • 知道"元素存在"与"条件成立"是两件事 ★★★(本轮踩过);
  • 会用显式等待的常用条件 ★★;
  • 知道等待的反模式与后果 ★★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看 UI 测试里最常见的四个"定位失败"及其真实原因 ✗:

报错真实原因占比
NoSuchElementException元素还没渲染(异步加载)高 ★★★
NoSuchElementException定位器指向了别的地方(全局选择器)中 ★★
ElementClickInterceptedException元素存在但被遮挡/还在动画中中 ★★
StaleElementReferenceException元素被重新渲染了(DOM 换了新节点)低 ★

★ 只有极少数是"元素真的不存在" ✓

所以本节的策略是:

先分清"不存在"与"还没出现",再谈怎么定位 ✓✓

5. 核心概念 ​

5.1 六种定位方式与优先级 ★★★ ​

优先级方式写法评价
①data-testid ★★★By.cssSelector("[data-testid='create-submit']")最稳:为测试保留的契约 ✓✓
②id ★★By.id("order-table")好,但可能为了布局被改 ⚠️
③cssSelector ★★By.cssSelector("p.summary")灵活、快 ✓
④xpath ★By.xpath("//td[@class='cell-product']")能表达"文本等于",但慢且脆 ⚠️
⑤nameBy.name("product")表单字段好用 ✓
⑥linkText / tagNameBy.linkText("登录")容易命中多个,慎用 ✗

★ 为什么这个优先级是这样排的:

text
CSS 类名 → 会被"改样式"顺手改掉        ✗
id      → 会被"改布局"顺手改掉         ⚠️
data-*  → 只会在"页面真的变了"时改      ✓✓

本项目页面的写法(UiPageController):

html
<!-- 同时提供 id 与 data-testid,便于对比两种定位 -->
<button type="submit" id="create-btn" data-testid="create-submit">创建订单</button>

★ 本项目的实测(UiEnvironmentCheckTest 与 OrderLocatorTest 都在用):

java
WebElement submit = driver.findElement(By.cssSelector("[data-testid='create-submit']"));
assertEquals("创建订单", submit.getText());

5.2 相对定位:为什么"全局查找"会出错 ★★★ ​

先看一个真实会出 bug 的写法 ✗:

java
// 页面里每行都有"删除"按钮
driver.findElement(By.cssSelector(".btn-delete")).click();   // ✗ 点的是"第一个"删除按钮

问题:findElement 返回的是"第一个匹配" ✓ ——而你要删的可能是第 3 行 ✗✗

正确写法:先定位行,再在行内定位按钮 ✓✓:

java
// ① 用稳定的属性定位到"那一行"
WebElement row = driver.findElement(By.cssSelector("tr[data-order-no='ORD-2']"));
// ② 在行内查找 —— 范围被限制住了
WebElement product = row.findElement(By.cssSelector(".cell-product"));
String name = product.getText();
row.findElement(By.cssSelector(".btn-delete")).click();       // ✓ 删的是这一行

★ 这条规则的表述:

driver.findElement 是"全局查找",element.findElement 是"在范围内查找" ✓

凡是"列表里的第 N 项",都必须用相对定位 ✓✓

本项目页面的设计(UiPageController)就是为了演示这一点 ——** 每个 <tr> 都带 data-order-no,每个删除按钮都在自己的行里**:

html
<tr class="order-row" data-order-no="ORD-2">
  <td class="cell-no">ORD-2</td>
  <td class="cell-product">人体工学椅</td>
  ...
  <td><button class="btn-delete" data-order-no="ORD-2">删除</button></td>
</tr>

★ 本节的实测(OrderLocatorTest.row_relative_locator_should_work,已通过 ✓):

text
===== 相对定位 =====
  表格行数:2
  要删除的行商品:人体工学椅
  删除后行数:1(原 2)

注意它是"删了指定的那一行"(人体工学椅 / ORD-2) ✓ ——而不是"删了第一行" ✓✓

5.3 三种等待:隐式、显式、固定睡眠 ★★★ ​

隐式等待显式等待 ★★Thread.sleep ✗
写法driver.manage().timeouts().implicitlyWait(...)new WebDriverWait(driver, ...).until(...)Thread.sleep(2000)
作用范围全局(所有 findElement)单次调用 ✓当前线程
等什么只等"元素出现在 DOM 里" ✗等任意条件 ✓✓什么都不等,纯睡 ✗
找到后立即返回 ✓条件满足立即返回 ✓✓仍要睡满 ✗✗
评价可作为兜底 ✓主力手段 ✓✓反模式 ✗✗

★ 关键差别:隐式等待"只等元素存在",不等条件成立 ✓✓

本节的实测(UiStabilityTest.proper_wait_beats_retry,已通过 ✓):

text
===== 正确等待 vs 重试 =====
  等 300ms(太短):超时,耗时 300+ ms
  等 10s(条件驱动):通过,耗时约 2000 ms —— 条件一满足就返回,不是死等 10 秒
  结论:把"等多久"换成"等什么条件",抖动就消失了,根本用不到重试

★ 这张数据里最有价值的是第二行:

超时设 10 秒,实际只等了约 2 秒 ✓ ——因为页面里的 JS 是 2000ms 后写入内容 ✓

这就是"条件驱动"与"时间驱动"的本质区别 ✓✓

如果换成 Thread.sleep ✗:

java
Thread.sleep(2000);            // ✗ 机器快时白等 2 秒;机器慢时还不够
driver.findElement(By.id("slow-panel")).getText();

两个问题:

  • 慢环境要等 3 秒 → 失败 ✗
  • 快环境等 2 秒就够 → 白白浪费 ✗

★ 所以"显式等待"同时解决了"稳定性"与"速度"两个问题 ✓✓

5.4 "元素存在" ≠ "条件成立" ★★★ ​

这是本轮真实踩到的坑 ——而且它以"编译错误"的形式出现 ✓

页面上的设计(UiPageController):

html
<div id="slow-panel" class="slow">加载中……</div>
<script>
  setTimeout(function () {
    document.getElementById('slow-panel').textContent = '最近一次同步:刚刚';
    document.getElementById('slow-panel').setAttribute('data-loaded', 'true');   // ★ 2 秒后才加
  }, 2000);
</script>

关键:#slow-panel 这个元素"一开始就存在" ✓ ——存在时它是"加载中……" ✓ 而"加载完成"体现为一个属性 data-loaded="true" 的出现 ✓✓

所以:

java
// ✗ 这样写会立刻返回(元素本来就存在),然后读到"加载中……"
WebElement panel = driver.findElement(By.id("slow-panel"));

// ✗ 隐式等待也救不了:它只等"元素存在"
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
driver.findElement(By.id("slow-panel"));      // 立刻返回 ✗

// ✓ 显式等待"条件成立"
new WebDriverWait(driver, Duration.ofSeconds(10))
        .until(ExpectedConditions.attributeToBe(By.id("slow-panel"), "data-loaded", "true"));

★★ 一个真实踩过的坑:until() 的返回类型由"条件"决定 ✓✓

java
// ✗ 编译错误:attributeToBe 返回 Boolean,不是 WebElement
WebElement panel = new WebDriverWait(driver, Duration.ofSeconds(10))
        .until(ExpectedConditions.attributeToBe(By.id("slow-panel"), "data-loaded", "true"));
// error: 不兼容的类型 / 找不到符号 getText

正确写法:

java
// ① 先等条件
new WebDriverWait(driver, Duration.ofSeconds(10))
        .until(ExpectedConditions.attributeToBe(By.id("slow-panel"), "data-loaded", "true"));
// ② 再取元素
WebElement panel = driver.findElement(By.id("slow-panel"));
String text = panel.getText();      // ✓ "最近一次同步:刚刚"

★ 这条值得单独记住:

返回 ExpectedCondition<Boolean> 的条件(如 attributeToBe、invisibilityOf、textToBe) 返回的就是布尔值 ✓ ——想要元素得再查一次 ✓✓

5.5 常用显式等待条件 ★★ ​

条件等什么返回
visibilityOfElementLocated ★★元素可见(存在且有尺寸)WebElement ✓
presenceOfElementLocated元素出现在 DOM(可能不可见)WebElement ✓
elementToBeClickable ★可见且可点击WebElement ✓
textToBe ★文本等于某值Boolean ⚠️
attributeToBe ★属性等于某值Boolean ⚠️
invisibilityOfElementLocated ★★元素消失(删除/关闭后)Boolean ⚠️
numberOfElementsToBeMoreThan ★元素个数 > N(列表新增)List<WebElement> ✓
urlContains / titleContains地址/标题变化Boolean ⚠️

★ 选条件的两个原则:

原则例子
等"最终状态",不等"中间状态" ★★等"列表已渲染"而不是"正在加载" ✗
等"最贴近业务的那个事实" ★★★等"我创建的那个商品出现在列表里" ✓✓(比"等 3 条行"更准)

本项目 OrderPage.createOrder 就是这么写的 ✓:

java
driver.findElement(SUBMIT).click();
// 等"这一行真的出现"再返回 —— 而不是等"行数变了"或"睡 2 秒"
wait.until(ExpectedConditions.presenceOfElementLocated(
        By.xpath("//td[@class='cell-product' and text()='" + product + "']")));

★ 这条写法是本项目的一个"修复成果" ✓ ——最初的版本只是 click() 就返回, 导致调用方读到旧列表,报 Index -1 out of bounds ✗(T2-010 有完整复盘)✓

5.6 等待的反模式 ★★★ ​

反模式为什么坏正确做法
Thread.sleep(固定值) ✗✗慢环境失败、快环境浪费显式等待条件 ✓
隐式等待设得很大(如 30 秒)✗每个 findElement 失败都要等 30 秒 ——用例一多,时间爆炸 ✗✗隐式等 0~3 秒,靠显式等 ✓
隐式 + 显式混用 ⚠️两者的总等待时间可能叠加(行为难以预测)✗只用一个(推荐显式) ✓✓
等待失败就无条件重试 ✗✗掩盖真缺陷(T2-011)只在"已知外部抖动"处重试 ✓
用"元素个数为 N"代替"元素是哪个" ⚠️数量对但内容错也能通过 ✗按内容定位 ✓

★ "隐式等待设得很大"这条的代价,用本项目的量级感受一下:

text
22 个 UI 用例,如果每个里有 3 次 findElement 失败(正常情况也可能发生)
隐式等待 30 秒 → 22 × 3 × 30 = 1980 秒 = 33 分钟   ✗✗
隐式等待 0(靠显式等)→ 失败立即抛出,几秒内定位到问题  ✓

6. 递进示例 ​

6.1 最小示例:等一个元素可点击 ​

java
WebElement button = new WebDriverWait(driver, Duration.ofSeconds(10))
        .until(ExpectedConditions.elementToBeClickable(By.cssSelector("[data-testid='create-submit']")));
button.click();

6.2 业务示例:列表操作(相对定位 + 条件等待) ​

见 5.2 与 5.5 ✓

6.3 反例:Thread.sleep + 全局定位 ​

java
Thread.sleep(2000);
driver.findElement(By.cssSelector(".btn-delete")).click();    // ✗ 两个坑叠加

7. 完整可运行代码与实测 ​

测试代码:

  • labs/ui-testing/src/test/java/cn/kb/learn/ui/OrderLocatorTest.java(定位与等待)
  • labs/ui-testing/src/test/java/cn/kb/learn/ui/UiStabilityTest.java(等待的边界)

运行:

powershell
pwsh -File scripts\ui-test.ps1

实测结果(本项目真实运行):

text
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 80.25 s
  --- in cn.kb.learn.ui.OrderLocatorTest          ← 定位相关 5 个用例全过

用例构成:

用例验证什么
five_locators_should_find_same_element五种定位方式都能找到,并各自计时
data_testid_should_be_preferreddata-testid 的稳定性
implicit_wait_should_fail_on_delayed_element隐式等待的边界:它等不到"条件成立" ✗
explicit_wait_should_succeed_on_delayed_element显式等待能等到"条件成立" ✓
row_relative_locator_should_work相对定位:删的是指定那一行 ✓

★ 其中第 3、4 个用例是最有教学价值的:

text
implicit_wait_should_fail_on_delayed_element  → 5 个用例中的 1 个
explicit_wait_should_succeed_on_delayed_element

它们用同一个元素、同一个条件,只换等待方式 ✓ ——一个失败、一个成功 ✓✓

★ 五种定位方式的耗时实测(各执行 200 次,同一台机器):

text
===== 五种定位方式的耗时(各执行 200 次)=====
  id                            → 单次 11585.5 μs(约 11.6 ms),文本=订单管理
  cssSelector(#id)              → 单次  7987.2 μs(约  8.0 ms),文本=订单管理
  tagName(h1)                   → 单次 10478.2 μs(约 10.5 ms),文本=订单管理
  cssSelector(标签+类)           → 单次  9489.8 μs(约  9.5 ms),文本=当前共 2 条订单
  xpath(//h1[@id='page-title']) → 单次  6741.7 μs(约  6.7 ms),文本=订单管理

★★ 这张表的结果"不符合教科书" ✓✓:

常见说法实测
"CSS 比 XPath 快"XPath 反而最快(6.7 ms) ✗
"id 定位最快"id 反而最慢(11.6 ms) ✗
"定位方式的选择影响性能"五种都在 6~12 ms 之间,差距不到 2 倍 ⚠️

★ 为什么 ✓✓✓:

单次 6~12 ms 这个量级,说明时间几乎全花在"一次 W3C 协议往返"上 ✓

而"选择器怎么解析"只是浏览器内部极小的一部分 ✓

所以"CSS 比 XPath 快"那个说法(针对浏览器内部解析)在 Selenium 场景下几乎不起作用 ✓✓

★ 这条实测的教学价值:

不要背"哪个更快",要测量 ✓✓

因为瓶颈在哪取决于你测量的整条链路,而不只是链路里你关注的那一段 ✓ (《进程与端口排查》 的"CPU 低却假死"是同一种思路)

★ 所以选定位方式应该按"可维护性"排序,而不是"性能" ✓:

text
可维护性:data-testid  >  id  >  cssSelector  >  xpath  >  name  >  linkText
性能    :五种方式差距不到 2 倍,且都被协议往返淹没 —— 不作为选择依据 ✗

★ 本项目还有一个"失败留下的证据"可以看:

UiStabilityTest.failure_should_leave_artifacts 会把截图与页面源码写到:

text
labs/ui-testing/target/ui-artifacts/failure-<时间戳>.png
labs/ui-testing/target/ui-artifacts/failure-<时间戳>.html
labs/ui-testing/target/ui-artifacts/failure-<时间戳>.txt

三件套的作用:截图看"长什么样"、HTML 看"DOM 是什么样"、txt 记"当时的 URL 与标题" ✓✓

8. Debug 观察点 ​

  1. 把 --headless=new 去掉:亲眼看着"明明元素在屏幕上,脚本却说找不到" ★★★ —— 多数时候你能看到它正在加载 ✓;
  2. 把 data-loaded 的条件改成"元素存在":观察它立刻返回并读到"加载中……" ★★;
  3. 在页面里加 Thread.sleep 式的等待(把条件等待换成固定 sleep):观察在慢机器上失败 ⚠️;
  4. 打印 driver.getPageSource():确认元素到底在不在 DOM 里 ★;
  5. 故意用全局选择器点第 2 行的删除按钮:观察它删掉的是第 1 行 ★★★(5.2)。

9. 常见错误 ​

#错误说明
1用 Thread.sleep 代替显式等待 ✗✗慢环境失败、快环境浪费(5.6)✗✗
2以为"元素存在"就是"准备好了" ✗✗见 5.4 ✓
3全局 findElement 操作列表项 ✗✗删错行(5.2)✗✗
4把 Boolean 条件当 WebElement 用 ✗✗编译错误(5.4)✗
5隐式等待设得过大 ✗✗失败时长时间空等(5.6)✗
6隐式与显式混用 ⚠️等待时间叠加,行为难预测 ✗
7XPath 里用绝对路径(/html/body/div[3]/...)✗✗DOM 一改就全废 ✗
8用 CSS 类名定位(div.mt-4.text-red-500)✗样式类会变 ✗

10. 动手实验 ​

目标:亲手制造一次"找不到元素",再分别用三种等待看结果。

步骤:

  1. 跑一次 scripts\ui-test.ps1,确认 OrderLocatorTest 的 5 个用例都通过 ✓;
  2. 把 explicit_wait_should_succeed_on_delayed_element 的等待改成 Thread.sleep(500) → 观察失败 ★;
  3. 改回显式等待 → 观察通过,并留意耗时(约 2 秒,不是 10 秒)✓;
  4. 把 row_relative_locator_should_work 改成全局 findElement(By.cssSelector(".btn-delete")) → 观察它删掉的不是预期那一行 ★★★;
  5. 打开 target/ui-artifacts,看 failure_should_leave_artifacts 留下的截图与 HTML ✓。

验收方式:能说出"第 4 步删掉的是哪一行、为什么"。

扩展任务:把页面的"慢面板"延迟从 2000ms 改成 5000ms:

  • 观察显式等待(10 秒超时)仍然通过,且耗时变成约 5 秒 ✓;
  • 说明:这就是"条件驱动"的优势 —— 超时值是上限,不是固定等待时间 ✓✓

11. 练习题 ​

  1. 判断:隐式等待可以替代显式等待。(对 / 错)
  2. 填空:等"元素出现且可见"用 ______;等"文本等于某值"用 ______,后者返回 ______ 类型。
  3. 简答:为什么 driver.findElement(By.cssSelector(".btn-delete")) 在列表页里容易出 bug?
  4. (代码阅读)
java
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30));
driver.findElement(By.id("does-not-exist"));

这段代码最坏情况下要等多久?为什么这是个问题? 5. (编程)给 OrderPage 加一个方法 waitForRowCount(int n),用"个数条件"等待,并说明它和 waitForSlowPanel 的差别。

参考答案(要点)
  1. 错。隐式等待只等"元素存在" ✗ ——等不到"条件成立" ✓(5.3、5.4)
  2. visibilityOfElementLocated;textToBe;Boolean ✓(5.5)
  3. 因为它是全局查找,返回"第一个匹配" ✗ ——列表里每行都有删除按钮,会点错行 ✓(5.2)
  4. 最坏等 30 秒 ✗ ——因为隐式等待是全局的,每个失败的 findElement 都要耗尽超时 ✓ —— 用例一多,失败时的总耗时会失控 ✓(5.6)
  5. 要点:用 numberOfElementsToBe 或 numberOfElementsToBeMoreThan ✓ 差别:个数条件只保证"有几行",不保证"是哪几行" ⚠️ —— 所以本项目 createOrder 特意用"按商品名文本定位"这种更贴近业务的等待 ✓✓

12. 掌握标准 ​

  • [ ] 能说出六种定位方式的优先级,并知道 data-testid 最稳 ★★★;
  • [ ] 会用相对定位操作列表项 ★★★;
  • [ ] 能说清三种等待的差别,并知道隐式等待的边界 ★★★;
  • [ ] 知道"元素存在" ≠ "条件成立" ★★★;
  • [ ] 知道 until() 的返回类型由条件决定 ★★;
  • [ ] 会用常用条件,并知道哪些返回 Boolean ✓;
  • [ ] 知道"等最贴近业务的事实"(等"我创建的行"而不是"行数变了")★★。

13. 扩展资源 ​

14. 下一步学习 ​

《Page Object 与框架封装》:把定位器收进一个地方,让测试长得像业务。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引