ARTICLE DETAIL

资讯详情

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

Selenium与requests联手:封装POST请求,破解Web自动化数据构造难题

Selenium与requests联手:封装POST请求,破解Web自动化数据构造难题 简介面向熟悉 Web 自动化但需要处理接口异步请求的测试开发工程师这份围绕 Selenium 封装 POST 参数提交的实战资源主要解决如何用 WebDriver 发起 POST 并拿到 JSON 返回值的问题。它重点讲解借助 JavaScript 构造 POST 请求、解析异步响应的方法并补充请求头、Cookie、认证令牌等扩展设置内容涵盖从请求构造到结果解析的完整思路方便用于接口联调和自动化测试。压缩包共 3 个文件包含 Java 示例、文本说明及备份文件包体仅 5KB结构简洁且聚焦关键代码。目前已有 3697 人学习可见该场景在测试开发中具有一定普遍性。通过阅读读者能掌握 Selenium 与 POST 请求结合的基本思路理解异步返回数据的封装与解析流程并能按项目需要调整代码以处理返回非 HTML 等边界情况对测试和开发人员都有直接帮助。 自己平时做Web自动化的时候经常被一个事情卡住有些操作在页面上做起来很费劲比如批量构造测试数据、预填复杂表单、上传文件前先走接口把任务状态置为完成……这些场景如果只靠Selenium一步步点页面又慢又容易翻车。后来我干脆把“提交POST参数”这个动作单独摘出来封装成一个独立模块让Selenium只管页面交互POST请求交给辅助函数去发两者配合着来。这篇文章把我这段时间沉淀的思路、封装代码和一些踩过的坑完整梳理一遍。1. 先搞清楚一件事Selenium本身能不能直接发POST请求1.1 为什么Selenium不擅长“提交参数”Selenium的核心定位是模拟真实用户在浏览器里的操作它的API设计出来就是为了点击、输入、滚动、切换窗口这些交互动作。你可以用driver.get()来发起GET请求但它的实现逻辑是“打开一个新的页面”而不是“发起一次网络请求”。也就是说就算你用get()带一堆参数拼到URL里页面照样会加载一遍资源开销很大而且很多后端接口根本不接受GET传参。至于POST请求Selenium原生压根没有暴露这个能力。你可以通过JavaScript的XMLHttpRequest或fetch去发POST但这样做有几个副作用跨域问题要处理、页面的跳转逻辑会受干扰、返回的数据也不好拿。更麻烦的是Selenium执行JS脚本拿返回值时浏览器和WebDriver之间的通信是有局限的同步等待处理不好就容易拿不到结果。所以我的结论是不要在Selenium内部硬造POST请求正确做法是把POST请求从浏览器上下文里剥离出来用专门的HTTP客户端去发。这个结论听起来简单但很多人一开始都栽在“想用Selenium一个工具解决所有问题”上。1.2 POST和GET的本质差异直接决定封装思路我不打算重复课本上那些“GET有长度限制、POST更安全”的老话就讲封装时真正影响代码设计的差异第一参数位置不同。GET的参数在URL上用?和连接POST的参数在请求体里可以是application/x-www-form-urlencoded、application/json或多部分表单。这就意味着封装时不能让调用方把参数统统塞进一个字典就完事还得让调用方指定Content-Type。第二语义不同。GET是幂等的你发一百次和发一次效果一样POST不是每次提交都可能创建新资源、改状态。做自动化的时候你往同一个接口POST两次数据可能就重复了两份封装里需要提供清晰的调用约定避免误用。第三鉴权方式不同。很多系统的POST接口需要带Session ID、Token或者签名而Selenium打开的页面里已经持有这套凭据。单纯封装一个requests工具类不够还得考虑怎么把浏览器的会话状态搬过来用。2. 方案设计谁来发请求谁来操作页面职责先分清楚2.1 整体思路requests负责接口Selenium负责UI验证我的方案很简单用Python的requests库实现POST请求发送自己写一个HttpClient类做参数管理、响应处理和异常捕捉Selenium负责打开页面、触发操作和验证页面结果。两者之间通过Session和Cookie共享状态这样既能拿POST接口直接造数据又能用Selenium去页面上确认数据真的展示出来了。打个比方Selenium是一个你雇来“点鼠标”的人requests是一个“跑腿寄快递”的人。你不可能让点鼠标的人去跑腿但你可以让跑腿的人带上点鼠标那个人的工牌Cookie这样门卫才放他进系统。这套分工的逻辑其实是把测试拆成“接口级准备 UI级验证”两个阶段。接口级准备解决的是“前置条件构造慢”的问题——你不需要在页面上一步步把数据填完UI级验证解决的是“页面是否真的正常工作”的问题——有些前端逻辑错误光靠接口返回200是发现不了的。两条腿走路效率才高。2.2 Session和Cookie让请求带上浏览器的“身份”这一步是整个封装的关键。你直接写requests.post(url, dataparams)大概率会失败因为后端不认这个陌生请求会踢回登录页或者返回未授权。解决办法是复用Selenium浏览器里的Cookie。Selenium的driver.get_cookies()能拿到当前域名的所有Cookie每一条是个字典包含name、value、domain、path等字段。把它们转换成requests兼容的字典格式塞进requests.Session的Cookie容器里再发POST请求就顺理成章了。我用的代码模式大概是这样的def transfer_cookies(driver, session): for cookie in driver.get_cookies(): session.cookies.set(cookie[name], cookie[value]) return session注意这里的session.cookies.set()带两个参数就够别直接把Selenium返回的原始字典丢给requests因为requests要求的Cookie结构里没有sameSite这类字段传进去会报错。这个坑我在早期封装时踩过后面会专门讲。2.3 为什么选择requests而不是别的方式也许你会问为什么不用urllib或者干脆用Python自带的http.client原因很直接requests的API设计更能承载“封装”这件事。Session自动管理Cookie、超时参数单一明确、响应对象自带json()方法、异常体系清晰。这些特性在做工具类时能省下大量样板代码。但requests也不是万能的它不跑JS、不渲染页面所以我才说它是“辅助腿”不是“主力”。这个定位想清楚了后面代码才不会越写越别扭。3. 动手封装一个可直接复制的POST提交工具类3.1 基础版HttpClient类的核心实现先分享一个最基础的版本。这个类只有三个职责接管Session、设置公共参数、发送POST请求并返回结构化结果。import requests import json from urllib.parse import urljoin class PostClient: def __init__(self, base_url, timeout10): self.base_url base_url self.timeout timeout self.session requests.Session() # 公共请求头大部分系统都认 self.session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), Accept: application/json, text/plain, */*, }) def set_cookie_from_selenium(self, driver): for cookie in driver.get_cookies(): self.session.cookies.set(cookie[name], cookie[value]) def post(self, path, dataNone, json_dataNone, extra_headersNone): url urljoin(self.base_url, path) try: if extra_headers: self.session.headers.update(extra_headers) resp self.session.post( url, datadata, jsonjson_data, timeoutself.timeout ) resp.raise_for_status() result { success: True, status_code: resp.status_code, body: resp.json() if resp.headers.get(content-type, ).find(json) 0 else resp.text, url: resp.url, elapsed: resp.elapsed.total_seconds(), } return result except requests.exceptions.RequestException as e: return { success: False, error: str(e), status_code: None, body: None, }这个类的使用方式分三步client PostClient(base_urlhttps://example.com) client.set_cookie_from_selenium(driver) result client.post(/api/order/create, json_data{sku: A1001, qty: 3})json_data和data分开传是有意设计的data用于表单格式json_data用于JSON格式requests内部会帮你把dict序列化并设置对应的Content-Type。封装的意义就在这里——调用方不用关心HTTP协议细节只传业务参数就行。3.2 进阶版本自动同步Cookie、自动处理Token过期基础版够应付简单项目但真实系统往往会搞Token鉴权Token过期后POST请求就返回401。这时候再手动去页面点一遍登录那封装就失去了意义。我后来在原有基础上加了一个“过期重试”的钩子POST返回401或特定业务码时自动调用Selenium刷新页面、重新拿Cookie、再重发请求。这个逻辑能让用例在一整晚回归测试里少掉一大批偶发失败。class RetryPostClient(PostClient): def post_with_retry(self, driver, path, dataNone, json_dataNone, max_retry2): for attempt in range(max_retry): result self.post(path, datadata, json_datajson_data) if result[success]: return result # 401场景刷新页面重新种Cookie if result.get(status_code) 401: driver.refresh() self.set_cookie_from_selenium(driver) continue return result这段代码的精髓在于“重试一次不是简单再调一次post”而是先把浏览器的会话拉回来保证第二发请求带着有效身份。如果刷新之后还是401那基本可以判定不是Token过期而是后端抽风或接口路径写错了不需要再傻傻重试。3.3 参数处理细节从字典到请求体的转换封装POST参数最容易出问题的就是参数格式。我抓到的实际案例一个接口文档说接收application/json但开发实际写的是表单解析你用json传参对方拿到的是None于是接口直接500。所以封装里我留了一个格式切换的口子def post(self, path, dataNone, json_dataNone): # 如果调用方同时传了data和json_data优先json_data # 但更推荐的做法是在业务回调里显式指定 if json_data is not None: resp self.session.post(url, jsonjson_data, timeoutself.timeout) elif data is not None: resp self.session.post(url, datadata, timeoutself.timeout) else: raise ValueError(至少需要提供data或json_data其中一个)另外还有一个容易被忽视的细节data参数里的值如果是布尔类型requests会把它转成True字符串但有些后端Java或Go框架解析时只认true小写。这里我没在代码里做全局强制转换而是建议业务侧自己处理好。因为不同系统千奇百怪全局转换反而容易引入新问题。4. 真实场景Selenium构造数据页面完成闭环验证4.1 场景描述批量创建订单并验证列表页展示举个真实项目里的例子。当时我做一个电商后台的回归测试每个用例都要预先创建几笔不同状态的订单。最开始的做法是打开“创建订单”页面一步步选商品、填价格、填收货地址、点提交每笔订单耗时将近一分钟整个用例集跑下来光是造数据就占了一半时间。后来我做了改造用POST接口直接创建订单Selenium只负责打开订单列表页验证数据是否正常展示。这样做有两个好处一是造数据从一分钟降到两三秒二是用例的执行顺序更好控制不会再因为前端某个下拉框加载慢导致创建失败。改造后的核心流程大概长这样def create_order_via_api(client, sku, qty, address_id): payload { sku: sku, qty: qty, address_id: address_id, source: auto_test, } result client.post(/api/v2/order/create, json_datapayload) if not result[success]: raise RuntimeError(f订单创建失败: {result[error]}) return result[body][data][order_id] def test_order_list_page(driver, client): client.set_cookie_from_selenium(driver) order_id create_order_via_api(client, A1001, 2, 1021) driver.get(https://example.com/order/list) # 在列表页搜索刚刚创建的订单号 search_box driver.find_element(By.CSS_SELECTOR, input.search-input) search_box.send_keys(order_id) search_box.send_keys(Keys.ENTER) time.sleep(2) assert driver.find_element(By.CSS_SELECTOR, .order-card).is_displayed()你看整个流程里Selenium没有参与创建动作它只负责“最后一步验证”。接口创建完数据页面上立刻能看到前端渲染逻辑有没有问题一目了然。这才是两者配合的正确姿势。4.2 解决“POST后页面状态没更新”的问题这个算是高频问题了。POST请求成功数据库里数据也在但页面上看不到新数据——问题多半出在前端有缓存策略或者数据列表不是实时刷新而是依赖本地缓存。咱不能说这是前端bug就撂挑子。测试自动化里我的处理方式有两种第一种是页面强制刷新driver.refresh()简单粗暴但有效第二种是用Selenium执行一段JS强制清除局部缓存再重新调用列表的接口数据刷新。大多数情况下driver.refresh()就够了。刷新后如果还看不到那就要看是不是前端路由是SPA单页应用刷新回到了首页而不是停留在列表页。这时候改用driver.get(列表页URL)直接以URL方式进入比单纯refresh更可靠。4.3 多接口组合依赖顺序怎么控有时候创建一笔订单要连续调用好几个接口先创建商品、再创建订单、最后上传凭证。接口之间有依赖关系后一个接口需要前一个接口的返回数据。我的经验是封装时别把业务流程写死在PostClient里而是单独写一个BusinessFlow层专门组合接口调用顺序。PostClient永远只做“发一个请求”这件事业务编排在外面做这样任何一个环节失败排查起来都特别清楚——是接口问题还是数据问题看日志就能定位。5. 常见问题与排查技巧实录5.1 高频问题速查表现象可能原因排查动作POST返回302跳转登录页Cookie没有成功同步打印driver.get_cookies()看是否有登录态字段POST超时接口本身耗时较长或测试环境网络慢调大timeout参数增加到20s/30s返回csrf token mismatch框架强制校验CSRF Token先从页面源码或Cookie中提取Token再放进请求体返回500但页面操作正常参数格式不对后端解析失败对比浏览器Network面板里Payload的格式返回200但业务状态失败数据校验未过或业务字段缺失读取响应体里的code和message字段POST成功后页面列表空白前端缓存或列表刷新机制问题用driver.get(url)强刷进入页面这张表是我把所有遇到的POST相关报错汇总出来的覆盖面挺高。每次有新同学问我“为什么POST不成功”我都是先让他对着这个表自查一轮大部分问题都能解决。5.2 独家避坑Cookie同步时的sameSite字段问题这是我在第一版封装时踩得最深的坑。Seleniumget_cookies()返回的Cookie字典里包含sameSite字段而requests.cookies.set()方法不接受这个字段直接传入会抛异常。我当时用的还是把整个字典直接丢进去的方式结果每次跑到一半就报错。后来的解决方案分两层第一层是把需要的字段提取出来再设置只看name和value第二层是加一个Try-Except保护万一某个Cookie字段格式异常不至于整个请求挂掉。def set_cookie_from_selenium(self, driver): for cookie in driver.get_cookies(): try: self.session.cookies.set(cookie[name], cookie[value]) except Exception: # 个别格式异常的Cookie跳过不影响主流程 continue这个细节看起来小但在大型系统里Cookie数量动辄十几个任何一个格式不对都会拖垮整个用例集。加了保护之后稳定性提升非常明显。5.3 关于“等待”和“异步执行”的提醒另一个高频问题是POST请求发完了但后端是异步处理的接口立刻返回了“受理成功”真正的业务数据要过一会儿才落库。这时候你立刻用Selenium去页面上查询大概率查不到。处理方式是加一个轮询等待在页面验证前主动轮询调用一个查询接口直到业务数据就绪再往下走。def wait_order_ready(client, order_id, timeout30): deadline time.time() timeout while time.time() deadline: result client.post(/api/v2/order/query, json_data{order_id: order_id}) if result[success] and result[body][data][status] READY: return True time.sleep(2) return False这个轮询机制不只适用于当前场景任何“接口先返回、业务后执行”的场景都可以复用。我在封装里把它单独抽成了一个方法传入查询函数和期望状态就完事调用方不需要懂轮询内部怎么实现。6. 进一步扩展这套封装能用到哪里6.1 在自动化测试框架里的定位如果项目用的是pytest或unittest这套PostClient很适合做成fixture或setUp里面的公共依赖。比如pytest里定义了一个clientfixture自动从登录后的Selenium实例里同步Cookie所有测试用例都可以直接注入client使用。pytest.fixture def client(driver): base PostClient(base_urlhttps://example.com) base.set_cookie_from_selenium(driver) return base这样每个用例专注自己的业务断言前置数据全部走接口准备代码量大幅减少维护成本也低。6.2 在爬虫和数据采集里的变体虽然标题说的是测试方向的封装但同样的思路也能用在爬虫场景先用Selenium完成登录拿Cookie再把后续高频的数据提交和查询全部切到requests。Selenium只启动一次后面完全靠requests高效率请求速度和稳定性都比全程用Selenium好得多。这种变体的代码和测试版几乎一样唯一区别是Cookie的获取时机从“每轮测试同步”变成了“登录完成后同步一次”然后Session一直复用。我就是这样把某个耗时五分钟的采集任务压缩到四十秒左右。写在最后的个人体会做这个封装的初衷本来只是偷懒——不想让Selenium去填那些又臭又长的表单但真正做完之后我发现它带来的最大价值其实是让测试用例的结构更清晰了。前置条件用接口准备页面只做用户行为的验证出了问题能一眼看出是接口的问题还是前端的问题再也不会出现“数据没创建成功但报错在页面点击那一步”的糊涂账。如果你现在也在被“Selenium造数据太慢”“POST请求不会发”这类问题困扰我建议你不要在Selenium内部硬找方案直接按照“requests发请求Selenium做验证”这条路去试。先跑通一个最简单场景再把公共功能抽成类等你把Cookie同步、超时处理和格式转换这些都捋顺了会发现这套组合远比想象中好用。本文还有配套的精品资源点击获取
返回列表