❄️ 我的个人专栏:
《智能软件工程AI4SE》
《嵌入式面试总结》
《嵌入式处理器架构解析》
《嵌入式与虚拟化》
《嵌入式软件测试》
🌟 Simplicity is the ultimate sophistication
摘要:本文聚焦嵌入式软件动态测试中的 UI 自动化稳定性问题,深入剖析 Flaky Test(不稳定测试)的常见诱因,并系统讲解 Playwright 的自动等待机制与稳健选择器策略。通过一个嵌入式设备管理界面的实战案例,演示如何用自动等待替代固定 sleep、用基于角色、标签和测试属性的定位替代脆弱 CSS 选择器,最终将 Flaky 失败率从约 25% 降至约 5%,执行耗时缩短约 60%。
1. 引言
在嵌入式软件动态测试中,UI 自动化测试的稳定性往往比功能实现本身更让人头疼。明明代码没有改动,测试却时好时坏,这种被业界称为 Flaky Test(不稳定测试)的问题,会严重消耗团队的排查精力,也会让自动化测试的可信度大打折扣。本文聚焦 Playwright 在嵌入式 UI 自动化中的应用,重点讲解如何通过自动等待机制和选择器策略,把 Flaky Test 的发生率降低 80%。
2. 为什么 UI 自动化测试会不稳定
要解决 Flaky Test,先要理解它产生的根源。嵌入式设备的 UI 自动化测试与普通 Web 测试不同,设备性能差异、网络延迟、界面渲染时序、资源加载速度等因素,都会让测试脚本在运行时出现不确定性。
2.1 常见的 Flaky 诱因
- 时序竞争:页面元素尚未渲染完成,脚本就开始点击或输入,导致操作落空。
- 固定等待滥用:使用 sleep 固定等待,要么等待时间不足导致失败,要么等待时间过长拖慢整体执行。
- 选择器脆弱:依赖易变的 CSS 类名、动态 ID 或深层嵌套结构,界面微调后选择器失效。
- 资源加载延迟:嵌入式设备性能有限,图片、脚本、样式表加载速度不稳定。
2.2 传统等待方式的弊端
很多团队习惯用固定 sleep 来规避时序问题,例如在点击前强制等待 2 秒。这种做法看似简单,实则隐患很大:等待时间不足时测试依然会失败,等待时间过长时又会让整个测试套件变得缓慢。更关键的是,固定等待无法感知页面真实状态,属于典型的“盲等”。
3. Playwright 自动等待机制
Playwright 的核心设计理念之一,就是内置自动等待(Auto-waiting)。它会在执行操作前,自动等待目标元素达到可操作状态,从而大幅减少因时序问题导致的失败。
3.1 自动等待的核心原则
Playwright 在执行点击、输入、勾选等操作前,会自动等待元素满足以下条件:元素已附加到 DOM、元素可见、元素稳定(不持续变化)、元素可接收事件、元素未被遮挡。只有这些条件全部满足,操作才会真正执行。
3.2 可操作性检查示例
// Playwright 会自动等待按钮可点击后再执行点击
await page.getByRole('button', { name: '启动设备' }).click();
// 输入框会自动等待可输入状态
await page.getByLabel('设备名称').fill('TestDevice-01');
上面的代码没有写任何 sleep,Playwright 会在内部完成等待与重试,直到元素可操作或超时。这种机制从根源上消除了“盲等”带来的不确定性。
3.3 显式等待与断言轮询
除了操作前的自动等待,Playwright 还提供了断言轮询机制。使用 expect 进行断言时,Playwright 会持续轮询直到条件满足或超时,而不是只检查一次。
// 等待设备状态文本变为“运行中”,最长等待 10 秒
await expect(page.getByText('运行中')).toBeVisible({ timeout: 10000 });
这种轮询式断言非常适合嵌入式场景,因为设备状态切换往往需要一定时间,轮询可以平滑地覆盖这段延迟。
4. 选择器策略:让定位更稳健
选择器是 UI 自动化的另一大痛点。选择器写得脆弱,界面稍有改动测试就会挂掉。Playwright 提供了多种定位方式,合理组合可以显著提升稳定性。
4.1 优先使用用户可见文本定位
相比 CSS 类名和 ID,用户可见文本更稳定,也更贴近真实用户的操作方式。Playwright 的 getByText、getByRole、getByLabel 等 API 都基于可访问性语义,对界面结构调整的容忍度更高。
// 不推荐:依赖易变的 CSS 类名
await page.locator('.btn-start-device').click();
// 推荐:基于角色和可见文本定位
await page.getByRole('button', { name: '启动设备' }).click();
4.2 避免过度依赖深层嵌套结构
选择器层级越深,越容易因中间节点变化而失效。建议尽量使用扁平化的定位方式,或者结合 data-testid 这类专为测试设计的属性。
// 在页面元素上添加 data-testid 属性
// <button data-testid="device-start-btn">启动设备</button>
// 测试中直接定位,稳定且语义清晰
await page.getByTestId('device-start-btn').click();
4.3 选择器优先级建议
| 高 | getByRole / getByLabel | 高 | 按钮、输入框、表单控件 |
| 高 | getByTestId | 高 | 关键交互元素,测试专用属性 |
| 中 | getByText | 中 | 文本展示、状态提示 |
| 低 | CSS 类名 / ID | 低 | 仅作为兜底,避免单独使用 |
5. 实战:把 Flaky Test 砍掉 80%
下面通过一个嵌入式设备管理界面的测试案例,演示如何综合运用自动等待和选择器策略,把不稳定测试降到最低。
5.1 场景描述
被测对象是一台嵌入式设备的 Web 管理界面。测试流程为:登录系统、进入设备列表、启动指定设备、等待设备状态变为“运行中”、校验状态展示。该场景在旧脚本中频繁出现点击落空和状态断言失败。
5.2 优化前的脆弱脚本
// 旧脚本:大量固定等待 + 脆弱选择器
await page.goto('http://device-admin.local');
await page.waitForTimeout(3000); // 盲等登录页加载
await page.fill('#login-username', 'admin');
await page.fill('#login-password', '123456');
await page.click('.login-btn');
await page.waitForTimeout(5000); // 盲等列表加载
await page.click('.device-item .start-btn');
await page.waitForTimeout(8000); // 盲等设备启动
const status = await page.textContent('.device-status');
if (status !== '运行中') {
throw new Error('设备状态异常');
}
这段脚本的问题非常明显:三处固定等待累计 16 秒,且选择器全部依赖易变的 CSS 类名。一旦界面样式调整或设备启动变慢,测试就会随机失败。
5.3 优化后的稳定脚本
// 新脚本:自动等待 + 稳健选择器
await page.goto('http://device-admin.local');
// 登录:基于标签定位,自动等待可输入
await page.getByLabel('用户名').fill('admin');
await page.getByLabel('密码').fill('123456');
await page.getByRole('button', { name: '登录' }).click();
// 等待设备列表加载完成
await expect(page.getByText('设备列表')).toBeVisible();
// 启动设备:基于测试属性定位
await page.getByTestId('device-start-btn').click();
// 等待设备状态变为“运行中”,轮询最长 15 秒
await expect(page.getByTestId('device-status')).toHaveText('运行中', { timeout: 15000 });
优化后的脚本去掉了所有固定等待,改用 Playwright 的自动等待和轮询断言;选择器全部替换为基于标签、角色和测试属性的稳定定位。整体执行时间反而更短,因为不再需要为“最坏情况”预留冗余等待。
5.4 优化效果对比
| 固定等待 | 16 秒(3 处盲等) | 0 秒 |
| 选择器类型 | CSS 类名 | Role / Label / TestId |
| 单次执行耗时 | 约 20 秒 | 约 8 秒 |
| Flaky 失败率 | 约 25% | 约 5% |
从对比可以看出,优化后不仅 Flaky 失败率大幅下降,执行耗时也缩短了 60% 左右,测试套件的整体效率得到明显提升。
6. 更多稳定性建议
除了自动等待和选择器策略,以下几点也能帮助进一步降低 Flaky Test 的发生率。
6.1 合理设置超时时间
嵌入式设备性能差异较大,建议为关键操作设置合理的超时时间,而不是使用默认值。例如设备启动这类耗时操作,可以单独设置较长的超时。
// 为慢速操作单独设置超时
await expect(page.getByTestId('device-status')).toHaveText('运行中', {
timeout: 20000
});
6.2 保持测试数据独立
测试用例之间应尽量避免共享状态。每次测试前重置设备状态或使用独立测试数据,可以避免因数据残留导致的偶发失败。
6.3 关注测试环境一致性
嵌入式设备的网络环境、固件版本、外设状态都会影响测试结果。建议在 CI 中固定测试环境,或在测试前置条件中明确校验环境状态。
7. 总结
Flaky Test 是 UI 自动化测试稳定性的最大敌人,而 Playwright 的自动等待机制和稳健的选择器策略,正是对抗这一问题的两把利器。自动等待让脚本不再“盲等”,选择器策略让定位不再脆弱。结合合理的超时设置、独立测试数据和稳定的测试环境,完全可以把 Flaky Test 的发生率降低 80%,让嵌入式 UI 自动化测试真正可靠、高效。

