☰
Selenium元素定位入门:pytest下六种定位策略与踩坑指南
2026/10/21 18:29:35 网站建设 项目流程

干测试这行,不管是功能测试还是自动化测试,绕不开的第一座山就是元素定位。我见过不少刚开始写Selenium脚本的同学,代码能敲出来,框架也能跑通,一到自己写用例就卡在find_element上,要么找不到,要么找错,要么定位到一个根本不该点的按钮上。Selenium的API本身不难,难的是你面对一个真实页面时,怎么快速选对定位策略,怎么把页面里那几十上百个控件精准抓出来。这篇就聊聊pytest+Selenium自动化测试里的元素定位入门,把定位策略的原理、适用场景、配合pytest的写法,以及我踩过的那些坑一次说透。

这篇文章适合两类人看:一类是刚接触Web自动化、能把环境跑通但写定位时没底气的初学者;另一类是写过一阵子脚本,想系统梳理一下定位思路、顺便解决“明明按教程写了还是报找不到”这种问题的测试工程师。核心就一句话:初级定位的策略一共就这么几种,每一种都有自己的脾气,搞清楚它们的脾气,你就成功了一大半。

1. 元素定位为什么是自动化测试的第一道坎

先说个真实的场景。你去测一个登录页面,要往用户名输入框里填账密,脚本要做的第一步就是先找到那个输入框。页面渲染出来的是一个一个的HTML元素,浏览器知道它们在哪儿,但你的脚本不知道。Selenium本身也不知道,它需要你告诉它“那个输入框是什么样子的”,这就是定位做的事。

打个比方:你去一个没有门牌号的办公楼找人,前台问你找谁,你说“找那个穿黑色卫衣的男生”。如果整栋楼就一个人穿黑卫衣,你一次就能找到;如果有五个穿黑卫衣的,你就得靠更多特征去区分——比如眼镜、身高、手里拿的文件。元素定位的本质就是这个:从整个页面的HTML结构里,通过一个或几个特征,精准地锁定你要操作的那个元素。

为什么说它是“第一道坎”?因为后续所有的操作,不管是输入、点击、读取文本还是断言,全部建立在“先找到这个元素”的基础上。定位写得烂,脚本的稳定性就烂;定位写得丑,代码维护成本翻倍涨。我见过最典型的反例:一整个项目的定位全用XPath写死,前端稍一改版,几十条用例全部飘红,一查全是定位失效。所以说,定位不是那种“能用就行”的小事,它决定了你测试代码的寿命。

再一个原因,定位策略的“选择”本身就是测试开发的基本功。你需要看的不是“哪个方法能定位到”,而是“哪个方法在这个场景里最稳妥、最快、最好维护”。初级定位之所以叫“初级”,是因为它主要指那些直观、基础、不需要复杂语法就能写出来的定位方式。但它们各有用武之地,你掌握好了,后面的XPath、CSS高级玩法都是在这个基础上延伸的。

2. 初级定位的六种武器:用法与适用场景详解

Selenium提供了一整套元素定位策略,初级阶段的重点是这六种:ID、Name、Class Name、Tag Name、Link Text、Partial Link Text。它们的共同点是门槛低,基本就是你看到HTML里有什么属性就直接能用。但千万别因为简单就小看它们,用对了效率极高,用错了就是给自己埋坑。

在展开之前,先明确一个概念:所有这些定位方法,核心都在“找一个唯一的特征”。元素可以在页面上有多个,但定位结果默认取第一个匹配。所以你的定位表达式的质量,直接决定了它是否唯一命中目标元素。

2.1 ID定位:最快但最依赖前端规范

ID定位是Selenium里优先级最高的定位方式,没有之一。HTML规范里,一个页面内元素的ID必须唯一,这从语法层面保证了你能精准命中。所以只要页面上目标元素有ID,无脑用ID基本不会错。

举个例子,登录框的HTML大概长这样:

<input type="text" id="username" name="username" placeholder="请输入用户名">

对应的定位代码就是:

driver.find_element(By.ID, "username")

你可能会想,这太简单了。是的,就这么简单,但这里有几个隐藏的注意点。

第一个坑:动态ID。很多前端框架,像Vue、React,在列表渲染的时候会给元素生成动态ID,比如user_1001、user_1002,每次进入页面数字都不一样。这种ID你写死就没意义了,得想别的招。

第二个坑:前端同事可能压根没写ID。写ID对前端来说是“多一行代码的事”,有些团队不强调这个规范,你打开控制台一通翻,发现目标元素干干净净,一个ID都没有。

第三个坑:重复ID。虽然规范说ID唯一,但实际开发里我真见过不止一次多个元素同ID的情况——比如后端模板渲染把同样的代码块输出多遍。这时候find_element取第一个,可能根本不是你要的那个。

