有段时间我总在半夜被叫起来查线上问题,打开CI翻到昨晚的自动化测试报告,发现红了一片——不是业务真的崩了,是报告生成完就躺在那里,没人看。从那天起,我给自己定了个小目标:让测试结论主动找人,而不是人去找报告。这篇文章就来说说我用Python把这套自动化测试报告推送链路做完整的实战过程,核心是把pytest跑出来的结果转成飞书群机器人能识别的消息卡片,直接推到工作群里。适合所有做接口自动化、UI自动化的测试开发,也适合想优化团队反馈链路但不想上重型测试平台的人。不需要你有多深的Python基础,只要会写pytest用例,照着思路就能落地。
1. 为什么要把测试报告送到群里?自动化测试闭环缺失的最后一段
1.1 报告躺在CI上,等于没人看
很多团队的自动化测试是这么跑的:代码提交触发流水线,pytest跑完生成一份HTML或者Allure报告,然后传到某个只有测试自己知道的地方。表面上闭环了,实际上反馈链路断了。开发不会主动点开Jenkins看结果,业务更不会,连测试自己都经常因为手头事多忘记检查。
我见过最典型的一次事故:某个核心接口因为一次重构参数名变了,自动化测试当天晚上就红了,但无人感知,直到第二天用户反馈页面报错,大家才开始排查,最后翻报告才发现问题在十几个小时前就被测试捕捉到了。报告生成不等于报告送达,这才是自动化测试最常见的失效方式。
所以我在设计反馈链路时,强制给自己加了一条规则:测试结束后的三分钟内,必须让关键信息出现在工作群里。谁都可以不看CI,但工作群的消息你不会完全不看。与其依赖人主动去翻报告,不如让报告主动走到人面前。
1.2 消息卡片为什么比纯文本更适合测试结论
有人可能会说,用飞书机器人发段纯文本不就行了?我最初也是这么干的,跑完发一行字“接口自动化测试:通过97,失败3”。但用了一段时间发现两个问题。
一是信息密度太低。纯文本只能列数字,失败细节塞进来就是一坨,群里刷屏严重,看的人还得从上往下一行行扒。二是没有视觉重点。全绿和飘红的区别不够醒目,人的潜意识会忽略同质化的消息。
消息卡片解决的就是这个。卡片有颜色模板,失败红色、全绿绿色、有跳过橙色,群里一刷就能看到状态。卡片还有结构化排版,用例总数、通过率、耗时、失败列表各占一个区块,眼睛扫一遍就拿到了关键信息。最重要的是卡片上能放按钮,点击直接跳转完整报告,这个交互纯文本做不到。
从我踩坑的经验来看,消息卡片不是锦上添花,而是自动化测试报告推送的正解。它把一个“消息提醒”升级成了“可交互的报告摘要”。
2. 飞书群机器人的底层机制:Webhook、安全设置与签名
2.1 自定义机器人的能力边界
飞书群机器人本质上是个Webhook。你在群设置里添加一个自定义机器人,飞书会给你一个唯一的URL,格式大概是https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx。你往这个URL发一个POST请求,机器人就把消息发到群里。
这个设计很轻量,但也有很多限制。机器人只能发消息,不能收消息,没有主动对话能力,也不能拉人进群。它能发送的消息类型有限,最常用的就是text纯文本和interactive消息卡片。还有一个容易忽略的点:机器人只能把它被创建的那个群当作目标,你拿到Webhook后往群里发就行,但不能指定发到别的群。
这些边界搞清楚后,你就明白了:飞书群机器人适合做的是一件事——把某个固定流程产出的结论,稳定推送到一个固定群。自动化测试报告推送和这个定位完美契合。
2.2 三种安全配置怎么选
创建自定义机器人时,飞书会要求设置安全配置,有三种:自定义关键词、加签、IP白名单。很多人直接跳过,但我强烈建议至少开一个,否则任何拿到Webhook的人都能往你群里灌消息,这是个真实存在的风险。
| 安全方式 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 自定义关键词 | 消息内容必须包含指定关键词 | 配置简单,不影响请求格式 | 内容受限,卡片里得硬塞关键词 | 只想挡住误触发的场景 |
| 加签 | 请求URL带上时间戳和HMAC签名 | 防伪能力强,无法简单伪造 | 发送端要额外写签名逻辑 | 推荐,通用性最好 |
| IP白名单 | 只有指定IP能发送 | 从源头拦截伪造请求 | CI出口IP可能变动,维护麻烦 | 有固定CI服务器的团队 |
我个人建议用加签,必要的时候再加IP白名单。加签的核心思路是在Webhook地址后面拼两个参数:timestamp和sign,服务端会重新计算签名做比对。后面我在推送类里会给出完整实现。
2.3 签名算法推导与代码实现
加签算法的官方流程不复杂,但细节决定成败。完整的签名生成步骤如下:
- 拿到当前Unix时间戳,单位是秒,转成字符串。
- 用
timestamp + "\n" + secret拼出待签名字符串,secret就是你创建机器人时设置的那个密钥。 - 对待签名字符串做HMAC-SHA256运算。
- 对结果做Base64编码。
- 对Base64结果做URL编码,防止特殊字符干扰参数传递。
对应的Python代码非常短:
import base64 import hashlib import hmac import time import urllib.parse def gen_sign(secret: str) -> tuple[str, str]: timestamp = str(int(time.time())) string_to_sign = f"{timestamp}\n{secret}" hmac_code = hmac.new( string_to_sign.encode("utf-8"), digestmod=hashlib.sha256, ).digest() sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) return timestamp, sign这里我踩过一个很隐蔽的坑:Python的hmac.new()第一个参数是密钥,但飞书官方示例里直接传了string_to_sign,也就是说它把“待签名字符串”当成了密钥,对空消息做签名。这个写法看着不符合常理,但飞书服务端确实是按这个规则来校验的。所以你在网上搜到的各种版本,有的能通有的不能通,原因就在这。建议以飞书开放平台最新官方文档里的示例为准,遇到19021签名错误时,写个小脚本把timestamp、secret、签名结果逐段打印,和官方Demo比对,这是最快的排障路径。
还有一个细节:timestamp必须用当前时间,飞书服务端只允许一定时间范围内的偏移。有些初学的人把timestamp写死或者复用了上一次的值,就会一直签名失败。
3. 从pytest手里“掏”出测试结果:两种主流方案
3.1 方案一:pytest_terminal_summary钩子实时提取
要在测试结束后拿到汇总数据,最直接的做法是写一个conftest.py,在pytest_terminal_summary钩子里提取结果。这个钩子会在pytest收集完所有测试结果、准备打印终端总结时被调用,相当于给了你一个“临门一脚”的机会。
先看完整代码:
# conftest.py import time def pytest_terminal_summary(terminalreporter, exitstatus, config): stats = terminalreporter.stats passed = len(stats.get("passed", [])) failed = len(stats.get("failed", [])) error = len(stats.get("error", [])) skipped = len(stats.get("skipped", [])) xfailed = len(stats.get("xfailed", [])) xpassed = len(stats.get("xpassed", [])) total = passed + failed + error + skipped + xfailed + xpassed duration = round(time.time() - terminalreporter._sessionstarttime, 2) failed_cases = [] for report in stats.get("failed", []) + stats.get("error", []): error_line = str(report.longrepr).splitlines()[-1] if report.longrepr else "unknown" failed_cases.append({ "nodeid": report.nodeid, "error": error_line, }) print("\n[FeishuReporter] collected results") print(f"total={total}, passed={passed}, failed={failed}, " f"error={error}, skipped={skipped}, duration={duration}")这个方案的好处是不依赖任何第三方插件,只要pytest能跑完,数据就能拿到。terminalreporter.stats是一个字典,key是结果类型,value是TestReport对象列表。report.nodeid能定位到具体用例,report.longrepr包含失败时的堆栈和断言信息。
需要注意:report.longrepr在不同pytest版本里可能是字符串,也可能是不可变异常信息对象,直接转成str比较稳妥。我只取了最后一行当摘要,如果你想拿到完整堆栈,存整个字符串也行,但卡片里展示的时候就要做截断处理。
3.2 方案二:解析Allure结果目录
有些团队已经接入了Allure,测试跑完会生成allure-results目录,里面每个用例对应一个*-result.json,包含完整的执行状态、失败原因、步骤信息。这种情况下可以写一个独立脚本,测试结束后解析这些JSON文件,再推送飞书。
import glob import json def parse_allure_results(directory: str = "allure-results") -> dict: passed, failed, broken, skipped = 0, 0, 0, 0 failed_cases = [] for file_path in glob.glob(f"{directory}/*-result.json"): with open(file_path, encoding="utf-8") as fp: data = json.load(fp) status = data.get("status") if status == "passed": passed += 1 elif status == "failed": failed += 1 failed_cases.append({ "nodeid": data.get("fullName", data.get("name", "unknown")), "error": data.get("statusDetails", {}).get("message", "no message"), }) elif status == "broken": broken += 1 failed_cases.append({ "nodeid": data.get("fullName", data.get("name", "unknown")), "error": data.get("statusDetails", {}).get("trace", "no trace"), }) elif status == "skipped": skipped += 1 total = passed + failed + broken + skipped return { "total": total, "passed": passed, "failed": failed, "broken": broken, "skipped": skipped, "failed_cases": failed_cases, }用这个方案有个前提:pytest运行时要生成Allure原始数据,即在命令行加上--alluredir=allure-results。好在它不依赖pytest在同一个进程里执行,你可以在CI的after_script阶段调用独立推送脚本,也可以自己在本地跑完测试后手动执行一次,灵活度更高。
3.3 统一结构:把两种方案合成一个数据模型
两种方案各有优势,我更建议的做法是定义一个统一的数据结构,推送相关的代码只认这个结构,不管数据来自pytest还是Allure。这样以后换测试框架,或者改用其他数据源,推送逻辑完全不用动。
from dataclasses import dataclass, field @dataclass class ReportData: title: str total: int passed: int failed: int error: int skipped: int duration: float failed_cases: list = field(default_factory=list) report_url: str = "" @property def pass_rate(self) -> float: if self.total == 0: return 0.0 return round((self.passed + self.skipped) / self.total * 100, 1)建议至少存这些字段:总用例数、通过数、失败数、错误数、跳过数、耗时、失败用例列表、报告链接。前几项用于卡片上的汇总信息,失败用例列表用于明细展示,报告链接用于跳转完整结果。通过率我习惯把skipped算进去,因为跳过不等于失败,但也说明有用例没跑,如果全绿但是一半都跳过了,这种情况值得注意。
4. 消息卡片组装:如何让一份报告在群里“一眼看懂”
4.1 卡片2.0的结构拆解
飞书消息卡片目前推荐用2.0版本,它和1.0最大的区别是:2.0的卡片JSON需要放在请求体里的card字段下,而1.0是直接把卡片结构放在请求体根上。这个差异我印象太深了,后面踩坑详谈。
一个标准的卡片2.0请求体长这样:
{ "msg_type": "interactive", "card": { "schema": "2.0", "config": { "wide_screen_mode": true }, "header": { "title": { "tag": "plain_text", "content": "接口自动化测试报告 03-27 10:30" }, "template": "red" }, "elements": [] } }header就是卡片顶部那块带背景色的区域,template控制颜色,title是标题文字。elements是卡片的正文部分,按从上到下的顺序渲染,常用的组件有:
div:普通区块,可以放文本或多个字段。hr:分隔线。action:按钮交互区。note:灰色小字备注,适合放时间戳等辅助信息。
卡片的排版核心就是div和fields的组合。fields是自适应的双列布局,可以放两两一组的指标。我用它放用例总数、通过率、耗时、执行时间四个指标,两行排开,视觉上非常清爽。
4.2 状态颜色与失败用例展示策略
卡片模板颜色不要一成不变,一定要跟着测试结果动态切换,这是消息卡片的灵魂。
我的策略是:
| 测试结果 | 模板颜色 | 触发条件 |
|---|---|---|
| 全绿通过 | green | 失败和错误均为0,且跳过数量很少 |
| 有跳过/预期失败 | orange | 成功为主,但有较多跳过或xfailed |
| 有失败或错误 | red | 任意失败或错误存在 |
需要注意,error和failed在pytest里是两种状态,error通常指用例执行过程中出现意外异常,比如fixture抛错了;failed指断言失败。这两者都意味着测试没有通过,展示时应该都算作失败。
失败用例展示还有个细节:如果失败数量很多,比如一百个用例红了三十个,把三十个全部塞进卡片,群里刷出来就是一座山。我的经验是卡片上最多展示前5条,每条包含用例名称和一行错误摘要,后面加一句“还有其他N个失败用例,请查看完整报告”,把完整信息留给报告链接。这样既给了即时信息,又避免了卡片泛滥。
4.3 从测试数据到卡片的组装函数
有了数据结构,组装卡片就是纯拼接逻辑。下面这个函数接收ReportData,返回一个可以直接塞进请求体的卡片字典。
def escape_markdown(text: str) -> str: return text.replace("\\", "\\\\").replace("|", "\\|").replace("\n", " ") def build_report_card(report: ReportData) -> dict: if report.failed + report.error > 0: template = "red" elif report.skipped > 0 or report.passed == 0: template = "orange" else: template = "green" elements = [] summary = { "tag": "div", "text": { "tag": "lark_md", "content": ( f"**运行结果**:通过 {report.passed} / " f"失败 {report.failed} / " f"错误 {report.error} / " f"跳过 {report.skipped}" ), }, } elements.append(summary) elements.append({"tag": "hr"}) fields = [ {"is_short": True, "text": {"tag": "lark_md", "content": f"**用例总数**\n{report.total}"}}, {"is_short": True, "text": {"tag": "lark_md", "content": f"**通过率**\n{report.pass_rate}%"}}, {"is_short": True, "text": {"tag": "lark_md", "content": f"**总耗时**\n{report.duration}s"}}, {"is_short": True, "text": {"tag": "lark_md", "content": f"**执行时间**\n{report.title.split(' ')[-1]}"}}, ] elements.append({"tag": "div", "fields": fields}) if report.failed_cases: elements.append({"tag": "hr"}) lines = [] for case in report.failed_cases[:5]: name = escape_markdown(case["nodeid"]) err = escape_markdown(case["error"]) lines.append(f"1. {name}\n {err}") content = "\n".join(lines) if len(report.failed_cases) > 5: content += f"\n\n还有其他 {len(report.failed_cases) - 5} 个失败用例,请查看完整报告。" elements.append({ "tag": "div", "text": {"tag": "lark_md", "content": f"**失败用例**\n{content}"}, }) if report.report_url: elements.append({"tag": "hr"}) elements.append({ "tag": "action", "actions": [{ "tag": "button", "text": {"tag": "plain_text", "content": "查看完整报告"}, "url": report.report_url, "type": "primary", }], }) return { "schema": "2.0", "config": {"wide_screen_mode": True}, "header": { "title": {"tag": "plain_text", "content": report.title}, "template": template, }, "elements": elements, }escape_markdown这个函数一定要加。我曾经遇到过失败信息里带|管道符,在lark_md语法里被当成了表格分隔符,整个卡片的渲染就错乱了。还有信息里带[和]的,也可能被理解为链接语法。用户输出永远是不可信的,进卡片前统一转义一次,省了很多麻烦。
5. 推送服务的完整封装:签名、重试、异步与日志
5.1 核心发送类实现
组装好卡片后,接下来就是发送。看起来就是一个requests.post,但我在实际项目中反复打磨后发现,发送这个动作本身值得封装成一个独立类,至少在超时、重试、错误处理三个维度做好保障。
import time import hmac import base64 import hashlib import urllib.parse import requests class FeishuReporter: def __init__(self, webhook_url: str, secret: str = ""): self.webhook_url = webhook_url self.secret = secret def _gen_sign(self) -> tuple[str, str]: timestamp = str(int(time.time())) string_to_sign = f"{timestamp}\n{self.secret}" hmac_code = hmac.new( string_to_sign.encode("utf-8"), digestmod=hashlib.sha256, ).digest() sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) return timestamp, sign def _build_url(self) -> str: if not self.secret: return self.webhook_url timestamp, sign = self._gen_sign() separator = "&" if "?" in self.webhook_url else "?" return f"{self.webhook_url}{separator}timestamp={timestamp}&sign={sign}" def send(self, card: dict, timeout: int = 10) -> dict: url = self._build_url() payload = {"msg_type": "interactive", "card": card} resp = requests.post( url, json=payload, headers={"Content-Type": "application/json; charset=utf-8"}, timeout=timeout, ) resp.raise_for_status() result = resp.json() if result.get("code") != 0: raise RuntimeError( f"Feishu API error, code={result.get('code')}, msg={result.get('msg')}" ) return result def send_with_retry(self, card: dict, retries: int = 3) -> dict: last_error = None for attempt in range(1, retries + 1): try: return self.send(card) except Exception as e: last_error = e print(f"[FeishuReporter] attempt {attempt}/{retries} failed: {e}") if attempt < retries: time.sleep(2 ** attempt) raise RuntimeError(f"FeishuReporter send failed after {retries} retries: {last_error}")_gen_sign里的签名逻辑和第2章是一致的。send_with_retry用了指数退避,第一次失败等2秒,第二次等4秒,第三次放弃。我在实际使用中一般设置3次重试就够了,因为飞书接口抖动通常几秒内就能恢复。
这里有个细节:构建URL时我判断了webhook_url里是否已经有?参数,虽然飞书默认的hook地址没有query,但保不齐有人把地址包装了一层代理,加这个判断更稳妥。
5.2 接入pytest钩子与CI流水线
有了FeishuReporter和build_report_card,接入pytest的最后一步就是在conftest.py里串联起来。我在第3章只打印了汇总数据,现在把完整的推送逻辑加上。
# conftest.py import os import time import traceback def pytest_terminal_summary(terminalreporter, exitstatus, config): try: report = collect_report(terminalreporter, exitstatus) webhook = os.environ.get("FEISHU_WEBHOOK") secret = os.environ.get("FEISHU_SECRET", "") if not webhook: print("[FeishuReporter] FEISHU_WEBHOOK not set, skip push") return reporter = FeishuReporter(webhook_url=webhook, secret=secret) card = build_report_card(report) reporter.send_with_retry(card, retries=3) print("[FeishuReporter] report pushed to Feishu successfully") except Exception: traceback.print_exc()Webhook和secret从环境变量读取,不要硬编码到代码里。如果是Jenkins,在构建任务的凭据里配置;如果是GitLab CI,在Settings里的CI/CD Variables里配置;本地调试就用.env文件或者直接在shell里export。把凭据和代码分离,这是基本的工程素养。
GitLab CI的配置也很简单,关键是用after_script处理推送,这样即使测试用例本身有失败,也不会影响推送执行:
test: stage: test script: - pytest --alluredir=allure-results after_script: - python scripts/push_feishu_report.py artifacts: when: always paths: - allure-results如果你的推送逻辑放在conftest.py的hook里,那script阶段跑pytest时就已经推送了,after_script里可以不放。但如果希望推送动作独立、可复用、或者不想让测试进程被网络请求拖慢,就建议做成独立脚本,在after_script里执行。
5.3 稳定性与安全性的隐性要求
推送这个动作看似简单,但它跑在测试链路的末端,如果它不稳定,反而会成为新的噪音。我总结了几个隐性要求。
第一,推送失败绝对不能影响测试退出码。这就是为什么我在conftest.py里用try/except包住整个推送逻辑。你想想这个场景:测试全绿,但推送的时候网络抖了一下,结果CI任务变红了,这是多大的乌龙。
第二,请求超时必须设置。requests默认不设超时,一个挂起的请求可能等上几十秒甚至更久。pytest进程如果卡在推送这一步,整条流水线的时间就被拖长了。设个timeout=10,失败就重试,重试不行就放弃,推送的重要性永远低于测试本身。
第三,重试要有节奏,不能无限重试。我这里用指数退避,最多3次。有的团队喜欢用无限的while True重试,我个人不推荐,飞书接口如果持续异常,说明是它们那边的问题,你重试一百次也没用,不如先把异常记录下来,让值班的人去处理。
第四,从安全角度看,加签后的secret一旦泄露,等于攻击者有了往群里发消息的能力。所以我每次看到有人在代码仓库里提交带secret的Webhook地址,都会提醒他们立刻到飞书后台重置密钥。此外,飞书对每个自定义机器人有发送频率限制,一分钟大概是100条,如果你在重试循环里不加控制,极端情况可能触发频控,返回9499错误,这时候无论怎么重试都没用,只能等冷却时间过去。
6. 我实际踩过的坑:从排查到解决的完整链路
6.1 坑一:19021签名校验失败
第一次接加签的时候,我按网上一个教程写完代码,信心满满地跑,结果飞书返回{"code": 19021, "msg": "sign match fail"},整个人是懵的。
我的排查链路是这样的:先把签名和发送写成独立脚本,加上日志,打印出timestamp、secret、string_to_sign、sign,盯着看没发现异常;然后把同样的参数手动拼到URL里用Postman发了一遍,还是19021;接着怀疑secret有问题,复制的时候可能多带了空格或者换行,又去飞书后台重新拷贝了一次,依然报错;最后我下狠心,把拼接好的timestamp、secret、sign逐项输入到飞书官方的签名校验页面,终于定位到问题——我写的签名公式和官方示例不是同一个规则。
网上有些文章把签名流程写成了“对secret做HMAC加密”,还把timestamp忽略掉了,实际上飞书要求的是对timestamp + "\n" + secret这个整体做签名,一个字符都不能差。排查这个问题的过程让我养成了一个习惯:凡是第三方接口的签名、加密逻辑,一律以官方文档的示例代码为最终基准,不轻信任何二手转载。
6.2 坑二:卡片2.0的结构多包了一层
项目初期我用的还是飞书消息卡片1.0的写法,后来想给卡片增加更多区块,就去飞书开放平台的卡片搭建工具里拖拽配置,复制出来的JSON直接放进请求体,结果返回19024参数错误。
反复看发现,卡片搭建工具默认生成的是卡片2.0结构,而2.0要求在请求体的card字段下多包一层。1.0时期是这样的:
{ "msg_type": "interactive", "header": {...}, "elements": [...] }2.0时期必须是这样的:
{ "msg_type": "interactive", "card": { "schema": "2.0", "config": {...}, "header": {...}, "elements": [...] } }如果直接把2.0的卡片结构平铺在请求体根上,飞书就会报参数错误。这个坑特别容易踩,因为卡片搭建工具复制的JSON已经带了schema: "2.0",但发送时外包一层card这个动作,工具并不会提醒你。
我现在的做法是:不在工具里复制最终JSON,而是把工具当作视觉参考,最终请求体代码自己维护。这样结构始终在自己的掌握中,不会被工具的版本机制坑到。
6.3 坑三:请求超时拖慢pytest退出
这个问题最隐蔽。一开始推送逻辑放在pytest_terminal_summary里,用默认的requests.post,没有设置timeout。某天CI上一个小时的测试跑完了,却在推送这一步卡了四十多秒,整条流水线时间被拉长了很多。
排查链路也简单:看CI日志,发现pytest已经打印完所有结果,进程却没有立刻退出;再一查,是requests.post挂在等待响应,飞书接口偶发慢响应,默认不设超时的requests会一直等。
修复方案就是我在第5章里强调的——所有网络请求都要设超时。加了timeout=10之后,哪怕飞书真的抽风,最多等10秒就放弃了,不会无限拖下去。另外一个相关经验是:如果推送逻辑放在pytest进程里,最好用同步发送而不是开新线程,因为pytest退出时会直接杀线程,异步任务的日志可能还没打印出来就没了。
6.4 坑四:失败信息里的竖线把卡片渲染搞乱了
有一次测试失败的断言信息里带了JSON片段,里面有个|字符,卡片发出去后,那个区块的文字错乱,多出了表格线,整张卡片丑得没法看。
原因是卡片2.0的lark_md文本模块会把|识别成表格分隔符。解决方式我已经在前面写好了:对用户可控的文本做转义,|替换成\|,换行替换成空格。这个坑提醒我,任何从测试代码、接口响应、第三方日志里拿到的东西,进卡片之前都要当成不可信数据对待。
6.5 思考:把推送能力沉淀成团队工具
这套链路跑通之后,我又花了一些时间把推送脚本从测试项目里拆成了一个独立的Python包,参数化webhook、密钥、卡片模板,让不同项目组直接引用。这样做的理由很简单:自动化测试的反馈链路不是某一个人的需求,只要一个团队在跑自动化,就值得拥有统一的报告推送能力。
我见过很多团队测试报告推送都是“每个项目各写一份”,最终就是功能重复、维护困难、有人写3个重试有人写0个重试。不如抽一次公共能力,把签名、发送、重试、错误码处理这些逻辑收敛在一个地方,各项目只需要提供自己的数据。
最后再分享一个小技巧:如果你的团队对失败特别敏感,可以在卡片上的“查看完整报告”按钮下面加一行note,用灰色小字标注“若连续失败请关注XX服务状态页”。我试过之后,群里@测试的人明显少了,因为大家自己就能根据提示去判断是测试环境问题还是代码问题,这大概就是自动化测试反馈链路该有的样子。