☰
HAR文件一键生成禅道Bug描述与测试用例的自动化方案
2026/10/20 16:43:59 网站建设 项目流程

1. 项目背景:提Bug这件“小事”,为什么这么耗时

做测试的兄弟应该都有过这种体验:发现一个前端问题,第一反应是打开浏览器DevTools的Network面板,刷新页面抓请求,然后手动把那个出错的接口挨个点开,复制Request Headers、Query String Parameters、Response Body,再截图、再整理成禅道Bug描述。一套操作下来,运气好三五分钟,运气不好遇到接口多、参数嵌套深的,十分钟都打不住。更要命的是,同一个问题在禅道里建用例时,又得把这些请求信息重新梳理一遍,填前置条件、填步骤、填预期结果,重复劳动量非常大。

我接手一个跨平台客户端项目之后,每天要处理大量前后端联调问题,Bug单和用例的产出频率高得离谱,手动整理这种“脏活累活”彻底把我惹毛了。后来琢磨出一个方案:利用浏览器导出的HAR文件,写一个自动化Skill,一键解析请求快照,自动生成禅道用例和Bug描述。用下来以后,原来十分钟起步的活,现在十几秒搞定。这篇就是把整个工具的设计思路、实现细节和踩过的坑完整记录下来,给还在手动搬砖的测试同行一个参考。

先说清楚这个方案适合谁:日常用禅道管理用例和Bug、前端问题需要频繁提测提Bug的测试工程师;以及被“同一个问题流两个系统”折磨得不行的QA。如果你对写脚本完全陌生也能看懂,我会把每一步的逻辑和操作都拆开讲;如果你想直接复用到自己的项目里,重点看第3章的代码和配置部分。

这件事的本质,是把测试工程师从“信息搬运工”的角色里解放出来——HAR文件里其实已经记录了出问题那一刻完整的前端行为,我们要做的只是把它转成禅道看得懂的格式化描述。

2. 整体方案与工具选型

2.1 为什么选HAR文件作为数据源

HAR(HTTP Archive)是浏览器DevTools导出的一种JSON格式网络日志,几乎是所有主流浏览器通用的标准。Chrome、Firefox、Edge都能一键导出,里面记录了页面加载过程中的每一个HTTP请求,包括请求头、请求体、响应体、状态码、耗时、发起者等完整信息。

选它做数据源,核心原因有三个。第一是获取成本极低,任何一个测试都会开DevTools,不需要额外装抓包工具就能拿到;第二是信息足够完整,Bug描述里最需要的“某个接口返回了500”“某个字段值不对”这类证据,HAR里天然就有;第三是格式足够规范,JSON结构化数据非常适合程序解析,不用像以前那样对着日志文件写正则硬抠。

我在方案选型的时候也考虑过直接用Charles或Fiddler导出的抓包文件,但HAR的互通性最好,哪怕是开发丢过来一个HAR文件,也能直接喂给这个Skill处理,不需要强绑某一款抓包工具。

2.2 “Skill”这种形态解决什么问题

标题里说的“一键生成Skill”,其实是一个自动化执行单元——输入是一个HAR文件路径,输出是结构化的禅道用例文案和Bug描述文案。我选择做成Skill而不是一个完整网站或者独立客户端,是出于三个实际考量:

  • 贴近测试日常操作链路。测试平时的工作流是“打开页面→发现Bug→导出HAR→提Bug”,Skill可以嵌入到这个链路里,用命令行或小脚本调用,而不是开一个无关的GUI窗口。
  • 技术成本可控。整个核心逻辑就是写一个Python脚本,解析HAR JSON,再套模板生成文案,不需要搭后端、不需要数据库,维护起来非常简单。
  • 兼容性灵活。禅道的版本、字段自定义情况因项目而异,把核心逻辑封装成可替换的模板和配置项,调整成本远低于改一套系统。

2.3 技术栈与核心依赖