所以我的习惯是:发现ID先确认,打开浏览器开发者工具,Ctrl+F搜索一下这个ID,看页面里有几个结果。只有一个,放心用;有多个,立刻放弃ID。

2.2 Name定位与Class Name定位:形态相似的“备用钥匙”

Name和Class Name都是基于属性的定位,一个用name属性,一个用class属性。它们不像ID那样强制唯一,所以用起来要更小心。

Name定位长这样:

<input type="text" id="username" name="username">
driver.find_element(By.NAME, "username")

Name定位在表单场景里特别好用,因为表单控件的name属性通常就是提交给后端的字段名,有习惯性命名。但要注意,同名的name属性可以出现多次——比如一组单选框,它们的name往往是相同的。你要定位一组单选框中的某一个,光用Name就分不清了,得配合其他方式。

Class Name定位是用来匹配元素的CSS类名的。注意一个重点:By.CLASS_NAME匹配的是单个类名,不是完整的class属性值。你看到这样一段HTML:

<button class="btn btn-primary login-btn">登录</button>

类的完整值是btn btn-primary login-btn,但用By.CLASS_NAME时只能写其中一个,比如写"login-btn"或"btn-primary",不能写"btn btn-primary login-btn",那样是匹配不到的。这一点我见过太多人栽跟头,报了NoSuchElementException,查了半天才反应过来是自己把整个class属性值粘进去了。

Class定位的好处是类名通常带有语义,像submit-btn、error-message这种一看就知道是干嘛的。缺点是你去匹配的类名如果样式变更换掉了,脚本跟着凉。还有,同一个类名能用在无数元素上,选的时候一定要确认目标是不是唯一。

2.3 Tag Name定位:能用但千万别单用

Tag Name是按HTML标签名定位,比如input、button、a、div。理论上它能用,实际上除了极少数场景,我不推荐单用。

为什么?你想啊,一个正常页面上有多少个div、多少个input?你写一句:

driver.find_element(By.TAG_NAME, "input")

拿到的永远是这个页面的第一个input,大概率不是你想要的那个。除非你操作的对象非常特殊,整个页面上就一个同类标签,比如某个页面只有一个canvas或者只有一个iframe,那用Tag Name还挺方便。我实际用Tag Name最多的场景是定位页面里的iframe:

driver.switch_to.frame(driver.find_element(By.TAG_NAME, "iframe"))

所以这个定位方式你要知道、会写,但别当主力用。真正做项目的时候,Tag Name通常是和其他定位方式组合使用的,比如先定位到某个大的容器div,再在它的范围内用Tag Name找子元素,这种用法我在后面讲XPath的时候会展开。

2.4 Link Text与Partial Link Text定位:超链接专用

这两个定位方式只针对<a>标签,也就是页面上的超链接。它们按链接的显示文本来匹配,定位方式非常直观。

<a href="/forgot-password">忘记密码?</a>
driver.find_element(By.LINK_TEXT, "忘记密码?")

Link Text要求文本完全匹配,多了少了一个字都算找不到。Partial Link Text则是模糊匹配,只要文本包含你写的部分就行:

driver.find_element(By.PARTIAL_LINK_TEXT, "忘记")

这里有个使用上的细节:Link Text匹配的是链接的可见文本,不是href属性,也不是title属性。还有,一个页面上可能有多处同样文字的链接——比如顶部导航和页脚都有“联系我们”,用Link Text只会取第一个,你需要根据实际页面结构再判断。

Partial Link Text有个老毛病,就是“缘分匹配”。你写了个“忘记”,结果页面上有“忘记密码”和“忘记账号”两个链接,它就找第一个,不一定是你要的。我个人经验是:能用全文本匹配就别用模糊匹配,模糊匹配是最后手段。

来,把上面五种总结成一个对比,方便你按场景选:

定位方式匹配依据是否要求唯一典型适用场景主要风险
IDid属性值规范要求唯一有ID的输入框、按钮动态ID、前端不写ID
Namename属性值不要求表单控件、单选框组同名元素多
Class Name单个class类名不要求有语义类名的按钮/容器类型名易变、重复多
Tag NameHTML标签名不要求定位iframe、特殊页面同名标签太多
Link Text超链接可见文本不要求导航链接、文字按钮文本重复
Partial Link Text超链接文本包含不要求长文本链接的模糊匹配匹配到错误元素

这里我得再强调一次:表格里说的“唯一”,你写完定位后最好自己验证一下。验证方法不复杂,用浏览器F12开发者工具,在Elements面板里按Ctrl+F,输入你的定位表达式,它会把所有匹配到的元素高亮出来,你数一数匹配数量,这是我最常用的自查方式。

3. pytest与Selenium的第一次握手:最小可用测试骨架

