十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

UI自动化测试:CSS定位从入门到实战

UI自动化测试:CSS定位从入门到实战 做UI自动化这几年我发现自己写用例时的第一反应已经从“用XPath找元素”慢慢变成了“先试试CSS能不能解决”。原因说出来有点直白CSS定位在大部分场景下比XPath更短、更稳、更容易排查而且很多看起来复杂的结构一条CSS选择器就能干净利落地收掉。这篇就把CSS定位从基础语法到组合用法再到实战坑点一次讲透。先交代清楚这文章能干什么如果你在用Selenium、Playwright这类工具做UI自动化每天都要和元素定位打交道如果你觉得XPath写起来又长又容易踩雷如果你想搞明白CSS选择器那条长长的字符串到底是怎么拼出来的并且希望“照着抄就能用”的示例——那这篇文章正好能接住你。新人可以当入门教程翻有点经验的老手也可以重点看后面的调试思路和选型对比。1. 为什么CSS定位能在UI自动化里顶半边天1.1 语法更接近前端原生语言排查问题不费劲UI自动化的本质是用代码模拟人去操作页面而页面上的按钮、输入框、列表项在浏览器里都是DOM节点。CSS选择器本来就是前端开发用来给DOM节点“化妆”的语法你拿它来做定位相当于直接说浏览器最熟悉的语言。我见过不少测试同学定位元素全靠录制脚本或复制XPath。录制脚本最大的问题是一旦前端稍作改动脚本里那串长长的XPath就废了。而CSS选择器通常很短短到你可以一眼看出它找的是哪个元素。出了问题打开浏览器的Elements面板直接按CtrlF输入选择器能匹配到几个元素、是不是高亮在你想找的位置一目了然。这种直观性XPath很难给到。1.2 执行速度和稳定性在多数场景下占优CSS定位在底层走的是浏览器的querySelector/querySelectorAll原生接口这是浏览器本身做元素查询用的机制引擎经过了大量优化。XPath则需要额外的解析和遍历逻辑虽然现代浏览器里两者的差距已经很小但在大型页面、大量元素、频繁轮询等待的场景下CSS的响应通常更稳。稳定性方面的优势更值得关注XPath里经常出现//*[idfoo]/div[3]/div[1]/span这种依赖层级顺序的绝对路径前端稍微多套一个div就全废了。CSS定位很少会写成这种“一路数孩子”的写法更多是用属性、类名、状态来锁定目标对页面结构调整的容忍度高很多。1.3 和前端代码同源沟通成本低如果你和前端开发协作想要定位一个元素直接问“这个按钮的class是什么”或者“这个输入框有没有id”。前端同事给你一个.btn-submit或#login-form你拿过去就能用。反过来如果你用XPath定位某个元素失败跟前端同事沟通时还得解释什么是XPath对方大概率一脸茫然。CSS选择器是同一种语言体系交流起来几乎没有障碍。2. 基础选择器语法盘点从最常用的到经常被忽略的2.1 标签、类名、ID三种基础写法最基础的三种CSS选择器是任何UI自动化测试里最常用的选择器含义示例tag按标签名选择button、input、div.class按类名选择.login-btn#id按ID选择#username在Selenium里用起来就是from selenium.webdriver.common.by import By driver.find_element(By.CSS_SELECTOR, button) # 页面第一个button driver.find_element(By.CSS_SELECTOR, .login-btn) # 类名是login-btn的元素 driver.find_element(By.CSS_SELECTOR, #username) # id是username的元素这里有个小细节find_element返回的是匹配到的第一个元素。如果页面上有多个button你的定位条件又只写了button那它会找到DOM顺序里第一个button。所以实战里很少单独用标签名定位标签名通常是配合类名、id一起用的比如input#username、button.submit-btn。2.2 组合类名和带标签限定的写法一个元素通常有多个类名比如button classbtn small primary登录/button。CSS选择器可以用.btn.small.primary注意这里.btn、.small、.primary之间没有空格表示这个元素同时具备这三个类。这是CSS选择器一个非常好用的特性XPath写起来就啰嗦得多# CSS写法 driver.find_element(By.CSS_SELECTOR, button.btn.small.primary) # 等价的XPath写法 driver.find_element(By.XPATH, //button[contains(class, btn) and contains(class, small) and contains(class, primary)])两种写法一对比CSS的简洁优势直接体现出来了。不过要提醒一句前端框架里类名经常是动态生成的比如Vue/React项目里的scoped样式会带>input typetext idphone namemobile placeholder请输入手机号你可以用input[namemobile]或者input[placeholder请输入手机号]来定位。如果你的自动化脚本里某个输入框老是没有id属性选择器就是最好的兜底方案。模糊匹配的典型场景是动态类名。比如一个弹窗按钮类名在后面加了状态后缀变成btn-primary-active而其他按钮是btn-primary只匹配前几位更稳[class^btn-primary]或包含匹配[class*btn-primary]注意用[class*btn-primary]时如果页面上还有一个btn-primary-ghost也会被匹配到。所以模糊匹配要尽量让匹配串独一无二。2.4 常见误区多类选择器的空格问题很多新手刚接触CSS选择器时会写.btn .primary。这里区分一下.btn.primary同一个元素同时有btn和primary两个类.btn .primary某个元素的祖先类名是btn后代的类名是primary中间的空格是后代选择器这两种写法定位的完全是不同的元素。写反了最常见的结果是find_element抛NoSuchElementException或者定位到了完全不相关的元素。以后看到选择器中间有空格第一反应应该是“这里有祖先和后代的关系”。3. 组合与伪类定位处理复杂页面结构的核心手段3.1 后代、子元素、兄弟元素的层级关系CSS选择器支持几种层级组合方式对应页面元素之间不同的嵌套关系写法关系示例A BA的后代中存在B不限于直接子元素#header .logoA BA的直接子元素是B.menu liA BA后面紧邻的兄弟元素是Blabel inputA ~ BA后面所有的兄弟元素中包含B.item ~ .active实际使用时后代选择器和子元素选择器是最常用的。假设页面结构是这样的div classproduct-list div classproduct-item h3 classproduct-name商品A/h3 button classadd-cart加入购物车/button /div div classproduct-item h3 classproduct-name商品B/h3 button classadd-cart加入购物车/button /div /div要定位第一个产品的名称可以用.product-list .product-item .product-name也可以收得更紧一点.product-list .product-item .product-name两者区别在于要求必须是直接父子关系如果中间多包了一层div第二个写法就匹配不到第一个写法仍然能匹配到。从稳定性角度讲日常优先用后代选择器空格因为前端调整时多包一层div的频率非常高用会让定位变得脆弱。兄弟选择器和~用得相对少但在处理表单、开关这类结构时很有用。比如一个自定义checkbox视觉上是这样的div classcheckbox-wrapper input typecheckbox idagree classcheck-input label foragree同意协议/label /div要定位label旁边的checkbox用label input就是反着找一般用不到。更多时候是表单里的label和input相邻label用户名/label input typetext nameusername这时想找“用户名”旁边的输入框就可以label input[nameusername]3.2 伪类选择器第几个、不是某个、状态类都好使伪类选择器是CSS定位里最有“算法味”的一部分专门用来处理那种没有id、没有独特class的元素。最常用的几个伪类含义示例:first-child父元素下的第一个子元素.list li:first-child:last-child父元素下的最后一个子元素.list li:last-child:nth-child(n)父元素下的第n个子元素.list li:nth-child(2):nth-of-type(n)父元素下同类型的第n个div.item:nth-of-type(2):not(selector)排除匹配某选择器的元素input:not([disabled]):checked被选中的checkbox/radioinput[typeradio]:checked:disabled不可用的元素button:disabled:enabled可用的元素input:enabled:visible可见元素部分框架支持div:visible特别注意:nth-child的计数方式它从1开始计数而且数的是父元素下所有类型的子元素不是只有同类元素。比如div classcontainer h3标题/h3 p classtext第一段/p p classtext第二段/p button确定/button /div此时.container p:nth-child(2)能选中第一个p因为它是container里的第2个子元素但.container p:nth-child(1)选不中任何东西因为第1个子元素是h3。如果你不确定元素前面还有没有其他标签更稳的做法是用:nth-of-type它只数同类型元素.container p:nth-of-type(2)选中的是“第二段”。这个坑非常典型我见过不止一次因为第一个子元素是空文本节点或者隐藏标签导致:nth-child完全不对的情况。3.3 组合示例一个真实的列表页定位思路假设页面上有一个搜索结果列表每条结果长这样div classresult-item div classtitleUI自动化测试实战/div div classmeta作者张三/div span classtag hot热门/span a classdetail-link href/detail/1001查看详情/a /div需求定位“第二条结果的详情链接”。思路是逐步缩小的.result-item:nth-of-type(2) .detail-link如果列表里的result-item不是连续排列中间穿插了广告或其他模块可以换成.result-item:nth-of-type(2) a[href^/detail]这样即使结构变化只要第二条结果里有一个指向detail的链接就能命中。再看一个复杂点的场景弹窗里有一个“确认删除”按钮但页面其他地方也有一个“确认”按钮按钮的类名都是.confirm-btn唯一能区分的是弹窗的容器.modal-content .confirm-btn这种从容器角度收窄范围的思路比一股脑写长XPath高效得多。4. 四个真实可跑的定位实战场景与示例代码4.1 场景一登录表单的常规组合定位登录框是入门的经典场景页面结构一般是form idloginForm input typetext idusername placeholder请输入用户名 input typepassword idpassword placeholder请输入密码 button typesubmit classbtn-primary登 录/button /form自动化脚本可以这样写from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver webdriver.Chrome() driver.get(https://example.com/login) wait WebDriverWait(driver, 10) # 用form范围缩小目标避免页面其他位置有同名的username元素 username wait.until(EC.presence_of_element_located( (By.CSS_SELECTOR, #loginForm input[nameusername]) )) password driver.find_element( By.CSS_SELECTOR, input[typepassword] ) login_btn driver.find_element( By.CSS_SELECTOR, #loginForm button.btn-primary ) username.send_keys(testuser) password.send_keys(pass123) login_btn.click()这里有一个细节用户名输入框用了#loginForm input[nameusername]把范围限定在表单内是因为页面上可能存在其他隐藏的username输入框限定容器能避免误定位。密码框用input[typepassword]是因为一般页面里密码框不会有第二个这个选择器足够唯一。4.2 场景二动态列表里取得指定序号的一项最常见的需求是“点第二个商品的加入购物车”或“验证第三行数据的名称”。页面结构div classproduct-grid div classproduct-card span classname无线耳机/span button classadd-btn加入购物车/button /div div classproduct-card span classname机械键盘/span button classadd-btn加入购物车/button /div div classproduct-card span classname显示器/span button classadd-btn加入购物车/button /div /div定位第二个商品的名字second_name driver.find_element( By.CSS_SELECTOR, .product-card:nth-of-type(2) .name ) print(second_name.text)为什么用:nth-of-type而不用:nth-child因为product-card之间可能夹着空文本节点、注释节点或者被广告模块插入的元素:nth-child可能因这些问题整体错位。:nth-of-type只统计同类标签这里的product-card都是div稳定性更好。如果列表是动态加载的还需要先等待列表项出现wait.until(EC.presence_of_element_located( (By.CSS_SELECTOR, .product-grid .product-card) ))4.3 场景三同一页面多个相似按钮靠文本/属性区分有些页面上多个按钮长得一样比如确认弹窗里两个按钮分别是“确认”和“取消”类名相同div classdialog button classbtn取消/button button classbtn确认/button /divCSS选择器没有直接按文本匹配的标准语法:contains()并不是CSS标准伪类很多自动化框架里不可用。这种场景下最干净的做法是给按钮加一个可辨识属性但前端未必愿意配合。退而求其次可以用层级关系.dialog .btn:last-child如果“确认”和“取消”的HTML顺序不稳定那就别硬用CSS了直接XPath按文本来定位更稳driver.find_element(By.XPATH, //div[contains(class, dialog)]//button[contains(text(), 确认)])这里是CSS和XPath要各取所长的典型案例。我在实际项目中给团队定的原则是CSS优先用于属性/层级/序号定位按文本定位时别犹豫直接用XPath。4.4 场景四动态class下的模糊匹配现代前端框架经常生成带哈希后缀的类名比如button classsubmit-btn_8f3k2提交订单/button每次构建哈希可能变但你只需要稳定匹配前缀submit-btnsubmit_btn driver.find_element( By.CSS_SELECTOR, [class^submit-btn] )注意^匹配的是属性值开头的字符串不是每个类名的开头。如果class里还有其他类名比如classwrapper header submit-btn_8f3k2[class^submit-btn]就匹配不到了因为class属性的值以wrapper开头。这时要用包含匹配submit_btn driver.find_element( By.CSS_SELECTOR, [class*submit-btn] )这是最保险的写法只要class属性里任意位置包含submit-btn就能命中。4.5 示例代码的整体搭配建议实战中不要只写find_element一把梭。我通常这样组织from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC LOCATOR_SUBMIT (By.CSS_SELECTOR, [class*submit-btn]) # 显式等待元素可点击后再操作 submit_btn WebDriverWait(driver, 10).until( EC.element_to_be_clickable(LOCATOR_SUBMIT) ) submit_btn.click()先定位再等待再操作。三步分开出问题的时候能快速定位是哪一个环节挂了。5. 定位失败时你怎么排查调试流程与常见坑5.1 用浏览器控制台验证选择器写CSS选择器时我基本不会直接丢进脚本里跑一遍。先在浏览器里验证更快打开页面按F12打开开发者工具切到Console面板输入$$(你的选择器)$$是Chrome控制台的内置方法等价于document.querySelectorAll。它会返回所有匹配到元素的列表。你可以立刻看到匹配了几个元素展开列表看元素内容是不是你想要的。如果$$返回的是空数组说明选择器本身没匹配到任何元素。这时分两步排查先去掉最外层容器一层层往里缩看哪层开始匹配不到定位问题出在哪段选择器上。比如$$(#loginForm input[nameusername])返回空可以先$$(#loginForm)再$$(#loginForm input)再$$(#loginForm input[nameusername])。哪步为空问题就在哪。5.2 常见坑一元素在iframe或shadow DOM里CSS选择器无法跨iframe定位。如果你的定位一直报元素找不到检查一下目标元素是不是在iframe里。判断方法在Elements面板里搜索元素看它的祖先节点里有没有iframe。如果在iframe里必须先切换进去driver.switch_to.frame(frame-id) # 切换后选择器就能命中了 driver.find_element(By.CSS_SELECTOR, .iframe-content).click() # 操作完记得切回默认内容 driver.switch_to.default_content()shadow DOM也是类似的问题CSS常规写法无法穿透shadow边界需要利用shadow root节点逐层进入。这个稍复杂遇到可以单独研究。5.3 常见坑二匹配到多个元素但只想要一个find_element在匹配到多个元素时不会报错它只返回第一个。但这往往是隐患你以为定位到了目标其实定位到了页面上一模一样的第一个隐藏模块。直到这个隐藏模块因为某种原因消失脚本才暴露问题。验证方法还是在控制台用$$看数量。如果返回多个找一个能明显区分目标的父容器来收窄范围或者用:nth-of-type指定序号。5.4 常见坑三属性值是动态的精确匹配失效有些元素每次刷新后属性会变。比如有个>wait WebDriverWait(driver, 10) element wait.until( EC.presence_of_element_located( (By.CSS_SELECTOR, .dynamic-content) ) )显式等待只等到目标元素出现就继续比固定sleep更可靠也不会白白多等好几秒。6. CSS定位与XPath、参数化定位的选型参考6.1 CSS vs XPath什么时候怎么选做自动化久了会明白一个道理没有“某个定位方式天下第一”这回事只有“在某个场景下谁的效率最高”。对比项CSSXPath语法简洁度更简洁相对冗长按文本内容定位标准语法不支持支持contains(text(), xx)向上查找祖先元素不支持支持ancestor::/..按属性模糊匹配简洁可以用但啰嗦序号定位:nth-of-type方便可以调试直观度浏览器控制台直接验证需要在脚本里跑或借助工具执行性能略快现代引擎下差距微小我的实践倾向是有稳定id、class、name、type等属性的优先CSS依赖文本内容定位的用XPath需要从某个元素向上找祖先的用XPath大部分列表、表单、按钮、链接定位CSS足够6.2 把定位条件抽出来统一管理项目稍微大一点建议不要在每个用例里到处写选择器字符串。更好的做法是把定位条件集中管理比如放在一个locators.py里# locators.py LOGIN_FORM_USERNAME (By.CSS_SELECTOR, #loginForm input[nameusername]) LOGIN_FORM_PASSWORD (By.CSS_SELECTOR, input[typepassword]) LOGIN_BUTTON (By.CSS_SELECTOR, #loginForm button.btn-primary) PRODUCT_CARD_SECOND_NAME (By.CSS_SELECTOR, .product-card:nth-of-type(2) .name)这样如果前端改了某个class你只需要在一个文件里改。而且配合WebDriverWait时传LOCATOR和使用By.CSS_SELECTOR的写法是统一的比散落在各处的字符串好维护得多。6.3 CSS相对于XPath一个“打不过就跑”的补充最后说一个个人经验CSS定位虽然好用但别死磕。我一个同事曾经为了不用XPath在一个动态表格里用十几个选择器组合去定位某行某列的数据最后勉强拼出来一看又长又脆改一行前端就废。这个场景里XPath的//tr[contains(., 指定文案)]反而一行搞定。所以我的习惯是三分之二的场景用CSS三分之一的场景交给XPath。RPA、爬虫、UI自动化想清楚这个边界平时维护成本真的能降一大截。最后分享一个小技巧每隔一段时间把项目里比较长的CSS选择器拿出来过一遍看能不能用容器子元素的方式简化。很多同事写完定位就再也不管了等到前端重构才回头改。定期清理定位条件会让整个自动化测试的稳定性明显上一个档次。
返回列表