☰
Selenium自动化用例编写8大技巧:从稳定定位到高效执行
2026/10/21 10:05:06 网站建设 项目流程

作为一个成天和自动化用例打交道的人,我太清楚Selenium测试用例最让人头疼的是什么了——不是写不出来,而是写出来之后跑不稳、改不动、一换环境就崩。明明元素定位没问题,脚本却隔三差五报NoSuchElementException;昨天跑得好好的用例,今天换个浏览器版本就翻车;用例一多,执行时间暴涨,日志又看不出问题在哪里。这些坑,几乎每个写Selenium的测试同学都踩过。今天这篇文章,我把自己这些年总结出的8种必备Selenium编写自动化用例的技巧一次性整理出来,里面没有那种只讲概念的科普,全部是能直接落到代码里、能立刻改善用例稳定性和可维护性的实操经验。无论你是刚接触自动化的新人,还是已经维护过一批用例的中级工程师,这篇文章都能给你一些值得抄作业的思路。

1. 内容整体设计与思路拆解

1.1 8种技巧分别是什么

先把这8种技巧摆出来,后面逐一展开:

  1. 元素定位策略:不要用脆弱定位器,优先ID和CSS Selector。
  2. 等待策略:放弃不靠谱的强制等待,用显式等待替代。
  3. Page Object模型:把页面元素和操作封装起来,测试逻辑和页面细节解耦。
  4. 数据驱动测试:用参数化把测试数据从代码中抽离出来。
  5. 失败现场留存:自动截图、保存DOM和日志,出问题时能快速定位。
  6. 异常处理与重试机制:给偶发失败一个“自愈”的机会,但要有上限。
  7. 浏览器选项配置:手动稳定浏览器环境,减少外部因素干扰。
  8. 用例执行策略:合理分级、并行执行,让测试既快又稳。

这8个技巧不是相互独立的,它们之间会有很多交叉。比如显式等待本身就需要配合好用的元素定位方式,而Page Object里也离不开合理的异常处理和截图机制。把它们放在一起用,才能形成一套比较完整的“自动化用例工程化”打法。

1.2 为什么需要这些技巧

很多同学最开始写自动化用例的时候,心里想的是“只要能跑通就算成功”。于是脚本里面充满了sleep(3)、find_element_by_xpath("//div[3]/ul/li[2]/a")这种写法。短期内确实能跑,但时间一长问题全来了:页面多了一个div,定位就失败;网络慢了一秒,用例就开始随机失败。这种“能跑”的用例,维护成本往往比手工测试还高,最后只能被团队废弃。

我之所以整理这8个技巧,核心目标就三个字:稳定性、可读性、可维护性。稳定性解决的是“用例能不能可靠地反映产品问题”的问题。可读性解决的是“别人拿到代码能不能快速看懂”的问题。可维护性解决的是“页面变化时,修改成本是多高”的问题。现实中很多团队测的是快速迭代的Web应用,页面一周能改好几次,如果用例写得过于具体、过于脆弱,那自动化反而会成为团队的负担。

2. 元素定位与同步机制:让用例不“闯鬼”

2.1 元素定位:别用索引和过于脆弱的定位器

Selenium定位元素的方式有很多种,ID、Name、Class Name、Tag Name、CSS Selector、XPath,还有Link Text和Partial Link Text。实际写用例时,我的策略是有一个明确的优先级顺序:ID排第一,然后是CSS Selector,再往后是XPath,最后才是那些文本和索引相关的定位方式。

为什么ID优先级最高?因为ID在页面里理论上应该唯一且稳定,性能也好。CSS Selector之所以值得推荐,是因为它比XPath简洁,并且不受页面结构调整的影响太大。比如某个按钮,大家可以写成#submit-btn,也可以写成button.btn-primary。这些写法都比XPath那种一长串的//div[@class='container']//div[2]//button[1]要稳得多。索引定位最大的问题是它跟页面结构深度绑定,增删一个DOM节点,后面索引全变。一旦用例里出现[1]、[2]这种字面索引,基本等于埋了一颗雷。

XPath也不是不能用,它适合处理那些没有ID、没有独立属性,只能通过文本或层级关系来定位的元素。但即便是用XPath,我也建议尽量用相对路径加属性条件,像//button[contains(@class,'login')],而不是靠绝对路径。绝对路径不仅长,而且把整个DOM结构都耦合进去了,完全没有任何必要。