定位方法讲了一堆,但脚本不能只停在定位上。现实中你几乎不会单独用Selenium裸写脚本跑自动化,而是会搭一个测试框架来管理用例、断言和报告。pytest是目前Python生态里最主流的测试框架,和Selenium搭配起来非常舒服。这一节我们把环境、代码骨架和运行方式串一遍,确保你能亲手跑起来第一个定位用例。

3.1 环境准备与依赖安装

先把基础环境列一下。你需要一个Python环境(我用的是3.9以上的版本)和一个浏览器(比如Chrome或Firefox),以及对应的浏览器驱动。Chrome这块我用得最多,驱动要和浏览器版本匹配,不然启动就会报session异常。

依赖装起来很简单,就三个核心库:

pip install pytest selenium webdriver-manager

这里重点说下webdriver-manager。以前我们都要手动下载驱动文件,放到项目目录里,还要操心版本匹配问题。现在有了这个库,它会自动匹配你本地的浏览器版本并下载对应驱动,省掉了一堆环境上的破事。

装完依赖,你可以先跑一个小命令验证环境通不通:

from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())) driver.get("https://example.com")

这只是一次性的连通性验证。真正写到项目里,我不会让每个用例都去下载驱动,而是交给fixture去统一管理,这个我们马上讲。

3.2 写一个能跑的定位脚本

先看一个最朴素的写法,不讲究封装,纯粹演示逻辑:

from selenium import webdriver from selenium.webdriver.common.by import By def test_login_page_username(): driver = webdriver.Chrome() driver.implicitly_wait(10) driver.get("https://example.com/login") username = driver.find_element(By.ID, "username") username.send_keys("tester01") driver.quit()

这段代码干了三件事:打开浏览器、找到用户名输入框、输入内容。虽然简陋,但它已经是完整的“定位+操作”闭环了。

但我得说,直接这么写在pytest里有个问题:如果用例执行到一半挂了,比如定位报错,浏览器就不会关闭,残留的窗口可能会一直占着资源。更规范的做法是把浏览器启动和关闭放到fixture里,让pytest帮你处理清理工作。

3.3 用fixture管理浏览器生命周期

pytest的fixture机制是它最大的优势之一。我们定义一个driver的fixture,每个用例用它的时候自动拿到一个浏览器实例,用例结束后自动关闭,不需要手动driver.quit()。

写一个最小但完整的conftest.py:

import pytest from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service @pytest.fixture def driver(): driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())) driver.implicitly_wait(10) yield driver driver.quit()

然后用例文件里就可以这么写:

from selenium.webdriver.common.by import By def test_login_page_elements(driver): driver.get("https://example.com/login") username = driver.find_element(By.ID, "username") password = driver.find_element(By.NAME, "password") login_button = driver.find_element(By.CLASS_NAME, "login-btn") assert username.is_displayed() assert password.is_displayed() assert login_button.is_displayed()

看到差别了吗?用例不管成功失败,fixture都会执行清理逻辑。这就是为什么我一直建议新手别在用例里直接启动浏览器,框架帮你处理生命周期才是正规打法。

再进阶一步,有时候你不想每条用例都启动一次浏览器,太慢了。那可以考虑scope="module"或scope="session",让同模块或整个测试会话复用同一个浏览器实例:

@pytest.fixture(scope="module") def driver(): driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())) driver.implicitly_wait(10) yield driver driver.quit()

这能明显提速,但代价是用例之间的耦合变高,一条用例改了浏览器状态,后面的用例可能受影响。项目初期我建议先保持“每个用例一个浏览器”的模式,稳定了再优化。

3.4 定位不到的用例怎么写才能快速发现问题

定位是测试的一部分,但你写过用例都知道,定位失败时pytest给你的一堆traceback信息很不友好。我习惯做两件事来让报错信息变得可读。

第一,断言里加一句“定位到了”的辅助信息,直接用is_displayed()或者is_enabled()做基础检查。第二,用pytest的-v和-s参数跑用例,能看到哪条用例在哪一步挂了,配合print(driver.page_source)定位现场。

还有一个小技巧:先把定位表达式写成变量,放在文件顶部统一管理。比如:

USERNAME_INPUT = (By.ID, "username") PASSWORD_INPUT = (By.NAME, "password") LOGIN_BUTTON = (By.CLASS_NAME, "login-btn")

这种写法叫“定位器模式”,好处是前端改了某个元素的属性,你只需要改这一个文件里的常量,不需要去几十条用例里逐个翻找。这个习惯建议从一开始就养起来,项目变大后你会感谢自己。

4. 实测踩坑:定位失败的常见原因与排查方法

定位代码写得再漂亮,也架不住实际运行环境的各种意外。这一节我把这些年遇到的定位失败场景盘点一下,按出现频率从高往低排,每一个都附上排查思路,你照着对照就行。

