ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Web自动化测试实战:从选型到pytest+Selenium框架落地

Web自动化测试实战:从选型到pytest+Selenium框架落地 1. 先想清楚你的项目适不适合做 Web 自动化测试做 Web 自动化测试这几年我经常被新入行的同事问同一个问题“这个项目能不能上自动化” 每次我都先反问一句“你准备用它解决什么问题” 如果回答是“领导让做的”或者“别人家都有自动化了”那我会劝他再缓一缓。如果回答是“回归测试太费人力想用机器替代重复劳动”那这事就值得认真做。Web 自动化测试的本质是用脚本模拟真实用户在浏览器里的操作去验证系统功能是否符合预期。它解决的最大痛点是回归测试的效率问题——一个功能改了你不需要手工点十几遍页面跑一遍脚本就知道哪些地方被改坏了。它适合三类人参考纯业务测试想往测试开发方向转的、已经在写脚本但框架混乱想重新整理的、以及准备在公司从零搭建自动化体系的技术负责人。先说我的总体感受Web 自动化测试的入门门槛是技术但决定成败的往往是工程化意识。很多人写了一年自动化用例依然被元素定位、等待时间、跨环境执行这些事折磨得痛不欲生根本原因不是不懂 Selenium 语法而是没有建立一套完整的设计思路。这篇总结我会把选型、定位、等待策略、框架搭建、CI 集成、常见坑这些平时最容易踩雷的地方从头到尾过一遍中间穿插我踩过的坑和验证过有效的做法。1.1 自动化测试金字塔与 ROI 判断自动化测试圈子里有个经典模型叫测试金字塔从上到下分成三层UI 层、接口层、单元层。UI 自动化属于最上面那一层特点是执行速度最慢、编写和维护成本最高但最贴近用户真实使用路径。我见过很多团队一上来就猛写 UI 用例把页面上的每个按钮都写成脚本最后用例数量上去了但每次跑完红了一大片没人愿意维护最后整套东西变成摆设。判断一个项目适不适合做 Web 自动化我一般参考三个条件需求相对稳定页面结构和业务流程不会频繁大改。如果一个版本迭代里页面布局一次一变你写定位器的速度永远赶不上开发改代码的速度。回归价值明确核心业务链路值得反复验证修一次 bug 就要重新验证一遍主流程。有稳定的测试环境环境动不动就挂、测试数据缺东少西的项目脚本跑得再快也没意义。另外一个很多人都忽略的判断维度是投入产出比。我的经验是Web 自动化用例中大约 20% 的主流程用例能覆盖 80% 的回归风险所以最合理的起步方式是先圈定 10~20 条真正核心的业务链路把手动回归的时间降下来而不是追求用例数量上的“好看”。1.2 工具选型Selenium 依然够用但 Playwright 可以优先试工具选型是整个自动化方案里最容易被反复争论的话题。目前 Web 自动化领域的主流选择无非是三款Selenium、Playwright、Cypress。我把它们的核心差异整理成一张表方便对比工具浏览器支持等待机制语言生态上手难度典型优势Selenium WebDriverChrome、Firefox、Edge、Safari 等覆盖最广隐式等待 显式等待需自行处理Java、Python、JS 等几乎全语言中等生态成熟、资料多、WebDriver 标准事实上的标杆PlaywrightChromium、Firefox、WebKit自动等待 Actionability 检查JS、Python、Java 等偏低能多浏览器一致测试自带自动等待拦截网络请求方便CypressChrome 系 Edge Firefox运行在自己的进程里自动重试内置默认等待JS/TS偏低对前端开发者友好调试体验好很多老项目还在用 Selenium这是完全没问题的。Selenium 最大的资本是生态踩过坑的人足够多随便搜一个报错都能找到答案。但如果你是今年才开始搭新框架我会推荐优先试 Playwright。原因有两点第一Playwright 内置了Actionability 检查点击之前会自动判断元素是否可见、是否稳定、是否可被点击不再需要我们手动写一堆“等元素可见再操作”的代码脚本明显更简洁。第二它的codegen 录制工具能直接把用户在浏览器里的操作转成脚本我们把这个作为脚本初稿再手工补断言和维护比从零写快得多。不过要注意Playwright 官方对语言绑定支持最好的是 JavaScript/TypeScript 和 Python如果你团队主力是 JavaSelenium 依然是很稳妥的选择。2. 核心细节解析元素定位、等待策略与测试数据工具定了以后真正拉开脚本质量差距的是几个基础环节的细节处理。我在面试测试开发的时候经常问候选人两个问题你如何保证脚本能稳定找到页面上的元素你的脚本在慢网环境下会不会大面积超时能答好这两个问题的人通常都有过一段被线上用例折磨的实战经历。2.1 元素定位的优先级从 id 开始不要全靠 XPathSelenium 和 Playwright 都支持多种元素定位方式。Selenium 有 id、name、class_name、tag_name、link_text、partial_link_text、xpath、css_selector 这八大金刚。我给出的优先级建议是id页面里 id 理论上唯一定位速度最快要优先用。name表单类元素常用仅次于 id。css_selector按类名、属性、层级关系定位语法简洁性能也很好。xpath最灵活特别是处理复杂兄弟节点、父子嵌套关系时无往不利。link_text / partial_link_text只适合定位 a 链接。class_name / tag_name很少单独用基本都会配合层级过滤。很多人一上来就写//*[idloginForm]/div[3]/input这种一长串绝对路径 XPath页面里多包一层 div 就失效。我的习惯是能用属性精确匹配的就尽量不用路径。比如用 css 写成#loginForm input[nameusername]这比一长串/div/div[2]/div/input健壮得多。另外 XPath 里一个非常实用的小技巧是使用contains做模糊匹配//button[contains(class, submit-btn)]这个写法适合开发经常在 class 后面追加状态类active、disabled 这类的场景它能帮我们避免掉“今天能用、明天就断”的尴尬。2.2 等待策略显式等待优先强制等待慎用Web 自动化测试里最经典的翻车现场就是“脚本在本地能跑一到 CI 上就时不时报找不到元素”。绝大多数情况都是等待策略有问题。等待一共三种强制等待sleep不管页面加载快慢固定睡几秒。简单粗暴但极大拖慢测试速度而且只适合在调试的时候临时用。隐式等待implicitly_wait设置一个全局超时WebDriver 每次查找元素时如果没找到会继续轮询直到超时。缺点是无法应对“元素已存在但不可操作”的情况。显式等待WebDriverWait expected_conditions针对某个特定条件反复轮询直到元素满足“可见”“可点击”等状态再继续执行。我强烈建议以显式等待为主、隐式等待为辅。给个简单的 Python 示例from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By def wait_and_click(driver, locator, timeout10): element WebDriverWait(driver, timeout).until( EC.element_to_be_clickable(locator) ) element.click()这里的element_to_be_clickable是很多人都忽略的条件它的内部实现不仅是判断是否存在还会确认元素可见并且没有被遮挡能有效处理“按钮被弹窗盖住点不了”的场景。除此之外presence_of_element_located适合判断“页面渲染出来了”visibility_of_element_located适合判断“已经展示给用户了”两者语义不同用错了也会产生误报。2.3 Page Object 模式把页面逻辑和测试用例解耦写过几十条用例之后你就会发现如果每条用例里都直接写driver.find_element(...).click()一旦页面元素属性变了你就要翻遍所有用例逐条改那个滋味相当酸爽。所以成熟的项目都会引入Page Object ModePOM设计模式思想很简单每个页面封装成一个类页面上所有元素和可操作行为都收敛到这个类里测试用例只关心业务操作不关心元素怎么定位。举个例子登录页我会这样设计class LoginPage: def __init__(self, driver): self.driver driver self.username_input (By.ID, username) self.password_input (By.ID, password) self.login_button (By.CSS_SELECTOR, button[typesubmit]) def input_username(self, username): self.driver.find_element(*self.username_input).send_keys(username) def input_password(self, password): self.driver.find_element(*self.password_input).send_keys(password) def click_login(self): self.driver.find_element(*self.login_button).click() def login(self, username, password): self.input_username(username) self.input_password(password) self.click_login()这样改动的收益很明显页面结构变了我只改 LoginPage 一个类测试用例里调用的是login_page.login(user, pwd)可读性和业务贴合度都大幅提升。这条经验来自实际项目我的一个老项目有约 300 条 UI 用例引入 POM 之前一次页面改版要花三个人天去修定位器重构之后一个人半天就能搞定。2.4 测试数据管理不要每一条用例都在脚本里造数测试数据问题比元素定位更容易被忽略但爆发起来也更致命。最典型的反面案例是注册用例每次跑都用一个写死的手机号第一次成功、第二次就被提示“号码已注册”于是用例开始间歇性飘红。我的做法是这三个方向数据工厂化用代码生成唯一性的数据比如按时间戳拼接用户名用例之间不共享数据避免互相污染。接口预置数据在执行页面操作前通过后端接口直接造好前置数据而不是每次 UI 一步步点出来。这能省大量执行时间也减少失败点。数据清理在用例的 teardown 里删除或归档本次产生的数据让每轮执行的环境状态保持干净。拿注册用例来说数据预置思路大致是这样import time unique_name ftest_user_{int(time.time())} email f{unique_name}example.com # 调用注册接口直接创建账号或者干脆用 SQL 指令插入一条记录 # 这一步成功后再去 UI 层做登录验证这样就算页面还没到可以填写表单的状态你的数据也已经就位了用例本身的稳定性能提高一大截。3. 实操过程从零搭一套 pytest Selenium Allure 框架理论说再多不如落地一套框架来得实在。下面我用一套非常经典也易于扩展的技术栈来演示pytest 作为测试框架Selenium WebDriver 做浏览器驱动Allure 做测试报告。这套组合在国内团队里用得非常多资料齐全、岗位需求也大作为入门和团队标准都很合适。3.1 框架目录设计从一开始就别写成“一堆脚本堆在一起”很多刚接触自动化的同学喜欢把所有脚本放进同一个 py 文件里一两百条用例全挤在一起看着就脑壳疼。合理的目录结构至少要按职责拆分web_auto_test/ ├── config/ # 配置文件存放环境地址、账号信息等 │ └── config.yaml ├── pages/ # Page Object 层 │ ├── login_page.py │ └── order_page.py ├── testcases/ # 测试用例层 │ ├── test_login.py │ └── test_order.py ├── utils/ # 工具层截图、日志、等待封装、SQL 操作等 │ ├── driver.py │ └── wait_utils.py ├── conftest.py # pytest 的全局 fixture 和钩子文件 ├── requirements.txt └── pytest.ini这套结构背后的思路是分层用例层只表达业务场景页面层封装页面交互工具层提供底层能力。每一层各司其职改任何一层都不会引发全盘修改。我建议新人至少把 pages 和 testcases 分开这是避免后期维护灾难的底线。3.2 浏览器驱动管理与 conftest 里的全局逻辑先说浏览器驱动。Selenium 需要浏览器驱动文件如 chromedriver来操作浏览器驱动版本和本机 Chrome 大版本必须匹配否则启动时直接给你报session not created。遇到这个问题去 chrome://version 里看浏览器版本然后去镜像站下载对应的 driver 版本即可。接着是 pytest 里最关键的 conftest.py这里可以定义全局 fixture 和截图钩子。我一般会在 conftest 里做三件事启动浏览器、自动截图失败现场、清理环境。核心代码逻辑类似下面这样import pytest from selenium import webdriver from utils.driver import create_driver pytest.fixture(scopefunction) def driver(): drv create_driver(chrome) drv.maximize_window() yield drv drv.quit() pytest.hookimpl(wrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.failed: # 从 fixture 里取出 driver 实例截图并保存到文件 if driver in item.funcargs: screenshot_path f./reports/screenshots/{item.name}_{int(time.time())}.png item.funcargs[driver].save_screenshot(screenshot_path) # attach 到 Allure 报告 allure.attach.file(screenshot_path, nameitem.name, attachment_typeallure.attachment_type.PNG) return outcome这个截图的逻辑非常重要。没有截图记录的失败用例相当于没跑过——脚本报错了你只能看到一句异常文本完全不知道页面上发生了什么。加上自动截图以后打开 Allure 报告一眼就能看到失败瞬间的界面状态定位问题会快非常多。3.3 用例编写登录与下单场景的完整示例用例编写是整个框架的灵魂。我拿一个典型的“登录后下单”场景来演示两条用例分别测登录成功和登录失败用allure装饰器给内容挂上标题和层级import allure, pytest from pages.login_page import LoginPage from pages.order_page import OrderPage allure.feature(登录功能) class TestLogin: allure.story(正常登录) def test_login_success(self, driver): login_page LoginPage(driver) login_page.open(https://example.com/login) login_page.login(admin, 123456) assert login_page.get_current_username() admin allure.story(密码错误) def test_login_wrong_password(self, driver): login_page LoginPage(driver) login_page.open(https://example.com/login) login_page.login(admin, wrong_pwd) assert 用户名或密码错误 in login_page.get_error_message() allure.feature(下单流程) class TestOrder: allure.story(下单成功) def test_create_order(self, driver): login_page LoginPage(driver) login_page.open(https://example.com/login) login_page.login(admin, 123456) order_page OrderPage(driver) order_page.add_goods_to_cart(蓝牙耳机) order_page.checkout() assert 订单提交成功 in order_page.get_order_result()这里我想特别提一下断言选择的问题。UI 自动化的断言不要做太多“中间状态验证”比如在某一步操作后非要去检查一个不重要的元素文本。因为 UI 测试每多一个断言就多一个不稳定点我们的核心目标是验证这条业务链路能不能走得通不是验证每个局部的样式细节。3.4 测试报告与定时执行Allure 如何让结果一目了然用例写完了总要有输出。Allure 是目前团队接受度最高的测试报告方案它生成的 HTML 报告把每个用例的步骤、日志、截图、耗时都整合在一起展示效果相当直观。它的工作流程也很简单先运行 pytest 生成 JSON 结果文件再用 Allure 命令转换为 HTML 页面核心命令如下pytest testcases/ -s -q --alluredir./reports/allure-results allure generate ./reports/allure-results -o ./reports/allure-report --clean allure open ./reports/allure-report集成到 CI 上时可以只跑前两步然后把生成的 HTML 目录保存为产物文件供下载或者用 nginx 静态托管到内网一台服务器上团队所有成员打开浏览器就能看当天测试结果。我在实际项目里是把构建机上的定时任务设成每天凌晨跑一遍全量回归上午大家上班时点开报告就能知道昨晚的改动有没有破坏主流程。这里提醒一个重要细节尽量在 CI 上使用无头模式headless运行避免占用窗口资源、避免沙箱权限问题。Selenium 4 里开启 headless 很简单options webdriver.ChromeOptions() options.add_argument(--headless) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage)最后这个参数--disable-dev-shm-usage是很多 Linux CI 机器的隐藏坑不加的话 Chrome 在资源受限容器里非常容易闪退。4. 接口自动化与 UI 自动化的分工协同非常多团队在烧了一轮 UI 自动化之后都会幡然醒悟所有数据校验、批量查询、异常返回码类验证用 UI 来做就是杀鸡用牛刀。于是接口自动化成了整个测试体系里性价比最高的部分。4.1 为什么我建议先做接口层再做 UI 层接口测试直接面向 API不依赖页面渲染和浏览器环境执行速度是 UI 的十倍甚至百倍而且结果稳定得多。同时接口返回的结构化数据JSON 或 XML非常适合做精确断言——状态码对不对、字段值是否符合预期、关键字段是否存在这些在 UI 层都很难做到。我经历过的项目里最典型的一个优化方式是把 UI 用例中涉及数据准备、状态检查的一部分前置操作从页面点击改为接口调用。比如原来下单用例要一步步走 UI 完成登录和加入购物车改造后直接调登录接口、加入购物车接口达到同状态再让 UI 只负责最后的下单结算步骤验证。这种“UI 测流程、接口测数据”的拆分思路让我们的回归时间从 1 小时缩到 15 分钟而且用例稳定性明显提升。4.2 用 requests 写接口用例的核心套路Python 生态里最常用的接口测试库就是 requests结合 pytest 参数化可以快速覆盖多条数据组合。我用登录接口做个简单示例import requests import pytest def test_login_success(): resp requests.post( https://api.example.com/login, json{username: admin, password: 123456} ) assert resp.status_code 200 data resp.json() assert data[code] 0 assert data[data][token] ! pytest.mark.parametrize(pwd,expected, [ (wrong, 1001), # 错误密码返回业务码 1001 (, 1002), # 密码为空返回业务码 1002 ]) def test_login_wrong_password(pwd, expected): resp requests.post( https://api.example.com/login, json{username: admin, password: pwd} ) assert resp.json()[code] expected接口断言有三个层面第一层是 HTTP 状态码常见的 200、400、401第二层是业务状态码比如 code0 表示成功非 0 是业务失败第三层是数据内容本身字段值、数量、嵌套结构。很多新人只断言第一层这会导致“接口返回 200 但业务逻辑是错的”这种严重漏测。我的经验是第二层和第三层必须跟上。4.3 登录态复用与数据预置接口和 UI 的衔接技巧接口自动化和 UI 自动化不是两条互不相干的线它们之间有非常关键的衔接点登录态复用。UI 脚本每次跑都要重新走一遍登录流程浪费时间又容易受验证码影响。一个高效的做法是先用 requests 调登录接口拿到 token 或 cookie再把它注入到 Selenium 的浏览器会话里这样 UI 用例直接从已登录状态开始。Selenium 注入 cookie 的实现思路大致是import requests from selenium import webdriver # 1. 接口登录获取 cookie session requests.Session() session.post(https://api.example.com/login, json{username: admin, password: 123456}) cookie_value session.cookies.get(sessionid) # 2. 打开目标域名后注入 cookie driver webdriver.Chrome() driver.get(https://example.com) driver.add_cookie({name: sessionid, value: cookie_value, domain: example.com}) # 3. 再刷新页面就处于登录态了 driver.refresh()注意driver.add_cookie之前必须先访问一次目标域名否则浏览器不认识这个 cookie 属于谁。这是我实际踩过的坑第一次写的时候因为少了先打开页面的步骤明明 cookie 内容是对的注入后偏偏就是 302 跳回登录页排查了半天。这个技巧能把整个 UI 用例集的执行时间缩短 30% 以上。5. 常见问题与排查技巧实录Web 自动化测试是一门实践学科大量知识藏在具体报错里。我把这几年最常见的问题按出现频率排了一个序做成速查表方便大家直接对症下药问题现象根本原因推荐解法找不到元素报 NoSuchElementException元素未加载完成 / 定位器写错 / iframe 未切换先切 iframe再配合显式等待最后检查定位器是否在浏览器里能查到唯一节点切换到 iframe 后元素定位失败iframe 是动态加载的切完才加载出子页面等待 iframe 可见后再 switch_to.frame断言 iframe 内元素存在后再操作点击无效但无报错元素被遮罩层遮挡 / 按钮处于 disabled 状态改用 element_to_be_clickable检查页面是否有弹窗遮挡元素刚找到却报 StaleElementReferenceException页面发生刷新或 DOM 结构变化旧元素引用已失效用新的定位器重新获取元素避免跨页面保存元素引用本地能跑、CI 上失败率高headless 模式下渲染或时序不一样加大显式等待并避免依赖屏幕尺寸减少固定 sleep测试数据重复导致用例下次必挂用例使用了硬编码唯一性数据引入时间戳/随机数生成唯一数据做好 teardown 清理大量用例同时跑时浏览器内存飙升没有及时 quit 浏览器或没做进程回收每个用例正确走 fixture 的 teardowndriver.quit() 必须执行验证码挡路登录接口或 UI 页面需要人机校验测试环境预留万能验证码或由管理员关闭验证码开关不要让脚本去破解这里面最值得展开的是StaleElementReferenceException。很多人第一次碰见这个异常会觉得莫名其妙我明明刚找到元素为什么点击就说元素失效原因其实是你找到元素之后页面又发生了一次刷新刷新前的 DOM 引用已经不存在了。解决办法就是不要在整个用例生命周期里保存旧元素引用每次操作之前重新定位一次。把定位和操作写进一个方法里统一处理能最大限度减少这种异常的出现概率。另外关于 iframe我想多说一句。凡是测第三方嵌入页面支付收银台、地图选点、社交分享这类几乎一定会碰到 iframe。所以封装一个切换 iframe 的工具函数非常有必要from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def switch_to_iframe(driver, iframe_locator, timeout10): frame WebDriverWait(driver, timeout).until( EC.presence_of_element_located(iframe_locator) ) driver.switch_to.frame(frame)用完之后还要记得如果后续操作回到主页面务必调用driver.switch_to.default_content()退回顶层否则你会遇到一个诡异的现象明明定位器是对的Selenium 就是找不到任何元素抱怨页面空白。这些都是浪费过我一整个下午排查时间的坑写出来希望大家能少走一点弯路。6. 从自动化脚本到自动化测试平台和 AI 辅助聊到这儿基础框架已经成型了但我还想把视野稍微拉高一点。现在很多公司已经不满足于“能跑脚本”而是开始建设自动化测试平台甚至把 AI 能力引入脚本生成。这块内容比较前沿我结合自己的实践经验谈谈理解和方向。6.1 一个自动化测试平台应该具备哪些能力我理解一个真正能支撑团队持续使用的自动化测试平台至少要包含六个核心模块用例管理用树形结构管理项目和模块支持用例关键字、优先级、标签筛选。执行调度支持手动触发、定时触发、接口触发执行机可以是本机也可以是远程节点。报告展示趋势图、失败率、耗时统计、失败截图挂载。数据管理测试数据源维护、全局变量、环境切换配置。权限控制不同角色有不同的读写权限避免有人误删用例。告警通知用例失败后自动把结果发到群聊或邮件里。平台化最大的价值不是节省代码编写时间而是让手工测试同事也能参与维护用例把自动化从“测试开发专属玩具”变成“整个团队的地基”。我见过很多团队强推平台失败原因都是平台太重、使用门槛太高最后成为摆设。我的建议是先从最薄可用版本做起能跑用例、能出报告、能通知结果先把闭环打通再慢慢丰富能力。6.2 大模型和 AI 辅助在自动化测试里的落地路径近两年 AI 辅助生成自动化脚本是个很热的探索方向尤其是用大模型读取测试用例文档后自动生成 UI 脚本、用自然语言描述页面操作然后转成 Playwright 或 Selenium 代码这类尝试。像 Agent Browser、基于 LangChain 构建脚本生成代理这类工具和项目已经能从“想法”走向“能用的 MVP”阶段。以我目前观察可行的落地路径大概是这样的把测试用例文本比如“用户输入正确账号密码后点击登录断言页面跳转到首页”喂给大模型配合我们提前准备好的项目页面元素库一份描述页面操作的 JSON 或 Markdown 文档让模型产出对应的脚本代码块再由人工微调和格式化后落入代码库。这个思路能不能真正省时间瓶颈在于“页面元素描述的质量”和“提示词模板的设计”而不在于模型本身。我个人的建议是不要指望 AI 立刻全自动产出可直接放生产用的完整脚本但可以把 AI 当成一个非常高效的初稿生成器来用。代码返工的成本远低于从空白页开始写第一版的成本。这个模式在熟练测试开发手里提效非常明显但让完全没有代码能力的测试人员直接依赖 AI 出活目前坑还太多需要谨慎。最后回到个人体会Web 自动化测试能不能做好脚本语法只占三分剩下七分在工程习惯和数据思维。你不一定需要朋友圈刷屏的技术但需要扎实掌握“怎么稳定地找到元素”“怎么设计等待策略”“怎么安排测试数据”这些基本功。把这篇文章里提到的细节消化吸收再亲手搭一套哪怕只有 20 条用例的框架跑几轮你对 Web 自动化的理解一定会上一个台阶。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进