另外还有一点值得注意:很多初学者会用find_element_by_*这种老写法,新版Selenium里已经推荐用By对象了。比如driver.find_element(By.ID, "username")。这个改动不光是风格问题,统一写法之后,代码适配不同Selenium版本的迁移成本会低很多。我在实际维护老代码时,遇到最头痛的就是一堆find_element_by_xpath混着find_element_by_css_selector的写法,一旦升级框架,到处报DeprecationWarning,非常麻烦。

2.2 等待策略:强制等待、隐式等待、显式等待怎么选

等待这件事,是整个Selenium用例稳定性的分水岭。我见过太多用例失败,最后排查下来就是等的方式不对。这里先说一个基本结论:能不用sleep就不用sleep;隐式等待可以作为兜底,但不要依赖它;显式等待才是最适合做同步的手段。

强制等待,也就是time.sleep(3),它的问题是死等。网络快了,它浪费3秒;网络慢了,它倒可能还是不够。页面加载时间和数据返回时间本身就不可控,写死一个数字只会让用例要么慢、要么随机失败。偶尔调试的时候用一下还行,真正跑回归测试的用例里不应该出现。

隐式等待,也就是driver.implicitly_wait(10),它会告诉驱动在查找元素时最多轮询多长时间。它有一个很麻烦的特性:全局生效,一旦设置了,所有find_element操作都会受到影响,而且它只作用于元素是否存在,不关心元素是否可见、是否可点击。这就导致了一种尴尬场景:元素已经在DOM里了,但还在加载渲染,后台数据也没填充,你直接去点,照样报错。

显式等待是WebDriverWait配合expected_conditions,相当于只针对某个条件反复尝试,直到条件满足或者超时。这种等待的好处是精确打击:我要等的是“登录按钮可点击”,那就等到它可点击为止。前端慢一点,我就多等一会儿;前端快,我一点都不会浪费。在我的实际项目里,几乎每个用例的核心交互都会用到显式等待。

这里也提醒一句:等待时间不要设置得过于极端。我见过有人把超时时间设成60秒,理由是不想看到失败。这在我看来是掩耳盗铃,超时设太长,用例失败一次要等一分钟,调试效率极低。一般10秒到15秒是比较合理的区间,具体看你的应用响应速度。宁可偶尔因为慢失败,也不要让所有用例的平均执行时间被无意义的等待拉爆。

3. 用例的框架化:Page Object与数据驱动

3.1 Page Object模型:页面对象和测试逻辑解耦

Page Object,简称PO,是目前Web自动化领域最经典、也最值得推荐的设计模式。它的核心思想很简单:一个页面在测试代码里对应一个类,这个类里封装页面元素怎么定位、页面操作怎么做,而测试用例本身只关心业务步骤和断言。这样做的好处非常明显:页面改版时,我只需要去改对应的页面类,而不需要在十个用例里逐个修改选择器;用例的可读性也大大提高,一眼看上去就能知道在做什么业务操作。

举个例子。我现在要测一个登录功能,通常不会在用例里直接写driver.find_element(By.ID, "username").send_keys("user01")这种代码,而是先写一个LoginPage类:

class LoginPage: def __init__(self, driver): self.driver = driver self.username_input = (By.ID, "username") self.password_input = (By.ID, "password") self.login_btn = (By.CSS_SELECTOR, "button.login") def enter_username(self, username): WebDriverWait(self.driver, 10).until( EC.visibility_of_element_located(self.username_input) ).send_keys(username) def enter_password(self, password): self.driver.find_element(*self.password_input).send_keys(password) def click_login(self): self.driver.find_element(*self.login_btn).click()

注意,这里我把定位方式都写成tuple形式的变量,放在类属性里,而不是散落在每个方法中。这样在同一个页面类里,如果多处需要引用同一个元素,直接复用变量即可。将来这个元素的定位方式变了,只要改类属性这一行。