我的实现用的是Python 3,依赖库只有两个:json(标准库,解析HAR用)和urllib(标准库,处理禅道接口调用用)。全部用标准库,连pip install都不用执行,这也是为了让其他同事拿去用的时候不被环境问题劝退。

工具整体逻辑分成三层:

层级职责核心模块
数据解析层读取HAR,提取请求和响应关键字段har_parser.py
文本生成层将解析结果按模板生成用例步骤和Bug描述template_builder.py
对接层调用禅道API自动建用例、提Bugzentao_client.py

实际运行过程中不需要三层分开调用,最外层的main入口会把整条链路串起来:用户给一个HAR文件路径,脚本先解析,再自动生成描述,最后可选手动确认后调用禅道API写入。

3. 核心实现细节与关键代码

3.1 HAR解析:从JSON里精准捞出关键请求

拿到HAR文件的第一件事,是把它读进内存并确认结构。HAR最外层是log对象,真正要处理的是log.entries数组,每一个entry对象代表一次HTTP请求,里面包含request和response两个子对象。请求相关的核心字段在request.headers、request.method、request.url、request.postData里;响应相关的在response.status、response.headers、response.content.text里。

如果是前端SSR或单页应用,一个HAR文件里可能会有几十上百个请求,全部生成到用例里显然不现实。按什么规则筛选?我用的是两个维度。

第一个维度是“按状态码过滤”:把response.status >= 400的请求作为重点候选,因为提Bug时最有价值的证据就是出错请求;同时也保留2xx里和当前操作强相关的接口(比如关键提交类接口),这需要配合第二个维度。

第二个维度是“按接口URL关键字过滤”:在配置文件里维护一个关键字列表,比如login、save、submit、order等等,只挑选URL中包含这些关键字的请求。这样能把HAR里几十个静态资源请求直接筛掉,留下真正有价值的业务接口。如果业务接口URL没规律,也可以在配置里改成“按请求方法过滤”,优先关注POST请求。

解析出来的请求信息,需要按固定的数据结构存储,方便后续模板生成。我定义了一个简化的Entry:

def extract_entries(har_path, keyword_filters, status_filter=None): with open(har_path, "r", encoding="utf-8") as f: har = json.load(f) entries = [] for item in har["log"]["entries"]: req = item["request"] resp = item["response"] url = req["url"] if status_filter and resp["status"] not in status_filter: continue if keyword_filters and not any(k in url for k in keyword_filters): continue entry = { "url": url, "method": req["method"], "request_headers": extract_headers(req.get("headers", [])), "request_body": extract_body(req), "response_status": resp["status"], "response_body": extract_body(resp), "response_headers": extract_headers(resp.get("headers", [])), "duration_ms": round(item.get("time", 0), 2) } entries.append(entry) return entries

这里有个容易忽略的细节:HAR里的请求体不一定直接放在postData.text字段里。对JSON格式的POST请求,postData.text通常就是一段JSON字符串;但如果是FormData格式的请求,postData.params里才是真正的内容,text字段往往是空的。如果不处理这种情况,提取出来就是一片空白,用例建出来也是残缺的。所以extract_body这个函数需要做兼容:

def extract_body(obj): post_data = obj.get("postData", {}) if obj.get("postData") else {} if post_data.get("text"): return post_data["text"] if post_data.get("params"): return json.dumps( {p["name"]: p.get("value", "") for p in post_data["params"]}, ensure_ascii=False, indent=2 ) if obj.get("content", {}).get("text"): return obj["content"]["text"] return ""

这套逻辑既处理了请求体,也处理了响应体,因为response的content结构要稍微绕一层content.text。踩过这个坑之后我才意识到,HAR文件虽然格式标准,但不同浏览器导出的细节差异很大,解析代码写得越健壮,后面维护成本越低。

3.2 用例文本生成:让机器帮你写好“操作步骤”和“预期结果”

