测试这行干久了你会发现一个规律:不管你是用Selenium、Appium,还是后来冒出来的Playwright、Cypress,再换到带AI辅助的测试工具,日常执行失败的根因翻来覆去就那么几个——元素找不到、元素等不到、脚本跑一半因为定位或时机问题直接罢工。真正经历过几个大型项目之后,我越来越确信一件事:UI自动化测试里,最值得花时间掌握的Skills就是两个,一个是元素定位,一个是等待同步。这两个Skills掌握到位了,不敢说100%,但覆盖绝大多数UI测试场景是够用的。这篇文章我就把这两项能力的底层逻辑、实操细节、组合用法和一些踩坑经验一次性说透。
1. 为什么只聊两个Skills?先看UI自动化测试最常见的失败
1.1 九成执行失败都绕不开这两个原因
我见过的UI自动化测试项目,不管是Web端还是移动端,跑完一轮用例之后点开失败报告,报错信息基本可以归成两类。
第一类是元素定位异常,比如NoSuchElementException、Unable to locate element,通俗点说就是脚本要求的那个按钮、输入框、列表项压根找不着。为什么找不着?可能是前端改了结构,可能是某个字段用了动态ID,也可能是整个组件被重构了但测试代码没跟上。
第二类是元素同步异常,比如ElementNotInteractableException、ElementClickInterceptedException、TimeoutException。这类错误最迷惑人——元素在页面源码里能找到,但点的时候还没渲染完,或者还在加载态、动画滚了一半,脚本冲上去点击就被拦截了。
行业内做过粗略统计:UI自动化测试的失败原因中,定位问题和时序问题加起来占比往往在80%以上,剩余才是环境、数据、网络这类外围因素。所以把这两个Skills练扎实,等于先把主要矛盾摁住了。
1.2 技能与工具的区别:框架会迭代,底层能力不会过时
有朋友会问,现在AI都能写测试脚本了,还有必要花时间学元素定位和等待吗?我的看法是工具会被替换,但解决问题的思路不会。
Selenium流行的时候大家学find_element_by_id,后来这套API被废弃,换成了find_element(By.ID, ...);Appium的定位方式也和WebDriver通用。等到Playwright出来,选择器变得更强大,还能自动等待,但它的底层依然要求你理解“元素应该具备什么特征才稳定”“什么情况下元素处于可交互状态”。
换句话说,元素定位和等待同步是两个范式级别的能力,它们不绑定某个具体工具。你掌握了选择器怎么写得稳定,换框架只是换写法;你理解了显式等待背后的轮询机制,换工具也只是换API调用方式。这种底层能力才是“值得掌握”的真正含义。
1.3 谁说这两个Skills能覆盖“几乎所有场景”
我起初也觉得这话有点绝对。后来复盘了自己的项目才明白,所谓覆盖,不是指你不需要其他能力,而是当你面对一个新页面、新控件、新交互链路时,第一反应能落到定位和等待这两个核心动作上。比如:
- 处理动态列表:首先要想怎么定位每一行,其次要考虑数据加载完成的等待条件。
- 处理弹窗:先判断弹窗是原生还是H5(Web),再写弹窗出现和消失的等待。
- 处理跨端测试:Android和iOS的控件属性不同,但定位思路一致,等待机制也一致。
- 处理混合应用(Hybrid App):Web视图和原生视图都能用这两套思路去处理。
换句话说,UI自动化的技术栈可能很庞杂,但核心工具箱里最常用的就是这两件。把它们用到极致,再配合页面对象模型、数据驱动、失败重试这些工程化手段,你就能稳定支撑起大量业务回归诉求。
2. 技能一:元素定位——所有UI自动化框架的地基
2.1 先搞清楚定位的本质:找到页面中足够稳定的锚点
元素定位看起来只是写一行driver.find_element(...),但本质是你要从当前页面的结构里找到“不会轻易跑掉的锚点”。锚点越稳定,脚本生命周期越长。
我见过很多新手一上来就复制XPath,长得像一个超长咒语,结果前端稍微改个样式就全崩了。核心问题就是没想清楚“锚点”该选哪个。
一张表先看清常用定位策略的适用场景:
| 定位策略 | 推荐程度 | 典型场景 | 风险点 |
|---|---|---|---|
| id | 最优先 | 控件有固定ID时 | 动态ID会被系统和前端拼接,导致不稳定 |
| name | 次优先 | Web表单、部分App控件 | 移动端控件name属性使用率低 |
| className / class | 可用 | 同类型批量元素 | class中可能包含动态样式值 |
| accessibility_id(Appium) | App定位推荐 | iOS和Android都有便捷可达性标识 | 需要开发配合设置 |
| xpath(相对路径) | 优先使用相对XPath | 无ID、结构层级复杂的控件 | 绝对路径容易受结构变动影响 |
| css selector | Web端推荐 | 有稳定class或属性组合 | Appium原生控件支持有限 |
| image(图像定位) | 兜底方案 | 控件无可用属性、游戏或画布场景 | 受分辨率、缩放影响,效率低 |
从这张表能看出来,没有哪个策略是万能的,真正的技能是你能根据当前页面特征选出最佳策略。
2.2 Web端定位:最能看出选择器功底的细节
以Web自动化为例,我最常用的两个定位方式就是CSS选择器和相对XPath。即使页面结构复杂,我也会优先考虑CSS,因为它的解析速度快、可读性好,而且在Selenium和Playwright里都通用。
举例,如果有一个登录按钮:
<button type="submit" class="btn btn-primary mt-4 login-btn"># Python版本,Selenium写法 from selenium.webdriver.common.by import By driver.find_element(By.CSS_SELECTOR, "button[data-testid='login-button']") # 或者 driver.find_element(By.XPATH, "//button[contains(@class, 'login-btn')]")这里有两个细节值得琢磨。
第一,>driver.find_element(By.XPATH, "//button[@type='submit' and contains(., '登录')]")
这种写法表达的是“我找个提交类型的button,只要文本包含‘登录’就行”,语义清晰且抗结构变化。
2.3 App端定位:原生控件与Web控件的差异化处理
Appium做移动端UI自动化,定位时也要区分原生控件和Web控件。
原生Android控件常用resource-id,iOS常用accessibility_id或name。Appium里常见的写法:
// Java版本,Appium driver.findElement(By.id("com.example.app:id/login_button")).click(); // 或 driver.findElement(AppiumBy.accessibilityId("登录")).click();iOS上很多控件是XCUITest框架里的,用accessibilityId最靠谱。Android上如果resource-id是动态生成的,就需要退到XPath,找那些不随版本变化的属性,比如content-desc或者text。
这里有一个容易被忽略的经验:App端定位时,要尽量少用包含大量数字的resource-id,因为有些App会在构建时给ID拼上资源版本号,一旦版本升级,数字部分就变了。稳定做法是拿@resource-id="com.example.app:id/login"这种不带版本后缀的写法,或者干脆用text加部分匹配。
2.4 定位不到元素时的排查链路
定位不到元素,第一反应别急着改选择器。我有一套固定排查步骤:
- 打开页面,打开开发者工具或者App的页面结构查看器,确认元素是否真的存在。
- 检查当前页面是否在正确的窗口、Frame、WebView上下文里。Web自动化经常是iframe问题,Appium经常是原生上下文和Web上下文没切换。
- 看元素是否有多个相同匹配,导致脚本定位到了不可见的那一个。
- 看元素是否被遮住或处于滚动区域外,这类问题往往不是“找不到”,而是“找到了但不可操作”。
- 最后再考虑选择器本身是否写得太严格或太模糊。
这套链路走下来,90%的定位问题都能定位到根因。很多时候根本不是选择器问题,而是上下文或等待问题。
3. 技能二:等待同步策略——动态UI下的稳定性核心
3.1 为什么隐式等待解决不了全部问题
很多初学者知道要加等待,第一反应是加time.sleep(3),或者设置一个全局的implicitly_wait(10)。这两者都有明显缺陷。
time.sleep是“傻子式等待”,不管页面加载完没有,睡满时间才继续。页面快的时候浪费时间,页面慢的时候又不够用。
implicitly_wait是Selenium和Appium提供的全局等待,它会在每次查找元素时轮询一段时间。问题是这个等待只对“查找元素”有效,解决不了“元素找到了但还不能点击”的问题,也解决不了“元素已消失但页面还在动画”的问题。
真正可靠的做法是显式等待。你可以指定一个预期条件,脚本会反复轮询直到条件满足或超时。这才是UI自动化中“时间控制”的核心姿势。
3.2 显式等待:从轮询到预期条件的完整逻辑
显式等待背后的机制不难理解:它不是让脚本傻等,而是每隔一小段时间(默认一般是0.5秒左右)去检查一次预设条件,直到条件满足。
以Selenium为例,推荐写法:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 等待登录按钮变得可点击,然后点击 wait = WebDriverWait(driver, timeout=10) login_button = wait.until(EC.element_to_be_clickable((By.ID, "login-button"))) login_button.click()这段代码的意思是:给我最多10秒的时间,让我每0.5秒看一次“login-button是否可点击”,一旦可点击就立刻继续执行。
Appium里也一样:
import org.openqa.selenium.support.ui.WebDriverWait; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.By; WebDriverWait wait = new WebDriverWait(driver, java.time.Duration.ofSeconds(15)); wait.until(ExpectedConditions.elementToBeClickable(By.id("com.example.app:id/login"))).click();为什么这样写稳定?因为等待的目标不是“时间到了”,而是“业务状态到了”。页面加载快的时候脚本不会多等一秒,页面加载慢的时候又不会过早操作。
常见的预期条件远不止elementToBeClickable,我列一下最常用的几个:
| 预期条件 | 使用场景 |
|---|---|
presence_of_element_located | 元素已出现在DOM中,不要求可见 |
visibility_of_element_located | 元素可见(非隐藏、非0尺寸) |
element_to_be_clickable | 元素可见且可点击 |
invisibility_of_element_located | 等待元素消失,比如加载遮罩消失 |
text_to_be_present_in_element | 等待文本变化 |
number_of_windows_to_be | 等待新窗口打开或关闭 |
frame_to_be_available_and_switch_to_it | 等待iframe可用并切换 |
3.3 等待对象的选择:等待“业务状态”而不是等待“元素存在”
如果说显式等待是入门,那“选择等什么”就是进阶技能。
最经典的例子是列表页加载。很多时候列表里的占位符和真实数据长得一样,只是内容不同。如果你只等presence_of_element_located,占位符出现就算通过了,可业务数据还没加载完,后续断言就会失败。
正确做法是等一个能代表“加载完成”的信号:
# 等待某个业务数据的文本出现,例如页面出现“共 128 条记录” WebDriverWait(driver, 10).until( EC.text_to_be_present_in_element((By.CLASS_NAME, "total-count"), "128") )又比如,App里常见的下拉刷新,等刷新动画消失比等数据文本出现更稳妥:
// 等待加载动画消失,再执行后续操作 wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id("loading_indicator")));理解这个思路之后,你写等待条件就不会再停留在“等一个元素出现”的水平,而是会顺着业务逻辑问:到底哪个状态代表“页面真的准备好了”?
3.4 等待不能万能,还要配合线程安全与外部状态
这个点比较抽象,但我还是要提一下。
UI自动化本质上是多个异步操作的组合。页面在加载数据,脚本在轮询条件,如果遇到极端情况,比如接口超时10秒但等待时间是8秒,就会失败。很多团队会在显式等待外面再包一层失败重试机制。
重试不是让你盲目地把整个用例跑三遍,而是针对那些已经排除了定位逻辑错误、只是因为网络或服务端抖动导致偶发失败的场景。比如:
def click_with_retry(driver, locator, retry_count=3): for attempt in range(retry_count): try: element = WebDriverWait(driver, 10).until(EC.element_to_be_clickable(locator)) element.click() return except (ElementNotInteractableException, StaleElementReferenceException): if attempt == retry_count - 1: raise driver.refresh() # 或者回到上个页面重新进入这种设计能把UI自动化的偶发失败率从30%压到5%以内,但要注意重试逻辑不能滥用,否则会掩盖真正的功能bug。
4. 组合实战:PO模式+定位+等待搭一套可复用的UI自动化脚本
4.1 为什么定位和等待一定要捆在一起设计
很多人的Page Object模式(PO模式)写不好,是因为把元素定位和等待拆成了两件割裂的事情,要么只在定位方法里写等待,要么在测试用例里到处塞等待,最后代码又烂又难维护。
我的做法是:每个页面对象里的交互方法,必须同时包含“稳定定位”和“业务等待”。这句话怎么理解?
以登录页为例,一个合格的Page Object应该是这样的:
from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class LoginPage: USERNAME_INPUT = (By.ID, "username") PASSWORD_INPUT = (By.ID, "password") LOGIN_BUTTON = (By.CSS_SELECTOR, "button[data-testid='login-button']") WELCOME_TEXT = (By.CLASS_NAME, "welcome-message") def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def login(self, username, password): # 等待输入框可交互,而不是直接send_keys username_input = self.wait.until(EC.visibility_of_element_located(self.USERNAME_INPUT)) username_input.send_keys(username) password_input = self.wait.until(EC.visibility_of_element_located(self.PASSWORD_INPUT)) password_input.send_keys(password) self.wait.until(EC.element_to_be_clickable(self.LOGIN_BUTTON)).click() # 返回一个代表登录结果的对象或状态标志 self.wait.until(EC.visibility_of_element_located(self.WELCOME_TEXT))你看,每个动作都围绕“定位策略”和“等待条件”设计。元素定位信息被浓缩成Page Object顶部的元组常量,等待逻辑被放到交互方法内部。测试用例层就非常干净,只需要关心业务步骤。
4.2 把两个Skills固化成团队公共组件
这里说的公共组件,不是指每个项目都搞一套框架,而是指把定位和等待中的高频模式沉淀成简单好用的工具方法。
比如我在多个项目里都会放一个叫ActionHelper的类:
class ActionHelper: def __init__(self, driver, timeout=10): self.driver = driver self.wait = WebDriverWait(driver, timeout) def click_by(self, locator): self.wait.until(EC.element_to_be_clickable(locator)).click() def input_by(self, locator, text): element = self.wait.until(EC.visibility_of_element_located(locator)) element.clear() element.send_keys(text) def wait_element_disappear(self, locator): self.wait.until(EC.invisibility_of_element_located(locator))底层还是一样,但团队成员写用例时不用再重复写那些绕口的预期条件表达式,出错的概率也会降下来。移动端也是同理,可以用Appium的MobileElement封装类似的辅助类。
4.3 从“脚本能跑”到“套件稳定”的工程化路线
单个用例能跑,不代表整个回归套件稳定。工程化落地时我建议按这个顺序推进:
- 梳理核心页面,输出页面元素地图,这一步是定位的基础。
- 给每个关键交互状态定义明确的等待条件,让测试脚本的“时机”有据可依。
- 引入失败重试机制,但必须记录是第几次重试通过的,方便后续优化。
- 在CI流水线里接入测试报告,把
NoSuchElementException和TimeoutException单独归类,每周复盘一次,看变化趋势。 - 建立选择器维护规范,禁止写硬编码的绝对XPath,禁止在脚本里出现裸sleep。
这套路线走下来,自动化的稳定性才会真正上一个台阶。我也见过很多团队先买一堆测试平台,再堆两个自动化脚本,最后跑起来到处飘红。根子在于没把定位和等待这两项基本功打牢。
5. 从传统工具到AI Agent:Skills生态对UI自动化的扩展
5.1 AI辅助生成脚本,但两项技能仍是校验锚点
最近AI测试辅助工具很火,不管是Claude Code、Codex还是各种AI测试平台,都能快速生成一段自动化脚本。但AI生成不等于可靠。
举个例子,你让AI写一个定位脚本,它会给你一个看起来很合理的XPath,但元素是否稳定、等待条件是否真的反映了业务状态,它不一定判断得准。尤其是涉及到移动端动态列表、时间选择器、混合手势操作这类场景,AI可能生成看起来很完善但实际跑不通的代码。
这时候,真正能看出水平的就是你本人对元素定位和等待机制的理解。你会去检查AI生成的选择器是不是稳定,会去思考这个等待条件到底等的是“出现了”还是“可交互了”。所以说,AI不是在替代这两项技能,而是把这两项技能的产出效率放大。没有基本功,AI生成的结果你连判断对错的能力都没有。
5.2 把团队测试知识沉淀成可复用的Agent Skill
热词里反复出现“Skills”“Agent Skills”“Claude Code skills”,这其实反映了AI编码工具正在从单次对话转向“可积累的技能库”。对于测试团队来说,这是个很好的机会。
你可以把团队积累的“定位偏好”和“等待规范”写成一份结构化的Skill描述文件,交给AI Agent,在生成自动化脚本时自动遵守。比如这样一份ui-test-skills.md文件可以包含:
- 页面元素优先使用
>
家庭数据备份方案实战:三层架构、工具选型与恢复演练指南
开头先交代一下:这篇不是讲什么高大上的新玩意儿,而是把过去半年我自己折腾“家庭数据备份”这件事的完整记录整理了出来。起因很简单,硬盘里十年的照片、工作文档、攒的各种配置文件和插件,差点因为一次手滑全没了。从那之后我认…
AI重拓扑插件完整工作流:从高模到低模的自动化实战指南
这次我们来看 AI 重拓扑插件的完整工作流。对做 3D 建模、游戏资产、数字人和产品渲染的开发者来说,重拓扑一直是高模转低模里最耗时的环节。手动拓扑一圈一圈地连线,遇到布线密度不够、UV 拉伸、转角折痕不对,又得重来一遍。AI 重拓扑插件要…
CUDA调试实战:用Compute Sanitizer定位显存越界与数据竞争
我接手过一个让人印象深刻的“幽灵Bug”:一个图像卷积kernel,跑小规模测试完全正常,放到生产数据上跑几分钟就随机崩溃,有时甚至算出明显错误的结果但进程不退出。项目组前面换了好几种排查思路,打印、加锁、换数据分块…
MFC汉字编码转换实战:GBK与UTF-8互转原理及CString避坑指南
简介:这是一份由MFC编写的汉字编码转换器工程源代码,面向需要理解汉字编码规则和Windows界面程序开发的读者。资源包共38个文件、3.69MB,包含完整的头文件、C源文件、资源描述文件以及已经编译好的可执行程序,还附有工程文件和调试…
V4L2与Qt联动的USB摄像头采集显示录像完整方案
简介:面向Linux/Qt开发者的v4l2摄像头采集与显示录像示例工程,解决视频设备接入、MJPEG流解析、图像格式转换以及Qt界面实时预览和录像保存等问题。资源共210个文件,压缩后3.03MB,以h、c、cpp源码为主,辅以UI界面、库文…
LLM为何不擅长Harness工程?人机分工是关键
先解释一下标题里的“harness engineering”,免得有朋友一进来就懵。做LLM应用的同学应该都听过一个词叫agent harness,也有人叫它model harness、eval harness。说白了,就是包在模型外面、让模型能真正干活的那套工程系统:工具怎…