更进一步,我通常还会封装一个BasePage,把一些公共操作放进去,比如统一等待、统一点击、统一输入。这样每个页面类只需要继承BasePage,写自己的业务逻辑。这个模式的好处是,把技术细节下沉,测试用例看起来更像一个个业务动作。我参与过一个大型项目,一千多条用例,用的就是这套结构。后来前端框架大改,页面结构几乎重写,团队成员只花了两三天改页面类,用例层面几乎没动,这就能省下非常多的维护时间。

3.2 数据驱动:用参数化把测试数据搬出代码

数据驱动测试,核心思路是把测试数据从测试逻辑中分离出来。传统的用例写法是一个用例写死一组数据,比如调用login("user01", "123456"),如果你还想测一个密码错误的分支,就得再复制一个用例方法,改两个参数。数据一多,用例数量膨胀,维护成本极高。数据驱动则更聪明:同一个测试逻辑,由外部传入不同的数据组合来批量执行。

在实际工程里,我经常用pytest参数化来实现:

import pytest @pytest.mark.parametrize( "username,password,expected", [ ("tester01", "pass123", "登录成功"), ("tester02", "bad", "用户名或密码错误"), ("", "pass123", "请输入用户名"), ] ) def test_login(username, password, expected): page = LoginPage(driver) page.enter_username(username) page.enter_password(password) page.click_login() assert expected in page.get_tip_text()

这样一个test_login函数,就覆盖了成功、失败、空输入三种场景。后续要新增一组测试数据,只要往列表里再加一个tuple即可,完全不用动测试逻辑。如果数据量比较大,甚至可以抽到JSON或YAML文件里,在用例里统一读取,做到真正的数据与代码分离。

在使用参数化的时候,有一条经验很重要:测试数据里最好带上描述性字段,比如用例编号、预期结果等。这样当某组参数跑挂的时候,日志里能直接看出来是哪个业务场景出了问题,而不是只看得到一组原始的用户名和密码。我在自己项目里会习惯加一个字段叫case_desc,哪怕它只用来输出日志,也能让失败定位快很多。

4. 用例的鲁棒性:异常处理、重试与调试

4.1 失败截图与日志:第一现场才是最重要的

自动化用例挂掉的时候,最怕的就是只知道“断言失败”,没有现场信息。尤其是那些偶发失败,你本地跑一遍是好的,CI机器上一跑就挂。如果没有第一现场,你根本无从下手。所以我一直强调,用例里必须要有失败现场留存机制。最基础的两样东西:截图和DOM快照。

截图比较好理解,调用driver.save_screenshot就能把当时浏览器画面保存下来。但光有截图还不够,有些页面问题在截图里表现不明显,我还会把当时的页面源码保存成HTML文件。拿到这份HTML,我可以直接检查当前页面到底长什么样、哪些元素存在、哪些不存在。这个对于定位所谓“元素不存在”的问题尤其有用——有时候并不是元素真的不存在,而是你定位的写法和当前实际DOM结构不一致,或者元素隐藏在iframe里。

具体实现上,我在项目里会写一个pytest的hook,在用例失败时自动触发截图和DOM保存:

import datetime import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: driver = item.func_args[0].driver ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") driver.save_screenshot(f"artifacts/{item.name}_{ts}.png") with open(f"artifacts/{item.name}_{ts}.html", "w", encoding="utf-8") as f: f.write(driver.page_source)

这里的思路是:hookwrapper包裹了pytest的测试执行过程,测试方法失败后,从测试函数参数里取出driver实例,然后保存状态。大家可以根据自己框架的情况做适配,比如Java的TestNG里也有HttpResultCallback之类的扩展方式。总之,截图和HTML快照这两样,是自动化用例的“行车记录仪”,平时看不出价值,一出问题就知道它们多重要了。

另外,日志也很关键。我会建议在关键操作前后都输出明确的信息,比如“开始点击登录按钮”、“等待弹窗出现”、“输入用户名完成”。日志不是越多越好,而是要在关键时刻有记录。这样配合截图和DOM,很多偶发问题基本上在十分钟之内就能定位到根因,不用反复跑用例复现。

4.2 异常重试机制与容错处理

自动化用例在CI环境里跑,最让人抓狂的就是“偶发失败”。页面资源加载慢了100毫秒,某个动画稍微拖了一点,用例就挂了,可产品功能本身完全没问题。这种失败既消耗团队信任,也浪费时间。要让用例具备“自愈”能力,重试机制是一个比较有效的策略。