禅道建用例时的经典痛点是什么?是“前置条件写不详细”和“操作步骤描述太笼统”。一个前端Bug,往往需要把“打开某个页面、输入什么参数、点击哪里”都写清楚,开发才能快速复现。HAR里虽然没有直接的页面操作信息,但请求URL和请求体已经能反推出大部分前置条件。

我的做法是设计一套模板映射:把“请求方法 + URL + 关键参数 + 响应状态”组合成一句完整步骤描述。比如一个POST请求,自动生成的步骤就是:

  1. 打开相关页面,触发请求:POST https://业务域名/api/xx/save
  2. 请求参数如下:{"name":"测试","age":18}
  3. 观察系统返回结果。

预期结果那一栏则根据响应状态码和响应内容自动生成——如果response.status是2xx,预期结果就写“请求成功,页面展示正常返回数据”;如果是4xx或5xx,预期结果就写“系统应友好提示错误,不应影响页面整体可用性”。

模板文件我抽成了独立的JSON,这样不同项目可以自定义话术风格。举个例子:

{ "step_prefix": "打开相关页面,触发请求:", "step_param": "请求参数如下:", "expected_success": "请求成功,返回数据正确展示", "expected_error": "请求失败,系统给出明确错误提示,页面不崩溃" }

实际生成用例文本的核心函数长这样,逻辑非常直白:

def build_case_text(entry, template): lines = [] lines.append(f"{template['step_prefix']}{entry['method']} {entry['url']}") if entry["request_body"]: lines.append(template["step_param"] + entry["request_body"]) if entry["response_status"] >= 400: expected = template["expected_error"] else: expected = template["expected_success"] return "\n".join(lines), expected

你可能会问,自动生成的用例文本会不会太死板?确实,纯模板生成的东西没法百分百还原测试人员脑海里的业务上下文,所以我的方案里还加了“人工确认编辑”环节——脚本生成文本后,会在终端里展示,支持直接修改保存,再决定是否提交到禅道。机器的价值是搞定80%的重复劳动,剩下20%的“业务灵魂”留给测试本人补上。

3.3 禅道API对接:用接口消灭“复制粘贴”

禅道提供了比较完整的REST API,可以在不打开网页的情况下创建用例和Bug。对接时先要确认你所在项目的禅道版本对应的接口路径格式。我这边用的接口路径有两种:创建Bug的接口是POST /api.php/v1/bugs,创建用例的接口是POST /api.php/v1/testcases。两者都需要在请求头里带Token做鉴权,Token从禅道后台的“API权限”页面生成。

这里有一个很关键的社交问题:不是所有测试都有权限去调禅道API。如果你所在项目对API管控很严,可以考虑一个折中方案——脚本生成好结构化文本之后,不直接调API,而是输出一份Markdown格式的内容,测试手动复制进禅道网页。“一键生成”体现在“内容自动生成”上,最后一步手动粘贴的耗时已经可以忽略不计。

如果确认你的账号有API权限,调用方式和常规的HTTP POST没有区别:

def create_zentao_bug(api_url, token, payload): req = urllib.request.Request( api_url, data=json.dumps(payload).encode("utf-8"), headers={ "Content-Type": "application/json", "Token": token }, method="POST" ) with urllib.request.urlopen(req, timeout=10) as resp: return json.loads(resp.read().decode("utf-8"))

HTTP状态码是201代表创建成功,之后禅道页面里就能看到这条Bug。我实际调用时发现一个细节:禅道API不同版本对字段名要求不完全一样,有的版本要求模块ID是module,有的版本要求是moduleId。所以更稳妥的做法是把Payload字段名也做成配置项,而不是写死在代码里。这就是标题里“不受版本限制”的另一个层面——版本兼容问题用配置化解,而不是每次升级都改一遍代码。

3.4 关于“不受版本限制”的设计思考

