首页 / 资讯中心 / 文章详情

从HAR到接口自动化用例:浏览器操作一键生成pytest测试

从HAR到接口自动化用例:浏览器操作一键生成pytest测试 ★ FEATURED ARTICLE
把浏览器里的一次完整操作记录直接变成接口自动化测试用例这件事我前后做了快一年。最开始是纯手工从开发者工具里复制URL然后去写Python脚本一个接口几十分钟版本一迭代又要重新整理。后来注意到HARHTTP Archive文件本身就完整保存了页面运行过程中的所有HTTP交互包括URL、方法、请求头、请求体、响应状态码字段几乎和接口测试用例一一对应。把HAR作为转换源头写一个独立的解析生成工具就能把“手动操作页面”到“自动化回归运行”这个过程从几小时压缩到几分钟。这篇文章适合所有被用例维护成本折磨过的测试开发也适合刚入手接口自动化、想找一条高效路径的新人。下面从转换思路、HAR格式解析、脚本实现到坑位排查完整拆开讲。1. HAR转换的整体思路先想清楚再动手1.1 手工整理接口用例的痛点在哪接口数量一旦上去手工整理用例会遇到三类问题。第一是重复劳动版本迭代后接口路径、字段、鉴权方式大概率有变化人工更新几十条甚至上百条脚本非常损耗耐心。第二是信息丢失手工复制请求时很容易漏掉一个请求头、一个cookie参数漏掉任何一个都可能导致鉴权失败排错又得半天。第三是不一致不同人整理的用例风格差异很大有人喜欢把参数全部拼进URL有人喜欢单独维护配置文件交接成本居高不下。我后来坚持用“录制HAR再批量生成”这条路核心原因不是省那点打字时间而是把“人工漏信息”的概率强行降下来。HAR是浏览器开发者工具按统一规范记录的请求结构完整保留每个字段都有据可查转换出来的用例从源头就比手写更贴近真实请求。1.2 最小可行链路拆成四步一条完整的链路其实只有四步录制、过滤、生成、运行。录制阶段在开发者工具网络面板里打开预设日志保留手动走一遍核心业务路径把所有请求另存为har文件。过滤阶段用脚本把HAR里跟接口测试无关的静态请求、三方埋点请求、预检请求全部清理掉。生成阶段按预先定义好的模板把剩下的请求解析成可运行的pytest用例动态参数和接口依赖做占位处理。运行阶段把生成文件挂到现有测试框架上统一跑回归并输出报告。这里最容易忽略的就是过滤。HAR是一个原始会话记录里面可能混着几百个静态资源请求不去过滤的话生成出来的用例会有一堆“拿一张图片”“拿一个CSS文件”这种毫无意义的步骤既拖慢执行又干扰定位。过滤规则怎么定直接决定这套转换工具的可用性。1.3 生成策略代码文件还是数据驱动动手写脚本前要先决定输出什么格式。我对比过两条路线。一条是直接生成独立的pytest代码文件优点是单个用例可读性强团队成员可以直接修改缺点是如果接口调整后重跑生成可能会覆盖人工改动需要依赖git对比才能看清变化。另一条是只生成描述用例的JSON或YAML数据文件再写一个统一Runner去读取执行优点是变更集中在数据层不会有人到处打补丁缺点是学习者上手时看不到具体代码遇到复杂依赖链路也不够直观。我的建议是如果团队更看重可读性选第一条如果用例规模大、希望集中管理接口变更第二条更省心。我最终选了第一条因为当时所在的老测试项目里同事更习惯直接看代码遇到问题也能直接打开文件调试。2. 先读懂HAR文件再写解析代码2.1 HAR的基本骨架并不复杂HAR本质是一个JSON文件最外层是log对象里面包含version、creator、pages和entries。转换过程中真正要处理的只有entries它是一个数组每个元素记录一次完整的HTTP事务。一个典型entry包含startedDateTime发起时间、time总耗时、request请求详情、response响应详情、cache缓存信息、timings各阶段耗时。对一个做接口转换的人来说核心全部集中在request和response两个对象里。我习惯把一份很精简的HAR结构贴在脚本注释里方便开发时对照实际文件内容会比下面这段多得多{ log: { version: 1.2, creator: { name: browser-devtools, version: 120.0.0 }, entries: [ { startedDateTime: 2024-11-01T10:00:00.000Z, request: { method: POST, url: https://api.example.com/api/login, headers: [{ name: Content-Type, value: application/json }], queryString: [], cookies: [], postData: { mimeType: application/json, text: {\user\:\demo\} } }, response: { status: 200, statusText: OK, content: { text: {\code\:0,\data\:{}} } } } ] } }眼尖的读者会发现HAR并没有把请求体和响应体单独抽成容易直接使用的字段postData.text和content.text都是原始字符串后续解析时必须根据mimeType决定怎么处理。2.2 request对象里哪些字段真正可用request对象里最常用的字段是method、url、headers、queryString、cookies、postData。headers是一个数组每个元素包含name和value同一个header name可能出现多次。queryString也是一个数组保存URL上的查询参数。postData是一个对象里面mimeType告诉我们请求体是表单还是JSONtext字段保存原始内容。真实转换时我会把headers整体转成字典但有四个header名需要特殊对待。host不要保留requests库会根据URL自动补全保留反而可能发错环境。content-length不要保留请求体如果有任何修改这个值就不准了requests库会自动重算。connection不要保留这是传输层控制信息用例层不需要关心。accept-encoding可以保留也可以删掉requests库默认会做自动解压保留gzip也没有问题只是没有额外收益。很多人容易忽略的是header名大小写。HAR里同一个header可能出现不同写法生成字典时统一归一化成小写再交给requests发送能避免很多奇怪的签名错误。2.3 有效接口的筛选规则怎么定给一个实际案例。我曾在某个后台管理系统的列表页操作中录制过一次HAR原始请求130多条。静态资源请求占了40条埋点请求约25条预检请求20条真正和业务相关的API只有30多条。如果不过滤生成的用例绝大多数跟核心业务无关回归报告里全是噪音。我把筛选规则拆成几类放在可配置的过滤脚本里。静态资源后缀匹配js、css、png、jpg、gif、svg、woff2、ttf、ico等预检请求直接按OPTIONS方法过滤埋点和三方分析域名加入黑名单再叠加一个项目自定义规则比如必须包含/api/路径前缀。import re STATIC_SUFFIX_RE re.compile( r\.(js|css|png|jpe?g|gif|svg|woff2?|ttf|eot|ico|map)(\?.*)?$, re.IGNORECASE ) def should_filter(req): url req[url] if req.get(method) OPTIONS: return True if STATIC_SUFFIX_RE.search(url): return True path url.split(?)[0] if /api/ not in path: return True return False这里有一条血的教训过滤条件一开始不能定得太紧。有的项目接口路径不以/api/开头有的后端网关会透传一些没有路径前缀的接口。我第一次做的时候就因为规则太严漏掉了好几个核心接口之后改成先跑一个统计脚本把HAR里所有请求的路径和方法打印出来人工扫一遍再确认规则。宁可第一次多生成几条无用用例也不要漏掉真正的业务接口。2.4 动态参数识别这一层不做后面全是坑页面运行过程中大量参数是动态生成的最常见的是token、timestamp、request_id、uuid、订单号。如果转换时原样写死第一次执行可能还能过第二次几乎必挂。识别动态参数跟具体项目关系很大我最终分成三类处理。会话级参数整个测试运行生命周期内保持不变比如登录token我放在全局fixture里生成用例时用__TOKEN__占位符替换。接口级参数每个用例执行时重新生成比如时间戳、随机数用__TIMESTAMP__这样的占位符并在用例外层写一个渲染函数。依赖级参数当前用例依赖前一个接口返回的某个字段例如创建订单后拿到的orderId这类必须单独标记不能简单转成静态用例。识别方法说起来也简单录制时对同一条接口多触发几次把每次的请求体导出对比哪个字段每次都在变基本就是动态参数。变更字段记录到dynamic_rules文件里转换脚本扫描到就替换成对应的占位符。3. 转换脚本的实现细节3.1 环境准备和目录结构设计环境依赖非常简单Python 3.9以上即可标准库里的json、re、urllib.parse已经覆盖大部分工作额外需要requests和pytest。如果希望生成YAML版本参数文件再加一个pyyaml。我项目中用到的目录结构是这样har_convert/ ├── 01_har_sources/ # 录制好的HAR原始文件 ├── 02_filtered/ # 中间结果过滤后的请求列表 ├── generated_tests/ # 生成的pytest用例文件 ├── config/ │ ├── dynamic_rules.yaml │ └── filter_rules.yaml ├── har_to_case.py # 转换主脚本 └── conftest_template.py # 公共fixture模板这个结构最大的好处是把原始文件、中间产物和生成结果分开任何时候想重新生成只要把新录制的HAR丢进第一个目录跑一遍脚本即可不用担心把人工手工修改的内容弄乱。3.2 解析与过滤的骨架代码写到这里我想强调一个常见分歧HAR里request.url是完整URL字符串queryString又单独保存了一份结构化参数列表。如果你用requests的时候既传完整URL又同时用params传query参数就会出现二次编码问题。比如URL里的中文参数已经被浏览器编码过requests再编码一次参数就会乱掉。我的做法是生成用例时一律使用完整URL字符串不再单独拆query参数这样得到的就是录制时完全一致的请求地址。下面是解析过滤部分的骨架代码import json def load_har(filepath): with open(filepath, r, encodingutf-8) as f: return json.load(f) def collect_api_entries(har, filter_rules): entries har[log][entries] valid [] for ent in entries: req ent.get(request, {}) if should_filter(req, filter_rules): continue valid.append(ent) return valid def extract_request_info(ent): req ent[request] headers {} for h in req.get(headers, []): name h[name].lower() if name in (host, content-length, connection): continue headers[name] h[value] post_text if req.get(postData): post_text req[postData].get(text, ) return { method: req[method], url: req[url], headers: headers, post_text: post_text, expected_status: ent.get(response, {}).get(status, 200), }这段代码里过滤headers的目的前面已经解释过不再重复。真正需要注意的反而是post_text的处理这里拿到的是字符串并没有解析成字典。原因是表单请求和JSON请求的解析方式不同如果字符串里再嵌套转义字符直接在生成阶段处理容易出错。我选择保留原始字符串在生成的用例里用datapost_text直接发送requests库会自己处理格式。3.3 生成pytest用例时怎么处理细节把请求信息渲染成pytest用例时最核心的是编号函数。我习惯根据URL里的接口路径生成用例名这样出问题时一眼就能判断是哪个接口。生成模板长这样CASE_TEMPLATE \ def {case_name}(client): {remark} url r{url} headers {headers!r} post_text {post_text!r} resp client.request( method{method}, urlurl, headersheaders, datapost_text, timeout10, ) assert resp.status_code {expected_status}, resp.text assert resp.json().get(code) 0 展开之后一个典型的用例是这样的def test_login_from_har(client): /api/login url rhttp://host/api/login headers {content-type: application/json} post_text {username:demo,password:123} resp client.request( methodPOST, urlurl, headersheaders, datapost_text, timeout10, ) assert resp.status_code 200, resp.text assert resp.json().get(code) 0注意这里用的是client.request而不是直接requests.post。这样做的核心原因在后面统一讲简单说就是公共逻辑都可以收敛到一个fixture里生成的用例代码更干净也方便统一加超时、重试和鉴权头。生成用例名时还要注意合法性。接口路径最后一段可能包含斜杠、点号甚至中文需要清洗成合法的Python函数名。我用一个简单的规则只保留字母数字下划线同时加入序号保证唯一性。序号越大越能避免同名冲突代价是函数名变长实际上无伤大雅。3.4 动态占位符与接口依赖的落地方法动态占位符的渲染建议放在生成流程最后一步。先把请求原文按静态数据生成然后扫描用例内容发现__TOKEN__、__TIMESTAMP__这类标记时替换成环境变量读取或调用渲染函数。这样做的目的是把动态逻辑和静态逻辑彻底分开生成器只负责“格式化”运行时才负责“赋值”。接口依赖比占位符更复杂。比如创建订单后需要拿orderId去查询订单详情这种链路无法靠简单扫描自动识别。我的处理办法是用一个链路覆盖配置声明哪些用例之间有关联以及需要从上一步响应里取哪个字段。转换脚本不直接生成完整链路代码而是生成一个setup函数调用骨架函数体由人工维护。例如def setup_create_order_dependency(client, context): resp client.request(...) context[order_id] resp.json()[data][orderId]这种折中方案的思路是让转换脚本批量处理大量简单用例剩下真正需要业务理解的复杂链路人工补充两部分的成本都可控。4. 把生成的用例接入现有测试体系4.1 用client fixture解决公共请求问题一开始我图省事直接在生成的用例里写requests.get但生成几百条用例之后立刻发现公共逻辑重复严重。登录token怎么注入、base_url怎么切换、请求要不要重试这些问题散落到每个用例里改起来非常痛苦。于是我把公共部分收敛进pytest的fixture里约定每个生成用例的第一个参数必须是client。import os import pytest import requests pytest.fixture(scopesession) def client(): session requests.Session() base_url os.getenv(TEST_BASE_URL, http://host) session.headers.update({User-Agent: har-converter/1.0}) token os.getenv(TEST_TOKEN, ) if token: session.headers[Authorization] fBearer {token} def _request(method, url, **kwargs): full_url url if url.startswith(http) else f{base_url}{url} return session.request(method, full_url, **kwargs) session.request _request return session这里有一个值得记录的细节我没有在生成用例里拼接base_url而是利用闭包重写了session.request方法。生成用例里的URL可以是完整地址也可以和base_url拼接规则全部收敛到fixture里。后续如果环境切换只需要改环境变量不用重新生成代码。4.2 全量回归运行和结果判断生成完用例后的执行很简单一条命令pytest generated_tests -q --tbshort我在某一个后端业务项目上实测过一轮原始HAR约190条请求过滤掉静态资源和埋点后剩下63条有效接口转换生成61条用例还有2条是需要处理依赖链路的接口人工补足后整个回归从之前半个工作日缩短到不到10分钟。当然这个过程中踩了不少坑之前提到的动态token是最大的拦路虎后面把所有鉴权头统一放到fixture里通过率一下就稳定了。运行结果判断有一个经验可以分享不要只看用例数量多少还要看用例是否覆盖了核心业务路径。我的做法是把转换生成的用例按接口路径分类和项目中的核心业务清单做一次映射哪些有遗漏及时补而不是等执行完再去看失败列表。5. 常见问题速查与避坑经验5.1 高频问题排查速查表这几类问题几乎每个跑HAR转换方案的人都会遇到我整理成一张速查表方便对照排查。现象最常见原因解决思路用例第一次能跑第二次必失败动态参数被写死用动态规则扫描改成占位符返回HTML而不是JSONbase_url或环境配错token失效检查fixture中的认证头和base_url请求通了但断言失败不是每个接口响应都有code字段把断言改成状态码200加关键字段存在中文内容乱码HAR文件读取时编码不一致读取HAR统一用utf-8响应设置encoding生成用例执行超时依赖的外部服务没有启动fixture中配置重试和合理timeout过滤后仍有大量无关请求埋点域名没加入黑名单打印域名统计逐条补进过滤规则5.2 几条我实际踩出来的经验第一不要把HAR里所有请求头原封不动复制进用例。content-length、host、connection这三个必须处理否则只要环境切换或者请求体稍有改动用例就会莫名失败。requests库在发送时会自动维护这些传输层细节不需要我们操心。第二不要每次手工去改生成出来的用例文件。正确流程是改HAR源文件、改过滤规则、改动态参数规则然后重新生成。如果实在有必须人工调整的逻辑放到一个单独的fixups目录里在生成结束时再注入。这样版本管理非常干净不会出现改完用例重新生成又丢了的尴尬。第三HAR里如果同一个header出现多次生成字典时采用后覆盖前的方式。虽然请求侧很少出现重复但万一遇到先统一转小写再覆盖确保最终字典不会多出无意义的重复项。cookie则建议直接用headers里的cookie字段不要额外解析cookies数组避免两套数据格式不一致导致鉴权失败。第四建议给转换工具加一个对比模式。每次版本迭代后重新录制HAR重新生成用例用git diff看新旧用例差异。哪个接口新增了、哪个删掉了、哪个字段变化了一眼就能看清。这一步比打开代码变更列表更直观能帮你快速锁定接口层的变化对接口级回归非常有价值。我做这个转换工具最大的感受是自动化测试的维护成本大头从来不是写用例本身而是让测试数据跟上真实请求的变化。HAR转换替代不了你对业务的理解但它可以把你从繁琐的复制粘贴里解放出来把精力放到真正需要人工判断的地方比如数据依赖、动态参数策略、断言设计。第一版脚本不需要完美只要能接住自己手头最大的一块重复劳动后面再逐步叠加规则就已经值回投入了。
阅读完成 · 觉得有帮助?
咨询建站