你们有没有遇到这种情况:Playwright 把 UI 自动化跑得稳稳的,十几条关键路径用例全绿,CI 那边构建日志刷了一屏,但测试报告躺在工件归档里,压根没人打开。等哪个功能真的回归出问题,开发同学才想起来去翻报告,翻完还要在群里喊一句“谁看一下这个报错”。UI 自动化最尴尬的从来不是跑不动,而是跑完的结果离该看的人太远了。
这次分享的是一套我用下来的思路和完整实现:用 Playwright + Pytest 跑 UI 用例,跑完自动解析测试统计结果,组装成一张结构化的飞书消息卡片,直接推进测试群。推送路径走飞书群自定义机器人的 Webhook,不需要申请企业内部应用,不需要自己维护 Token,成本几乎为零。适合已经能用 Playwright 写用例、但一直没把结果触达做起来的团队,也适合个人项目想快速看到自动化反馈的场景。
1. 折腾这套东西之前,先搞清楚为什么需要推送
1.1 自动化测试跑完了,报告却没人打开
很多做 UI 自动化的同学都有一种错觉:只要用例全绿,质量就守住了。真不是这样。用例跑完只是把“验证”这件事做完了,后面还有“传递”这一步。开发关心的是自己这次改动有没有把主流程搞挂、挂的是哪一条;测试关心的是用例稳定性,是偶发失败还是真 bug;组长关心的是这轮回归整体通过率如何。一台 CI 机器上堆着的 HTML 报告,不会主动跑到这些人面前。
我最早也吃过这个亏。用例写成百上千条,每天晚上定时跑,第二天早上打开邮箱看一封 CI 系统自动发的测试报告邮件。邮件内容就是一长串附件列表,点开 HTML 报告还要等页面加载,用户体验极差。后来干脆不看了,反正邮件标题都是绿的。直到有一次主流程挂了三天没人发现,追责的时候每个人都觉得自己没错,而是“报告发了我没看”。从那之后我意识到,测试报告必须主动送到即时通讯工具里,让人在聊天的时候顺手就看到,不需要打开任何额外页面。
1.2 常见的实现路径,差距在哪儿
想把测试结果推进聊天工具,方案其实不少。我梳理过几条常见路线,各有各的适用场景。
| 方案 | 优点 | 缺点 | 适合谁 |
|---|---|---|---|
| CI 自动发 HTML 报告邮件 | 完整报告、零开发量 | 没人看、附件体验差 | 没有需求的过渡期 |
| 自建可视化测试报表平台 | 结构化、可沉淀历史趋势 | 开发维护成本高 | 大规模测试团队 |
| 钉钉/企业微信/飞书机器人 Webhook | 实时送达、零门槛、群内讨论 | 卡片样式有边界、图片上传受限 | 中小团队和个人 |
| 企业内部应用机器人(开放平台 API) | 功能最全、可交互 | 要申请应用、维护 Token | 有专门基建的团队 |
我最终的选型是飞书群自定义机器人 Webhook,理由很简单:接入成本最低,消息实时性和触达率最高。群是现成的,机器人是群设置里点两下就能建出来的,Webhook 地址就是一串 HTTP 接口,只要会发 POST 请求就能推送。对大多数团队来说,初期要解决的不是“报告平台做得有多完善”,而是“结果能不能立刻被看见”,自定义机器人恰好卡在这个需求点上。
1.3 一条消息从测试到飞书的完整链路
整体链路不算复杂,但每个环节都可以单独替换和扩展。我的实现思路是:
- Pytest 跑完用例,顺手生成 JUnit XML 格式的结果文件。
- 通知脚本读取这个 XML,统计出总数、通过数、失败数、跳过数、耗时,并提取失败用例的类名、方法名、错误信息。
- 根据通过率决定消息卡片的主题颜色和告警级别。
- 组装成飞书消息卡片要求的 JSON 结构,POST 到自定义机器人的 Webhook 地址。
- 飞书群立刻收到卡片,开发在群里直接点链接跳转完整 HTML 报告。
为什么偏偏选 JUnit XML 而不是直接在 Pytest 的 hook 里拿统计数据?因为 XML 是解耦的。XML 文件可以被通知脚本读取,也可以被 CI 平台收集,还可以被其他语言处理。哪怕某天推送模块从 Python 改成 Node.js 写,解析同一个 XML 就行,Pytest 那层完全不用动。追求可维护性的话,这个解耦非常值。
2. 前置准备:把一条最小链路先跑通
2.1 Playwright + Pytest 工程初始化
先说环境。我用的是 Python 生态,所以第一步是建虚拟环境、装依赖。版本上没有太苛刻的要求,我本地用的是 Python 3.11,Playwright 1.40 以上都行。
mkdir ui-report-push cd ui-report-push python -m venv venv source venv/bin/activate pip install playwright pytest pytest-html requests playwright install chromium这里有个细节:playwright install chromium会下载浏览器二进制,体积不小,国内网络下载偶尔会失败。如果公司有镜像源,顺手把 Playwright 的浏览器下载源也配上会省心很多。装完之后写一个最简单的用例验证环境:
from playwright.sync_api import sync_playwright def test_open_homepage(): with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.goto("https://example.com") assert page.title() == "Example Domain" browser.close()直接跑:
pytest tests/ -v --junitxml=reports/junit.xml跑完你会发现 reports 目录下多了一个junit.xml,这个文件就是我们后面所有统计数据的来源。可以先用文本编辑器打开看一眼,后面解析的时候你会需要熟悉它的结构。
2.2 建一个飞书群,拉来自定义机器人
飞书这边的配置更简单,全程在客户端里点。打开一个测试群(或者新建一个专门的自动化测试通知群),按下面的路径操作:
- 点群聊右上角的“设置”图标。
- 进入“群机器人” -> “添加机器人”。
- 选择“自定义机器人”,给它取个名字。
- 安全设置这里要仔细选,我后面细说。
- 创建完成后,复制 Webhook 地址。地址形如
https://open.feishu.cn/open-apis/bot/v2/hook/xxxxx。
安全设置有三种,至少要选一种:
- 自定义关键词:配置一个关键词之后,推送的消息正文必须包含这个词,否则飞书拒绝推送。我一般配“测试报告”,比较贴合场景。
- 加签:飞书会给一个密钥,推送时需要在请求参数中携带用密钥生成的签名。安全性最高,但代码里要多写一段签名逻辑。
- IP 白名单:只允许指定 IP 调用这个 Webhook。适合部署在固定公网 IP 的 CI 服务器上,本地调试会很痛苦。
建议至少在本地阶段用“自定义关键词”,等 CI 环境固定下来再加签或者上 IP 白名单。另外强烈建议:Webhook 地址本质上是密钥,把它存到环境变量里,别提交进 Git 仓库。我自己见过不止一次有人把 Webhook 地址写死在测试代码里然后仓库开源,别人拿到地址就能往你群里发消息。
2.3 用 curl 先发一条文本消息验证连通性
Webhook 地址拿到手,先别急着写代码,用 curl 手动发一条消息,把链路本身先打通。这一步能把配置问题和代码问题分开,省得后面调试时一脸懵。
curl -X POST -H "Content-Type: application/json" \ -d '{"msg_type": "text", "content": {"text": "测试报告:连通性验证"}}' \ https://open.feishu.cn/open-apis/bot/v2/hook/xxxxx如果配置的是自定义关键词,注意正文里一定要包含你设的那个词。我上面写了“测试报告”,就是因为关键词配的是它。请求成功会返回:
{"Status": "success", "StatusCode": 0, "msg": "success"}看到StatusCode: 0就说明飞书已经接受了这条消息,去群里看一眼,文本消息应该已经躺在那了。如果返回的是19001之类的错误码,先检查关键词是否匹配、签名是否携带、IP 是否在白名单里。这一步排查干净了,后面的推送脚本基本不会出大问题。
3. 消息卡片的设计与数据格式化
3.1 看懂飞书消息卡片的 JSON 结构
文本消息只能解决“有通知”,解决不了“信息结构”。全是文字堆在一起的报告,跟 CI 日志的可读性差不了太多。所以我选择了飞书的“消息卡片”,它支持标题、颜色、分区、Markdown 文本、分割线,视觉效果和信息密度比纯文本高一大截。
一个最简卡片的 JSON 长这样:
{ "msg_type": "interactive", "card": { "config": { "wide_screen_mode": true }, "header": { "template": "green", "title": { "tag": "plain_text", "content": "UI 自动化测试报告" } }, "elements": [ { "tag": "div", "text": { "tag": "lark_md", "content": "**通过率:** 100%\n**用例数:** 12" } }, { "tag": "hr" }, { "tag": "note", "elements": [ { "tag": "plain_text", "content": "运行时间: 2025-01-12 10:22:33" } ] } ] } }简单拆解一下:
msg_type固定为interactive,表示这是一张交互式卡片。card.config.wide_screen_mode控制卡片是否宽屏展示,移动端和桌面端观感不同,我习惯设为true。card.header是卡片顶部的标题栏,template控制颜色。常用颜色有green、red、orange、blue、grey,正好对应测试通过、失败、警告、信息、中性这些场景。card.elements是卡片正文区域,元素类型最常用的就三种:div用来放文本,hr是分割线,note是小字注释。div里的lark_md是一种 Markdown 子集,支持加粗、链接、换行,但别指望它能渲染复杂的表格和图片。
这套 JSON 不复杂,但很容易写错。新手最容易犯的错是漏掉elements里某个对象必需的tag字段,或者把note的elements直接写成了字符串。遇到卡片渲染不出来,先拿 JSON 去飞书开放平台的卡片调试工具里过一遍,比自己盯着代码猜快得多。
3.2 从 JUnit XML 里拿测试统计结果
飞书消息只是“壳”,里面装的数据质量才是核心。我选择从 JUnit XML 提取统计信息,因为它被 pytest 原生支持,也是 CI 系统普遍能识别的格式。先看一眼一张典型的junit.xml长什么样:
<testsuites tests="12" failures="1" errors="0" skipped="1" time="23.5"> <testsuite name="pytest" tests="12" failures="1" errors="0" skipped="1" time="23.5"> <testcase classname="tests.test_demo" name="test_open_homepage" time="0.42"/> <testcase classname="tests.test_login" name="test_login_fail" time="3.11"> <failure message="AssertionError: expected login success but got error"> ... </failure> </testcase> <testcase classname="tests.test_cart" name="test_add_to_cart" time="1.02"> <skipped message="not implemented yet"/> </testcase> </testsuite> </testsuites>用 Python 标准库xml.etree.ElementTree解析,连第三方依赖都不用加:
import xml.etree.ElementTree as ET def parse_junit(xml_path): root = ET.parse(xml_path).getroot() total = int(root.get("tests", 0)) failures = int(root.get("failures", 0)) errors = int(root.get("errors", 0)) skipped = int(root.get("skipped", 0)) passed = total - failures - errors - skipped duration = float(root.get("time", 0)) failed_cases = [] for case in root.iter("testcase"): failure_node = case.find("failure") if failure_node is not None: failed_cases.append({ "classname": case.get("classname", ""), "name": case.get("name", ""), "message": (failure_node.get("message") or "").strip()[:120], }) return { "total": total, "passed": passed, "failures": failures, "errors": errors, "skipped": skipped, "duration": duration, "failed_cases": failed_cases, }这个函数一次就把“概览”和“明细”都拿到了。需要注意:failures和errors在 pytest 语义下稍有差别,failures是断言失败,errors是用例执行阶段抛出的异常。但在实际推送时,两者对接收方来说是同一类东西——挂了就是挂了,所以我统计通过数时把两个都扣掉了,推送正文里也会把失败数显示成failures + errors。
3.3 统计数据转成好看的 Markdown
拿到统计结果,接下来就是把它拼成飞书lark_md能渲染的文本。我的习惯是先算通过率,再根据通过率决定显示优先级和颜色。
def build_summary_text(stats): pass_rate = stats["passed"] / stats["total"] * 100 if stats["total"] else 0 lines = [ f"**总计:** {stats['total']} 条用例", f"**通过:** {stats['passed']} 条", f"**失败:** {stats['failures'] + stats['errors']} 条", f"**跳过:** {stats['skipped']} 条", f"**通过率:** {pass_rate:.1f}%", f"**耗时:** {stats['duration']:.1f} 秒", ] return "\n".join(lines)话题性和排版上,我会把“通过率”这种最抓眼球的指标放在显眼位置,失败用例明细单独放一块。如果失败数量多,只把前五条完整列出来,其余的在 note 区域提示“还有 N 条失败未展开,请查看完整报告”。这样卡片信息是收敛的,不会出现一页卡片十几屏刷屏的情况。
关于卡片的颜色,我的映射规则是:通过率 100% 用green,90% 以上用orange,低于 90% 用red。如果本轮用例总数太少(比如少于 5 条),即使全部通过,我也会用blue而不是green,因为样本量不足时的“全绿”没有太多说服力,视觉上不要给团队造成“很稳”的错觉。
4. 核心代码:从测试结果到飞书卡片
4.1 封装飞书 Webhook 客户端
正式写代码之前,我先做了一个飞书推送客户端类,把签名、参数拼接、HTTP 请求这些都封起来。这样主流程里调用就是notifier.send_card(...)一句话的事,代码干净,也方便后面扩展多群推送。
import base64 import hashlib import hmac import time import urllib.parse import requests class FeishuNotifier: def __init__(self, webhook_url, secret=None): self.webhook_url = webhook_url self.secret = secret def _sign(self, timestamp): if not self.secret: return None string_to_sign = f"{timestamp}\n{self.secret}" hmac_code = hmac.new( string_to_sign.encode("utf-8"), digestmod=hashlib.sha256, ).digest() sign = base64.b64encode(hmac_code).decode("utf-8") return urllib.parse.quote_plus(sign) def send(self, payload): params = {} if self.secret: timestamp = str(int(time.time())) params["timestamp"] = timestamp params["sign"] = self._sign(timestamp) resp = requests.post(self.webhook_url, params=params, json=payload, timeout=10) data = resp.json() if data.get("StatusCode") != 0: raise RuntimeError(f"feishu push failed: {data}") return data def send_text(self, text): payload = {"msg_type": "text", "content": {"text": text}} return self.send(payload) def send_card(self, template, title, elements): payload = { "msg_type": "interactive", "card": { "config": {"wide_screen_mode": True}, "header": { "template": template, "title": {"tag": "plain_text", "content": title}, }, "elements": elements, }, } return self.send(payload)其中签名这段要重点说明一下。飞书加签的规则是:取当前 Unix 时间戳(秒级),把时间戳 + "\n" + 密钥作为 HMAC 的 key,对空消息体做 HMAC-SHA256 计算,结果做 Base64 编码,再 URL 编码,最终通过请求参数timestamp和sign传给飞书。很多人在这一步翻车,是因为用了毫秒级时间戳,或者对string_to_sign做了多余的换行或空格。如果只想跑通最小链路,不配加签,secret=None时签名部分整体跳过,请求参数也干干净净。
4.2 搭建通知脚本
客户端类有了,再写一个入口脚本,把解析 JUnit XML、组装卡片、发送这三件事串起来。我用argparse接收命令行参数,这样同一个脚本既能被本地手动调用,也能被 CI 的任务调用。
import argparse from feishu_notify import FeishuNotifier from parse_junit import parse_junit def build_failed_case_text(cases, limit=5): if not cases: return None lines = [f"**失败用例(前 {min(len(cases), limit)} 条):**"] for case in cases[:limit]: lines.append(f"- `{case['classname']}::{case['name']}`") lines.append(f" {case['message']}") if len(cases) > limit: lines.append(f"… 还有 {len(cases) - limit} 条失败未展示") return "\n".join(lines) def main(): parser = argparse.ArgumentParser() parser.add_argument("--xml", required=True, help="JUnit XML 路径") parser.add_argument("--webhook", required=True, help="飞书 Webhook 地址") parser.add_argument("--secret", default=None, help="飞书加签密钥") parser.add_argument("--title", default="UI 自动化测试报告", help="卡片标题") args = parser.parse_args() stats = parse_junit(args.xml) pass_rate = stats["passed"] / stats["total"] * 100 if stats["total"] else 0 if pass_rate == 100 and stats["total"] >= 5: template = "green" elif pass_rate >= 90: template = "orange" else: template = "red" elements = [] elements.append({ "tag": "div", "text": {"tag": "lark_md", "content": build_summary_text(stats)}, }) failed_text = build_failed_case_text(stats["failed_cases"]) if failed_text: elements.append({"tag": "hr"}) elements.append({ "tag": "div", "text": {"tag": "lark_md", "content": failed_text}, }) elements.append({"tag": "hr"}) elements.append({ "tag": "note", "elements": [ {"tag": "plain_text", "content": f"报告生成时间: {time.strftime('%Y-%m-%d %H:%M:%S')}"} ], }) notifier = FeishuNotifier(args.webhook, secret=args.secret) notifier.send_card(template, args.title, elements) print("push done") if __name__ == "__main__": main()运行方式就两条命令:
pytest tests/ -v --junitxml=reports/junit.xml python notify_feishu.py --xml reports/junit.xml --webhook "$FEISHU_WEBHOOK"如果配置了加签:
python notify_feishu.py --xml reports/junit.xml --webhook "$FEISHU_WEBHOOK" --secret "$FEISHU_SECRET"这套脚本的好处是反馈直接:终端会打印push done,飞书群里会立刻出现卡片。排查问题的时候可以把命令拆开跑,先跑 pytest 确认 XML 生成正常,再单独跑通知脚本确认推送正常,两边互不干扰。
4.3 与 Pytest 结合的两种接法
上面这个通知脚本是独立于测试流程的,缺点是每次跑完测试还要记得手动多敲一条命令。更省事的方式是把通知逻辑挂到 Pytest 的钩子里,让用例跑完自动推送。
方式一,在conftest.py里写 hook。Pytest 提供了pytest_sessionfinish,它在整个测试会话结束时触发,正好适合做总结性通知。可以把通知脚本的核心逻辑 import 进来,在 hook 里调用:
# conftest.py import os from notify_feishu import main as notify_main def pytest_sessionfinish(session, exitstatus): xml_path = session.config.option.junitxml if not xml_path: return webhook = os.environ.get("FEISHU_WEBHOOK") if not webhook: return notify_main(["--xml", xml_path, "--webhook", webhook])但这里有个坑:pytest_sessionfinish触发时,--junitxml指定的文件不一定已经落盘写完。实际测试下来,在较新的 Pytest 版本里,pytest_terminal_summary阶段 XML 通常已经生成,而pytest_sessionfinish时机要碰运气。所以我自己更推荐方式二。
方式二,用外部脚本封装整套流程,这也是我最终使用的方案。写一个run_all.sh或者直接在 CI 配置里写两条命令:
set -e pytest tests/ -v --junitxml=reports/junit.xml python notify_feishu.py --xml reports/junit.xml --webhook "$FEISHU_WEBHOOK" --secret "$FEISHU_SECRET"把“跑测试”和“发通知”分开,定位问题思路更清晰。团队如果要求每个人跑完都必须通知,可以把这两条命令包进一个统一的入口脚本;如果只是定时任务跑,那 CI 里写这两条命令就够了。
4.4 失败用例的展示策略
失败用例怎么展示,直接决定这条推送有没有用。只发一个“通过率 80%”,等于没发,大家还是得去翻报告才知道哪挂了。我在卡片里对失败明细做了几个处理:
- 每条失败展示
类名::用例名和错误信息的第一行,信息截断在 120 字符内,避免一张卡片被超长堆栈刷屏。 - 失败超过 5 条时只列前 5 条,重点引导大家看完整报告。
- 报告链接放到卡片结尾的 note 区域,让想深入排查的人有路可走。
这里有个比较遗憾的限制:自定义机器人 Webhook 方式,不能像企业应用机器人那样直接上传图片拿到 image_key 嵌入卡片。也就是说,Playwright 失败时自动截图虽然能生成,但没法直接塞进自定义机器人的卡片里。我的替代方案是,把 Playwright 的截图和 trace.zip 作为 CI 的工件输出,在卡片里给出可点击的链接,开发点开就能看截图和网络请求时间线。如果你们有内部图床或者对象存储,也可以把截图传上去生成公网 URL,再把 URL 拼进卡片的 Markdown 链接里。
另外关于报告链接,如果团队的 CI 是内网环境,链接别人打不开,那推送的价值会大打折扣。可以考虑把报告同步到团队内部有权限访问的页面,或者干脆把 HTML 报告也上传到对象存储生成临时链接。链路完整比什么都重要。
5. 常见问题与避坑记录
5.1 Webhook 状态正常但群里收不到消息
这是我在实际使用中遇到最多的一类问题。HTTP 请求返回成功了,飞书返回StatusCode: 0,但群里就是没有消息。排查方向基本集中在安全设置上:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 返回 success 但群内无消息 | 配置了自定义关键词,但消息正文不包含关键词 | 正文里包含关键词,比如“测试报告” |
| 关键词配了“测试”,消息却发不出去 | 关键词判定有误,或正文被 Markdown 语法分割 | 改用更具体的关键词,如“自动化测试” |
加签配置后报19001 | 签名计算错误或 timestamp 缺失 | 检查签名实现,时间戳用秒 |
| IP 白名单生效后本地调试被拒 | 本地公网 IP 不在白名单 | 先用关键词方式调试,再切白名单 |
还有一个隐藏很深的问题:如果你既配置了关键词,又配置了加签,那么请求参数timestamp和sign会同时失效?不会,签名和关键词是同时校验的。但如果你在某一步改了安全设置,飞书那边的缓存可能不会立刻生效,等个一两分钟再试。
5.2 签名一直校验失败
签名这块我单独拎出来说,因为代码看起来简单,坑却不少。
第一,secret一定是从飞书“加签”配置里复制的那串密钥,不是 Webhook 地址里自带的参数。很多人把两者搞混。
第二,时间戳一定是秒级。Python 的time.time()返回的是浮点秒,要转成整数再转字符串。
第三,string_to_sign的格式是时间戳\n密钥,中间是一个换行符。不要在末尾多一个换行,也不要有多余空格。
第四,最终生成的签名要做 URL 编码。如果你用requests库,把签名放在params里传,requests会自动编码;如果你自己拼 URL,就要手动urllib.parse.quote_plus(sign)。
最后提醒一句,飞书的加签实现是“同一个时间戳只能在一段时间内有效”。如果你的 CI 机器和飞书服务器时间偏差过大,即使签名算法完全正确,也会被拒。CI 容器里系统时间漂移的情况虽然不多见,但真遇到了会非常困惑,记得看一眼服务器时间对不对。
5.3 XML 路径在不同 CI 环境对不上
本地跑得好好的,一上 CI 就报File not found,十有八九是路径问题。CI 的工作目录、容器挂载的路径、pytest--junitxml的相对路径基准,跟本地完全不一样。
我的经验是:通知脚本的--xml参数一定要用绝对路径,或者在 CI 配置里先pwd打印当前目录,再用相对项目根的路径。另外,有条件的话在 CI 里把reports/目录做成固定产物目录,比如$WORKSPACE/reports/junit.xml,让测试命令和通知命令都指向同一个变量。
还有一个很实用的技巧:在 CI 的归档产物配置里把reports/整个目录保留下来,这样不仅通知脚本能读到,开发也能从 CI 的网页上下载完整的 HTML 报告和截图,相当于多了一个人工兜底入口。
5.4 别忘了这些细节
最后整理几条容易被忽略但影响体验的细节:
- Webhook 地址和加签密钥从环境变量读取,别硬编码。推荐用
os.environ.get("FEISHU_WEBHOOK")的方式,脚本里不出现任何敏感字符串。 - 推送频率要克制。定时任务每小时跑一次还好,如果用例每次提交都触发全量回归,群消息会变成噪音,最后大家直接把群静音了。我建议全量回归每天最多一次,PR 级快速验证的失败才单独推送。
- 消息卡片内容不要超过飞书的长度限制。失败用例一多,整个卡片内容很容易超限,被飞书截断或者干脆拒绝。解决方式就是我在前面说的,失败明细只列前几条,剩余引导看完整报告。
- 多群推送很简单。想同时推给测试群和产品群,循环调用
send_card传不同 Webhook 就行。不过产品群建议只推通过率和失败率,别把堆栈错误信息直接甩产品脸上,不体面。
把这套东西搭起来之后,我最深的感受是:测试报告推送这件事,技术含量不高,但对团队协作节奏的改善是实打实的。以前是我追着问开发“你看报告了吗”,现在是跑完不到三秒钟,飞书群弹出卡片,开发自己就会说“这轮挂了,我来看看”。反馈链路短了,问题处理速度自然就快了。
最后再分享一个小技巧:把 Webhook 地址和密钥写进 CI 的 secrets 管理,本地调试用.env文件加载,但.env一定加进.gitignore。我见过太多教训,都是因为密钥泄露导致测试群天天被陌生人刷垃圾消息。如果后续还想再进一步,可以把同一套通知脚本接进每日巡检任务,晚上定时跑一遍全量用例,第二天早上大家打开飞书就能看到昨天的主流程健康状态。测试报告的价值,从来不在于报告本身,而在于看完报告之后有人愿意去修问题。祝你的用例永远全绿,报告永远有人看。