标题里那句“提Bug不再受版本限制”,拆开看有两层含义。

第一层是禪道接口版本差异。禅道从早年的开源版到后来的专业版、旗舰版,API的字段结构有不少变化。为了不绑定某一个版本,我设计了配置层,把所有可能变化的字段名放在一个配置文件里。用户装好后第一件事是改配置,而不是改代码。

第二层是浏览器HAR的版本差异。Chrome和Firefox导出的HAR虽然都叫HAR,但部分字段的嵌套结构有细微差别。比如Firefox的某些版本里响应内容不在content.text里,而在content._initialText里。解析层里我加了一层兜底逻辑,找不到text就依次尝试_initialText、_base64,都找不到就返回空字符串,保证不会因为解析空值而报错。

这种“能用配置解决的兼容性问题绝不上代码硬编码”的思路,是我在这个项目里体会最深的一点。工具型的脚本最怕的就是绑定单一环境,一旦换了项目、换了禅道部署、换了浏览器,就得重新改代码,那这个工具的生命周期就太短了。

4. 实操全流程:从HAR文件到禅道用例的完整记录

4.1 环境准备与项目目录结构

先别急着写代码,把工程结构搭好。我的目录是这样的:

har-skill/ ├── config.json # 过滤器、模板、禅道接口配置 ├── main.py # 入口脚本 ├── har_parser.py # HAR解析模块 ├── template_builder.py # 用例文本生成模块 ├── zentao_client.py # 可选,禅道API对接模块 └── examples/ └── demo.har # 示例HAR文件

config.json是整套工具的“灵魂”,统一管理关键字过滤、状态码过滤、模板文案、禅道地址和Token。这里贴一份我这个项目的实际配置,字段注释写在旁边方便理解:

{ "har_keyword_filters": ["update", "save", "submit", "login", "order"], "status_filter": [200, 201, 400, 401, 500], "template": { "step_prefix": "触发请求:", "step_param": "请求参数:", "expected_success": "请求成功,数据正常返回", "expected_error": "请求失败,页面给出错误提示,不崩溃", "bug_title_prefix": "[自动生成]" }, "zentao_api": { "base_url": "http://your-zentao-server/api.php/v1", "token": "your-api-token", "bug_create_path": "/bugs", "testcase_create_path": "/testcases", "bug_module_field": "module", "testcase_module_field": "moduleId" } }

config.json里最值得留意的是status_filter,我故意把200和201都放进去了。因为有些项目里成功状态码是201,比如创建型接口,如果你只过滤4xx和5xx,这些成功请求就不会进入候选列表。实际跑下来,把2xx和错误码一起过滤能保证关键业务请求都被覆盖到。

4.2 五种方式拿到HAR文件

HAR文件怎么来取决于你用的浏览器和场景。Chrome用户按F12打开DevTools,切到Network面板,勾选Preserve log,复现Bug后右键任意请求,选Save all as HAR with content。Firefox则是切到“网络”面板,点右上角的齿轮图标里选“保存全部为HAR”。

如果你用的是Electron客户端应用,也可以直接用Chrome DevTools协议远程调试,或者让开发帮忙起一个带 --remote-debugging-port的调试实例。这种情况下导出的HAR内容质量更高,因为连WebSocket请求都有记录。

还有一种场景是别人转发给你一个HAR文件,比如前端同事说他那边复现了一个问题但自己没环境排查。这种不用重新构造,直接把文件路径喂给脚本就行。所以说这个工具不仅适合你自己提Bug,还能处理“背锅式HAR包”,效率提升非常明显。

4.3 命令行一键执行的完整链路

安装好依赖后,直接用命令行方式调用:

python main.py --har examples/demo.har --mode preview

preview模式下,脚本会解析HAR并在终端里展示生成的用例文本和Bug描述,不执行API提交,方便先人工校对。如果想直接推到禅道,把mode换成sync即可:

python main.py --har examples/demo.har --mode sync