重试不是无脑重跑整个用例,而是要分场景讨论。如果是断言失败,说明业务逻辑可能真有bug,这种不要盲目重试,否则会掩盖产品缺陷。但如果是NoSuchElementException、ElementNotInteractableException、ElementClickInterceptedException这类“环境因素”导致的失败,重试往往能解决问题。因为这些问题很多时候是网络延迟、资源未加载完、动画遮挡点击区域引起的临时性问题。

我个人的做法是按照“元素操作级别”设置有限重试,而不是整个用例无限重试。换句话说,点击某个按钮失败了,可以尝试重新查找、重新点击,但最多试两次。如果两次都失败,就立即抛出异常,进入失败流程。这样既避免了偶发问题影响结果,又不会把一个真正的bug掩盖掉。

如果是使用pytest,有一个现成插件pytest-rerunfailures,可以配置重跑次数。我一般会配置成:

pytest --rerun-failures 2 --rerun-delay 1

它会针对失败用例重跑最多2次,每次间隔1秒。注意,这里的重跑是整个用例级别的,对于那种有副作用的用例要格外小心。比如一个用例前两步已经下了订单、创建了数据,重跑时可能又创建一次,导致脏数据。这种用例最好不要整条重跑,而是在代码里对单个不稳定步骤做小范围重试。判断标准就一个:用例是否幂等。不幂等的用例,重试只会制造更多垃圾数据,风险大于收益。

5. 工程化落地:浏览器配置、并行执行与维护

5.1 浏览器选项配置:Headless与运行效率

很多团队会把自动化用例跑在CI机器上,这类机器通常没有显示器,这时就必须考虑浏览器的运行模式。Chrome和Firefox都支持Headless模式,也就是无头运行。无头模式下浏览器不会弹出窗口,执行速度也更快,非常适合CI环境。

用Chrome来举例,我一般这样配置:

from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--headless=new") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") options.add_argument("--window-size=1920,1080") driver = webdriver.Chrome(options=options)

这里几个参数的作用值得解释一下:--headless是新版无头模式;--disable-gpu是避免一些图形相关的兼容问题;--no-sandbox在部分容器环境里不加会直接报错;--window-size设置浏览器窗口大小,因为有些页面元素是否可见跟视口大小直接相关,尤其是那些滚动后才加载的元素,窗口太小会导致定位失败。窗口大小建议跟你的目标分辨率对齐,大多数Web应用默认按照1366x768或者1920x1080来设计,设成1080p常常是最稳的选择。

还有一点容易被忽略:浏览器的自动更新。CI环境里如果不锁定浏览器版本,经常会出现“昨天还跑得好好的,今天全部用例报错”的诡异情况。原因很可能就是浏览器自动升级到了新版本,而Selenium对应用的是旧版驱动。所以建议固定浏览器版本和驱动版本,最好在CI里用指定的镜像或安装包,不要让系统自动升级。

另外,在本地开发调试时,我通常不会用Headless模式,而是用有头模式跑一遍,看到浏览器实际操作过程,很多问题一眼就能发现。等用例稳定了,再切到Headless模式做回归。两种模式切换可以通过一个环境变量来控制,比如为True就加载无头配置,否则正常启动,这样开发和CI都照顾到了。

5.2 用例分级与并行执行:省时间但不牺牲质量

用例多了之后,一个绕不开的问题是执行时间。一个Web项目的回归用例如果有一千条,串行跑可能得四五个小时,这个时间成本谁都无法接受。所以用例执行策略必须考虑分级和并行。

分级,就是把用例按风险和价值分成不同层级。比如冒烟测试只覆盖核心流程,全量回归才跑所有用例。我一般会打标签,level=1、level=2、level=3,提交代码之后先跑level=1,只有通过才跑更高层级。这样可以在保障质量的前提下,极大减少无效等待。

并行,常见做法是用pytest-xdist插件的-n参数,比如同时开4个进程:

pytest -n 4 --dist loadfile

并行有个前提条件,就是用例之间不能互相依赖,否则就容易出问题。举个例子,测试A创建了一个订单,测试B依赖这个订单去执行下一步,那这两条用例如果并行跑,B大概率会失败。所以并行之前,务必先检查用例的独立性。我之前接手过一个项目,几个用例共用同一个测试账号,并行跑了没几次,就因为资源竞争导致一堆账号被锁定。后来改成每条用例用独立账号或者每次造数,问题才算解决。