4.1 NoSuchElementException的三种“假装找不到”

NoSuchElementException是大家最熟悉的报错,看到它第一反应是“啊,定位写错了”。但经验告诉我,报错的原因有几种,定位表达式写错只是其中之一。

第一种:定位表达式和页面实际HTML结构不匹配。比如页面用的是id="user_name",你写的是By.ID, "username",一字之差,找不到。这种情况最直接,去开发者工具里再核对一遍属性值就能解决。

第二种:元素在DOM里存在,但在当前“瞬时状态”下不可见。Selenium的find_element默认只找DOM里的元素,它不管元素是不是真的显示出来。有些页面元素是动态插入的——比如点击某个按钮后,再弹出一个确认框,这个框里的按钮在你点击之前并不存在。如果你一进页面就立刻找这个按钮,就会报找不到。

第三种:元素在iframe里。这个坑是重灾区。你的主页面DOM和iframe内部的DOM是两套独立的文档树,find_element默认只搜当前主文档,不会钻进iframe里。所以你那个元素明明在页面上肉眼可见,脚本死活定位不到。解决办法是先切进iframe,再定位:

driver.switch_to.frame(driver.find_element(By.TAG_NAME, "iframe")) element = driver.find_element(By.CLASS_NAME, "confirm-btn")

操作完还要记得切回主文档:

driver.switch_to.default_content()

4.2 元素加载慢引发的“薛定谔的定位”

第二种高频问题:报错信息不是找不到,而是时好时坏。有的用例跑第一次挂了,重跑一遍又通过了。这种“偶现”问题最磨人,而且大概率是元素加载速度的问题。

页面加载和脚本执行是两个并行的过程。脚本执行到find_element的时候,可能页面还在渲染中,元素还没出现。你需要给脚本一个“等待”的机制。

最基本的做法是隐式等待,就是之前代码里写的implicitly_wait(10),意思是find元素时最多等10秒,10秒内轮询查找,超时就放弃。全局生效,省心。但它有个局限:它只能等元素“出现在DOM里”,有些你的元素在DOM里了但还不可点击,比如按钮还是disabled状态,这时候你还得用显式等待。

显式等待里最好用的就是WebDriverWait配合expected_conditions,比如:

from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 10) login_button = wait.until(EC.element_to_be_clickable((By.CLASS_NAME, "login-btn")))

这句的意思是:最多等10秒,直到这个按钮处于可点击状态。比起find_element后直接点击,这种写法在动态页面上稳定得多。我推荐把核心操作都尽量写成这种,尤其是点击、输入之前。

4.3 同页面重复元素怎么选

有一种定位失败不是“找不到”,而是“找到了但不是我要的”。页面上一组表单,字段名一样,类名一样,你以为定位到了目标,执行的时候却操作了另一个元素,断言结果当然对不上。

遇到这种情况,先回头分析页面结构:这几个相似元素肯定在一个共同的容器里。初级定位的解决思路是缩小范围:先定位到它们的父容器,再在父容器内查找子元素。

Selenium的find_element支持在已找到的元素实例上继续调用:

form_container = driver.find_element(By.ID, "registerForm") username = form_container.find_element(By.NAME, "username")

这样,即使页面别处还有同样的输入框,你也只会拿到注册表单里的那一个。这个方法在页面信息结构复杂时尤其好用。

4.4 一个排查定位问题的标准动作清单

说了这么多,我把自己每次遇到定位失败时的排查动作固定成了下面几步,按着走基本能搞定八成问题。

先打开F12开发者工具,确认你要操作的元素在当前页面上真实存在。这步看着废话,但人一忙起来真会搞错页面,测试环境点错了站点、对象根本不在这个页面。

然后在开发者工具的Console里跑一段JS验证定位表达式:

document.querySelector("#username")

如果是ID或CSS选择器,这个方式能立刻告诉你页面里到底有没有匹配元素。如果是XPath,可以用$x("//input[@id='username']")。

再检查是不是iframe的问题,确认目标元素在没在iframe里。

接着看代码的执行时序:元素是不是在某个异步回调之后才出现?如果是,检查有没有等待逻辑。

最后才考虑修改定位方式。按这个顺序排查,大部分“找不到”的问题都能在几分钟内定位到根因。

5. 从初级定位迈向稳定的自动化:我的几点心得

文章最后聊几句实操心得。前面把六种初级定位和pytest的写法讲完了,但我想再强调几件“不是语法却能决定成败”的事。

第一,能写ID就优先ID,但别神化ID。ID定位最快,代码最好读,但它依赖前端的配合。如果你的项目前端不太注重可测试性,今天有ID明天就删了,那你把宝全押在ID上就会很痛苦。更稳妥的思路是:和前端约定一套稳定的测试标识,比如在元素上加>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询