执行过程中终端会打印每一步的日志,比如:

[INFO] 解析HAR文件: examples/demo.har [INFO] 共解析到187个请求,筛选出3个关键请求 [INFO] 生成用例文本完成 [INFO] 创建Bug中 ... [INFO] Bug创建成功,ID: 12345,链接: http://your-zentao-server/index.php?m=bug&f=view&bugID=12345

这条链路看起来简单,但实际写好不容易。main.py里面要做的事情不只是一个函数调用,还包括:处理文件不存在的情况、处理HAR文件JSON解析失败的情况、处理禅道接口超时的情况,以及最后统一汇总“这次处理了几个请求、生成了几条用例、创建了几个Bug”。没有这些异常处理和汇总信息,工具用起来会非常飘,出了问题都不知道是哪一步挂的。

4.4 一次实际生成内容的效果展示

拿一个实际场景举例。某次测试中复现了一个积分兑换接口报错的问题,导出的HAR文件里有一个POST请求,URL是https://api.example.com/v1/points/exchange,响应状态码500,响应体返回一大段异常堆栈。脚本配置里关键字过滤包含exchange,所以这个请求被顺利捞出来。

生成的Bug标题是“[自动生成]POST /v1/points/exchange 请求异常,状态码500”,Bug描述包含了请求头关键信息、请求参数JSON、响应体完整内容。生成完这条Bug之后,我只要手动补上“操作时使用的账号”“前置业务数据”这些上下文就能提交,比以前手动复制的流程不知道快了多少。

效率对比我做过统计:手动提10条Bug,平均一条15分钟;用这个工具后,10条Bug总共耗时不到20分钟,其中包括每条Bug人工补上下文的时间。建用例的效率提升更明显,因为用例的步骤描述和预期结果生成后基本不需要大改,填个标题就能入库。

5. 常见问题与踩坑排查实录

5.1 HAR文件解析后请求信息为空

最常遇到的问题是extract_body返回空字符串,排查思路分三步:

  • 确认HAR文件是用浏览器直接导出的完整包,而不是只导出了部分请求的截图或文件夹打包,后者可能缺失响应内容。
  • 确认用的是content.text取值路径。有的浏览器导出的HAR里,响应体的真正内容在content.text里,但有的代理工具生成的HAR会把响应放content._base64里,需要先base64解码才能看到原文。
  • 检查请求是否是OPTIONS预检请求。这类请求通常没有响应体,代码逻辑上要直接跳过,不然会生成一条内容几乎为空的无意义记录。

5.2 禅道API返回401鉴权失败

Token错误或者Token过期是最常见原因。禅道的API Token在后台是可以重置的,重置后旧Token立刻失效,必须同步更新config.json里的配置。

另一种情况是禅道部署在反向代理后面,外部请求的Host和禅道内部配置的域名不一致,导致鉴权中间件拒绝请求。解决方案是在调用API时,请求头里把Referer或Host字段改成禅道实际访问地址。

5.3 不同浏览器导出的HAR字段差异导致解析失败

Firefox导出HAR时,部分版本的请求头信息存储在request.headers里,但格式里没有headerSize字段;Chrome则每个header都有name和value。解析时统一按name和value取值,不要依赖任何浏览器特有的额外字段。如果遇到解析异常,先在浏览器里检查这个字段是否存在于HAR原始JSON里,不要假设浏览器会按标准输出。

我写了一个小工具函数,用于从headers数组中安全地提取指定名称的请求头:

def find_header(headers, target_name): for h in headers: if h.get("name", "").lower() == target_name.lower(): return h.get("value", "") return ""

这个函数在提取Authorization、Content-Type这些关键请求头时非常有用,能避免在模板里硬编码数组下标。能防御的兼容性问题,就不要指望浏览器端给一个绝对统一的结构。

5.4 自动生成的用例文本过长,禅道接口拒绝提交