也就是说,并行不是无脑加进程数。如果你的应用本身扛不住那么多并发请求,而数据库又需要保持数据隔离,并行度太高照样会失败。压测一下你的应用可以承受多少并发,再根据实际来定-n的数值。一般先在本地小范围并行跑一遍,观察失败率,再逐步增加并行度,是比较稳妥的方式。

6. 常见问题与排查技巧实录

6.1 等待失效、iframe、新窗口、Cookies四大典型翻车现场

写Selenium用例这么多年,我把最常见的失败原因整理成了一个速查表,每个问题基本都能在这种表里找到对应解法。

典型场景表现排查思路推荐解法
等待失效偶尔出现NoSuchElementException检查元素是否在iframe里,是否打开了新标签页用显式等待,并先switch_to.frame/switch_to.window
iframe内元素无法定位明明在页面上看得到元素却报找不到打开F12确认元素是否嵌套在iframe中先driver.switch_to.frame(iframe)再操作,用完记得切回
新窗口/新标签页操作新页面元素报元素不存在检查当前driver handle指向的窗口使用window_handles切换目标窗口,切完再操作
自动化导致页面行为差异手工能过,脚本跑不通可能是it_is_not事件、动态渲染时机不同使用逐元素显式等待,减少无意义等待
登录状态丢失用例A后用B失败用例执行顺序改变,共享了Cookies每条用例独立启动浏览器或在setUp中统一注入Cookies

这几个问题里我想单独说说iframe。很多测试新手第一次遇到iframe都懵了,明明元素就在眼前,代码却死活定位不到。原因很简单:页面里嵌了一个独立的文档。如果你不切换到iframe里,Selenium在顶级文档里是找不到内部元素的。只要看到报错里带着frame相关字眼,或者F12里看到元素的父节点有 > 标签,那就先切换再操作。

切换写法很简单:

driver.switch_to.frame("frame_id") # 或者按索引、按WebElement切换 # 操作iframe里的元素 driver.find_element(By.ID, "inside_frame_btn").click() # 操作完成后切回默认内容 driver.switch_to.default_content()

6.2 几点长久维护的实操心得

文章最后,我再分享几个不能直接写进代码,但能影响整个测试工程生命周期的经验。

第一,用例里的定位器要尽量避免使用过于脆弱的元素属性。比如动态生成的id,像user_12345这种,每次刷新都变化,你要是直接定位它,用例必然不稳定。遇到这种情况,优先找稳定的父级或者兄弟节点,又或者用data-*属性。给开发提需求的时候,我有时也会建议他们给关键元素加上data-testid之类的稳定标识。很多人觉得自动化是测试的事,实际上,开发配合加一两个稳定属性,能让整个自动化工程的稳定性上一个台阶。这事比写一百条技巧都管用。

第二,写用例的时候脑子里要有一个原则:用例测的是业务,不是测前端实现细节。不要为了覆盖率去关心某个div的class是不是变了,而应该关心“用户能不能完成这个操作”。所以定位元素时,尽量找语义上稳定的锚点,比如业务名称、button的文案等。这样前端重构的时候,用例受到的冲击才最小。

第三,定期审视失败用例。我一直建议团队每周抽时间看一眼测试报告,把那些一直失败或者被skip的用例清理掉。如果一条用例连续多次失败,可能不是产品bug,而是用例本身设计有问题。与其花时间反复重试一条注定失败的用例,不如花时间改进定位策略或者等待条件。自动化测试最重要的资产不是脚本本身,而是脚本背后你对产品业务的理解和把控。

最后再说一个我的习惯:每次写完一批用例,我都会在自己的机器上把用例迭代跑三遍,确认稳定了再提交到CI。这个过程看起来费时间,但能提前榨出很多由于环境差异造成的偶发问题。等你提交上去,再从CI回传一份红色报告,处理成本可就要高出一倍不止了。自动化测试这件事,说到底还是一个工程化的过程。把这些基础方法用扎实了,你的用例库才能真正做到既能高效回归,又能长期稳定地守住质量底线。

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

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

立即咨询