禅道不同版本对字段长度有限制,比如Bug标题最大255字符,步骤描述可能限制到几千字符。如果生成的描述里有超长响应体,直接塞进去会报数据格式错误。所以在生成描述时我加了一道截断逻辑:

def truncate_content(text, max_len=2000): if len(text) <= max_len: return text return text[:max_len] + "\n...内容过长已截断,完整信息见附件HAR文件"

这段逻辑虽然简单,但救了很多次。提Bug时最烦的就是描述被系统截断,开发看着不完整的信息又来回拷问。加一行“完整信息见附件HAR文件”,等于给Bug描述留了一个逃生通道,既保住了可读性,又让需要完整证据的人有条路可走。

5.5 防御性设计:脚本跑挂了也不会污染禅道数据

我在main.py里加了超时保护和失败重试。调用禅道接口时,如果某个请求超时,按0、2、5秒的间隔最多重试3次;重试仍失败则记录日志并跳过该条,继续处理下一条,而不是整个脚本退出。

因为工具是给测试同行用的,不是所有使用者都了解接口调用失败的各种姿势,所以“一次跑挂不阻塞全部”是底线。批量处理20条HAR记录时,宁可有一条漏掉单独处理,也不希望因为一条失败导致前19条全部白干。

6. 拓展思路:这套方案还能怎么玩出花

核心链路跑通之后,我又发现它不止能用于禅道用例和Bug生成,扩展场景非常多。

第一,可以对接消息通知。脚本生成Bug后,直接调用企业微信或钉钉机器人Webhook,把Bug链接推送到项目群里,开发不用再盯着禅道等更新。这个我试用过,效果很好,领导一看Bug流转速度上来了,节奏瞬间就不一样了。

第二,可以做HAR文件批量回归分析。把回归测试期间抓到的HAR文件批量喂给工具,让脚本输出每个接口的状态码分布、耗时Top N、失败接口列表。这相当于一个轻量级的接口健康度报告,不需要额外运维工具,一个脚本就搞定。对大项目阶段性验证非常有参考价值。

第三,可以结合自动化测试框架。把HAR的上报逻辑集成到现有的UI自动化用例里,一旦用例断言失败,自动导出当时的HAR,并用这个Skill生成Bug草稿。我在某个对接项目中实践过,自动化测试失败信息从“一串堆栈”变成“一条可读性极佳的Bug记录”,反馈链路的效率改善非常明显。

第四,可以做成共享模板资产。每个项目可以维护一套自己的config.json和模板,沉淀“这个项目怎么描述Bug、怎么建用例”的经验。新人进来不用从头学起,拷一份配置就能上手干活。

7. 最后的几点提醒

我个人在实际操作中的体会是:这类自动化工具的成败,八分在“模板和过滤规则的设定”,两分在“代码能不能跑”。很多测试同行拿到代码喜欢直接跑,跑完觉得生成的内容和自己的预期对不上,就放弃了。其实问题多半出在配置没调或者模板话术没对齐项目习惯。所以如果你决定复刻这套方案,头几次使用一定花点时间调config.json,把过滤关键字、状态码范围、模板文案都改成自己项目的样子,后续才能越用越顺。

另外一个小技巧:HAR文件生成后,顺手改个文件名,加上日期和场景描述,比如“20240611-积分兑换异常.har”。这个习惯会让你在批量处理多个HAR文件时少很多麻烦,脚本日志里也能一眼看出这次处理的是哪个版本的数据。我吃过几次“文件名全是demo.har”的亏,后来养成这个习惯之后再也没找错过文件。

最后,不要把自动生成当偷懒借口。测试的价值依然在于发现那些机器捕捉不到的业务逻辑问题、边界条件和体验细节。这个Skill只负责把“你已经发现的问题”用更快的方式记录下来,让时间花在更值得动脑子的测试设计上,这才是它存在的意义。

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

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

立即咨询