# Python自动化测试报告:如何用飞书群机器人发送精美消息卡片(附完整代码)
每次自动化测试跑完,看着终端里那一堆密密麻麻的日志和最终那个简单的“PASS”或“FAIL”,总觉得少了点什么。测试结果出来了,但团队里的产品、开发、测试同学可能还蒙在鼓里,或者需要手动去翻看冗长的报告链接。信息同步的延迟,往往就是一个小问题演变成线上事故的起点。
我们需要的,是一种更优雅、更主动的信息触达方式。想象一下:每当CI/CD流水线完成一轮自动化测试,相关的项目成员都能在飞书工作群里,立刻收到一张清晰、美观的消息卡片。这张卡片不仅告诉你测试通过了还是失败了,还能一眼看到关键数据——总用例数、通过率、失败详情,甚至直接附上一个点击就能查看详细Allure报告的按钮。这不仅仅是“通知”,而是将测试结果“推送”到团队的协作中心,让质量状态透明化、即时化。
这就是我们今天要深入探讨的主题:如何用Python,将你的自动化测试报告,变成飞书群里一张张会“说话”的精致消息卡片。我会带你从零开始,不仅理解飞书机器人消息卡片的构成逻辑,还会构建一个高度可配置、易于集成的Python工具类,并分享如何将其无缝嵌入到你的Pytest、Jenkins或GitLab CI流程中。让我们告别枯燥的日志,开启测试报告的新体验。
## 1. 理解飞书群机器人与消息卡片:超越简单的文本通知
在开始敲代码之前,我们得先搞清楚,为什么是飞书机器人?以及消息卡片到底强在哪里。如果你还停留在“机器人就是发一段文本”的认知,那可能会错过很多提升效率的机会。
飞书群机器人提供了一个标准的Webhook接口,这意味著任何能发送HTTP POST请求的程序都能与之通信。相比邮件通知容易被淹没,或即时通讯软件里纯文本信息的单薄,飞书消息卡片(Interactive Card)是一种富交互内容格式。它允许你定义标题、颜色模板、内容模块、按钮等元素,以一种结构化的、视觉友好的方式呈现信息。
**消息卡片的核心优势**:
* **信息结构化**:可以将关键数据(如通过数、失败数)以字段(Field)的形式并列展示,一目了然,避免了在长文本中寻找信息的麻烦。
* **视觉引导**:支持使用“绿色”、“红色”、“黄色”等颜色模板作为卡片标题背景,能瞬间传达测试的整体状态(成功、失败、警告)。
* **即时操作**:可以内嵌按钮,直接链接到详细的Allure报告页面、JIRA问题列表或CI构建日志,实现从通知到操作的闭环。
* **提升关注度**:在群聊中,一张设计精良的卡片比一段纯文本更容易引起成员的注意,确保重要信息不被遗漏。
为了更直观地对比,我们来看看不同通知方式的差异:
| 通知方式 | 信息密度 | 可读性 | 交互性 | 集成难度 | 适用场景 |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **控制台日志** | 高 | 低(需主动查看) | 无 | 低 | 开发者本地调试 |
| **纯文本群消息** | 中 | 中 | 无 | 中 | 简单的成功/失败广播 |
| **邮件报告** | 高 | 中 | 弱(点击链接) | 中 | 需要归档的详细报告 |
| **飞书消息卡片** | 高 | **高** | **强(内嵌按钮)** | 中 | **团队实时同步与快速跟进** |
从表格可以看出,消息卡片在信息呈现的友好度和交互性上优势明显。接下来,我们就开始动手,创建属于我们自己的机器人通知工具。
## 2. 搭建基础:创建飞书群机器人并获取Webhook
万事开头难,但这一步其实非常简单。我们所有的自动化推送,都将依赖于一个唯一的密钥——Webhook地址。
**第一步:在飞书中添加群机器人**
1. 打开你需要接收通知的飞书群。
2. 点击群聊天窗口右上角的`···`更多按钮,选择`设置`。
3. 在设置页面,找到`群机器人`选项卡,点击`添加机器人`。
4. 在机器人列表里,选择`自定义机器人`。
5. 为你的机器人起一个名字,比如“自动化测试哨兵”,并上传一个头像(可选,但更有辨识度)。
6. **重要**:在描述中,可以注明“用于自动化测试报告推送”。
7. 点击`添加`,机器人就创建好了。
**第二步:获取并保管Webhook地址**
机器人创建成功后,你会看到一个包含`Webhook`地址的页面。这个地址格式通常如下:
```
https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```
这个地址就是机器人的“门牌号”。**请务必立即复制并妥善保存**,因为出于安全考虑,飞书只会显示这一次。如果你忘记了,只能删除旧机器人重新创建一个。
> 注意:Webhook地址是最高权限凭证,任何人获得它都可以向你的群发送消息。请勿将其提交到公开的代码仓库(如GitHub)。最佳实践是将其作为环境变量或配置中心的密钥来管理。
**第三步:安全设置(可选但推荐)**
在创建机器人时,你可以看到`安全设置`选项。这里有两个重要的功能:
1. **自定义关键词**:机器人只会发送包含至少一个设定关键词的消息。例如,你设置了关键词“测试报告”,那么你的POST请求中必须包含“测试报告”这几个字,消息才会被成功发送。这是一个简单的校验机制。
2. **IP白名单**:你可以配置允许调用此Webhook的服务器IP地址范围。这对于从公司内网的CI服务器(如Jenkins)发起的调用是极佳的安全加固手段。
完成这三步,你的通信信道就准备好了。接下来,让我们用Python来构建发送消息的引擎。
## 3. 核心代码构建:一个健壮且可扩展的Python工具类
直接复制粘贴网上的代码片段或许能快速跑通,但想要在实际项目中稳定、灵活地使用,我们需要一个封装良好、具备错误处理、易于调试的工具类。下面,我将一步步拆解构建过程。
首先,我们需要安装必要的Python库。通常只需要`requests`。
```bash
pip install requests
```
现在,来看我们核心的`FeishuReporter`类。我将在代码中嵌入大量注释,解释每个关键部分的设计考量。
```python
import json
import requests
from datetime import datetime
from typing import Dict, List, Any, Optional
import logging
# 配置日志,便于追踪发送状态和排查问题
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class FeishuReporter:
"""
飞书群机器人测试报告发送工具类。
封装了消息卡片的构建与发送逻辑,支持自定义颜色、字段和操作。
"""
def __init__(self, webhook_url: str, timeout: int = 10):
"""
初始化机器人。
:param webhook_url: 从飞书群机器人设置中获取的Webhook地址
:param timeout: 请求超时时间(秒)
"""
if not webhook_url.startswith('https://open.feishu.cn/open-apis/bot/v2/hook/'):
logger.warning("Webhook URL格式可能与飞书机器人标准格式不符,请检查。")
self.webhook_url = webhook_url
self.timeout = timeout
self.headers = {
'Content-Type': 'application/json; charset=utf-8'
}
def _send_request(self, payload: Dict[str, Any]) -> bool:
"""
内部方法:执行实际的HTTP POST请求。
包含重试机制和详细的错误日志记录。
:param payload: 要发送的JSON数据体
:return: 发送成功返回True,失败返回False
"""
max_retries = 2
for attempt in range(max_retries + 1):
try:
response = requests.post(
url=self.webhook_url,
headers=self.headers,
data=json.dumps(payload, ensure_ascii=False).encode('utf-8'),
timeout=self.timeout
)
response.raise_for_status() # 检查HTTP状态码是否为200
result = response.json()
# 解析飞书机器人的响应
if result.get('code') == 0 or result.get('StatusCode') == 0:
logger.info(f"飞书消息发送成功。消息ID: {result.get('data', {}).get('message_id', 'N/A')}")
return True
else:
logger.error(f"飞书接口返回业务错误。响应: {result}")
# 如果是签名或关键词错误,重试无意义
if result.get('code') in [19001, 19002]:
break
except requests.exceptions.Timeout:
logger.warning(f"请求超时 (尝试 {attempt + 1}/{max_retries + 1})")
if attempt == max_retries:
logger.error("达到最大重试次数,消息发送失败。")
except requests.exceptions.RequestException as e:
logger.error(f"网络请求异常: {e}")
break
except json.JSONDecodeError as e:
logger.error(f"解析飞书响应JSON失败: {e},原始响应: {response.text}")
break
return False
def build_test_report_card(
self,
project_name: str,
total_cases: int,
passed_cases: int,
failed_cases: int,
skipped_cases: int = 0,
report_url: Optional[str] = None,
custom_message: str = "自动化测试执行完成,请相关同学查阅。",
extra_fields: Optional[List[Dict]] = None
) -> Dict[str, Any]:
"""
构建测试报告消息卡片。
这是核心方法,定义了卡片的完整结构。
"""
# 1. 确定卡片标题颜色
if failed_cases > 0:
template_color = "red"
title_emoji = "🔴"
elif skipped_cases > 0:
template_color = "yellow"
title_emoji = "🟡"
else:
template_color = "green"
title_emoji = "🟢"
# 2. 计算通过率
pass_rate = (passed_cases / total_cases * 100) if total_cases > 0 else 0
execute_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 3. 构建基础字段
base_fields = [
self._create_field("项目名称", project_name),
self._create_field("执行时间", execute_time),
self._create_field("用例总数", str(total_cases)),
self._create_field("通过用例", f"{passed_cases}"),
self._create_field("失败用例", f"{failed_cases}", is_alert=(failed_cases>0)),
self._create_field("跳过用例", str(skipped_cases)),
self._create_field("通过率", f"{pass_rate:.2f}%"),
]
# 4. 合并自定义额外字段(例如:执行耗时、测试分支、触发人等)
all_fields = base_fields
if extra_fields:
# 确保自定义字段格式正确
validated_extra = [f for f in extra_fields if isinstance(f, dict) and 'is_short' in f and 'text' in f]
all_fields.extend(validated_extra)
# 5. 构建卡片元素
elements = [
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": custom_message
}
},
{
"tag": "div",
"fields": all_fields
}
]
# 6. 如果有报告链接,添加按钮
if report_url:
elements.append({
"tag": "action",
"actions": [{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "📊 查看详细报告"
},
"url": report_url,
"type": "primary" # 蓝色主按钮
}]
})
# 7. 组装完整卡片
card = {
"config": {
"wide_screen_mode": True # 启用宽屏模式,显示效果更好
},
"header": {
"title": {
"tag": "plain_text",
"content": f"{title_emoji} 自动化测试报告"
},
"template": template_color # 应用颜色模板
},
"elements": elements
}
# 8. 最终的消息体
message = {
"msg_type": "interactive",
"card": card
}
return message
@staticmethod
def _create_field(name: str, value: str, is_short: bool = True, is_alert: bool = False) -> Dict:
"""
辅助方法:快速创建一个字段字典。
:param is_alert: 是否为告警字段,如果是,value可能会用红色等强调
"""
content = f"**{value}**"
if is_alert:
content = f"<font color='red'>{content}</font>"
return {
"is_short": is_short,
"text": {
"tag": "lark_md",
"content": f"**{name}**\n{content}"
}
}
def send_report(
self,
project_name: str,
total_cases: int,
passed_cases: int,
failed_cases: int,
**kwargs # 接收build_test_report_card的其他参数
) -> bool:
"""
一站式方法:构建并发送报告。
这是最常用的接口。
"""
message_payload = self.build_test_report_card(
project_name=project_name,
total_cases=total_cases,
passed_cases=passed_cases,
failed_cases=failed_cases,
**kwargs
)
return self._send_request(message_payload)
```
这个类设计的特点在于:
* **职责清晰**:`build_test_report_card`负责构造数据,`_send_request`负责通信,`send_report`提供快捷入口。
* **健壮性**:包含了网络超时重试、详细的错误日志记录和飞书响应码解析。
* **灵活性**:通过`extra_fields`和`**kwargs`,你可以轻松扩展字段或覆盖默认消息。
* **可读性**:静态方法`_create_field`简化了字段创建,卡片颜色根据失败情况动态决定。
## 4. 实战集成:与主流测试框架和CI/CD工具联动
工具类写好了,但它还是一个孤岛。真正的价值在于将其融入到你的自动化工作流中。下面我分享几种常见的集成模式。
**模式一:与Pytest集成(使用钩子函数)**
这是最直接的方式。我们可以编写一个Pytest插件,或者在`conftest.py`文件中利用`pytest_terminal_summary`钩子,在测试会话结束后收集数据并发送报告。
```python
# conftest.py
import pytest
from your_module import FeishuReporter # 导入上面写的工具类
import os
def pytest_terminal_summary(terminalreporter, exitstatus, config):
"""Pytest测试结束后自动调用的钩子"""
# 1. 从环境变量读取Webhook(安全!)
webhook_url = os.getenv('FEISHU_BOT_WEBHOOK')
if not webhook_url:
print("未设置 FEISHU_BOT_WEBHOOK 环境变量,跳过飞书通知。")
return
# 2. 收集测试统计信息
total = len(terminalreporter.stats.get('passed', [])) + \
len(terminalreporter.stats.get('failed', [])) + \
len(terminalreporter.stats.get('skipped', []))
passed = len(terminalreporter.stats.get('passed', []))
failed = len(terminalreporter.stats.get('failed', []))
skipped = len(terminalreporter.stats.get('skipped', []))
# 3. 获取Allure报告链接(假设由CI环境变量提供)
allure_url = os.getenv('ALLURE_REPORT_URL', '')
# 或者,如果你在本地生成,可以是一个内网穿透的地址
# allure_url = "http://localhost:8080/allure-report"
# 4. 构建自定义字段,例如测试环境、执行机
extra_fields = [
FeishuReporter._create_field("测试环境", os.getenv('TEST_ENV', '本地')),
FeishuReporter._create_field("Python版本", f"{sys.version_info.major}.{sys.version_info.minor}"),
]
# 5. 发送报告
reporter = FeishuReporter(webhook_url)
success = reporter.send_report(
project_name="核心交易服务API测试",
total_cases=total,
passed_cases=passed,
failed_cases=failed,
skipped_cases=skipped,
report_url=allure_url,
extra_fields=extra_fields,
custom_message=f"Pytest测试套件执行完毕,{'存在失败用例,请及时排查!' if failed > 0 else '全部通过。'}"
)
if success:
print("测试报告已成功发送至飞书群。")
```
**模式二:在Jenkins Pipeline中调用**
在Jenkinsfile中,我们通常在post阶段(无论成功失败)调用一个Python脚本发送汇总报告。
```groovy
// Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
environment {
// 在Jenkins凭据管理中存储webhook,这里引用
FEISHU_WEBHOOK = credentials('feishu-bot-webhook')
ALLURE_REPORT_URL = "${BUILD_URL}allure/"
}
stages {
stage('Test') {
steps {
script {
// 运行测试并生成Allure结果
sh 'pytest --alluredir=./allure-results'
}
}
}
stage('Generate Report') {
steps {
script {
// 生成Allure HTML报告
sh 'allure generate ./allure-results -o ./allure-report --clean'
}
}
}
}
post {
always {
script {
// 无论构建结果如何,都发送测试报告通知
sh """
python3 send_feishu_report.py \
--webhook \${FEISHU_WEBHOOK} \
--project "\${JOB_NAME}" \
--build-url "\${BUILD_URL}" \
--report-url "\${ALLURE_REPORT_URL}" \
--allure-results-dir ./allure-results
"""
}
}
}
}
```
对应的`send_feishu_report.py`脚本需要解析Allure结果JSON文件(如`categories.json`, `summary.json`)来获取详细的测试数据,这比Pytest钩子获取的信息更丰富(包括缺陷分类、时间线等)。
**模式三:封装为命令行工具**
提供一个独立的Python脚本,方便在任意地方调用,比如在GitLab CI的`.gitlab-ci.yml`中。
```python
# send_report_cli.py
import argparse
import sys
import os
sys.path.append(os.path.dirname(__file__))
from feishu_reporter import FeishuReporter
def main():
parser = argparse.ArgumentParser(description='发送测试报告到飞书')
parser.add_argument('--webhook', required=True, help='飞书机器人Webhook地址')
parser.add_argument('--project', required=True, help='项目名称')
parser.add_argument('--total', type=int, required=True, help='总用例数')
parser.add_argument('--passed', type=int, required=True, help='通过用例数')
parser.add_argument('--failed', type=int, required=True, help='失败用例数')
parser.add_argument('--skipped', type=int, default=0, help='跳过用例数')
parser.add_argument('--report-url', help='详细报告链接')
parser.add_argument('--message', default='自动化测试执行完成', help='自定义消息')
args = parser.parse_args()
reporter = FeishuReporter(args.webhook)
success = reporter.send_report(
project_name=args.project,
total_cases=args.total,
passed_cases=args.passed,
failed_cases=args.failed,
skipped_cases=args.skipped,
report_url=args.report_url,
custom_message=args.message
)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()
```
然后在GitLab CI中这样使用:
```yaml
# .gitlab-ci.yml
send-feishu-report:
stage: report
script:
- |
python send_report_cli.py \
--webhook $FEISHU_WEBHOOK \
--project "$CI_PROJECT_NAME" \
--total $TOTAL_CASES \
--passed $PASSED_CASES \
--failed $FAILED_CASES \
--report-url "$CI_PAGES_URL/allure-report"
only:
- main
- develop
```
## 5. 高级技巧与避坑指南:让通知更智能、更可靠
掌握了基础集成后,我们可以玩点更花的,让这个自动化报告系统更加强大和贴心。
**技巧一:失败用例详情折叠展示**
当失败用例不多时,直接把失败用例的名称和错误信息展示在卡片里会很实用。我们可以利用飞书卡片的“折叠面板”元素。
```python
# 在 build_test_report_card 方法中,添加逻辑
if failed_cases > 0 and failed_case_details: # failed_case_details 是一个包含失败信息的列表
failure_elements = []
for detail in failed_case_details[:5]: # 最多展示5条,避免卡片过长
failure_elements.append({
"tag": "div",
"text": {
"tag": "lark_md",
"content": f"**{detail['name']}**\n`{detail['error']}`"
}
})
# 将失败详情放入一个note或div中,或者使用折叠面板
elements.insert(1, { # 插入到消息正文之后,统计字段之前
"tag": "note",
"elements": [
{
"tag": "plain_text",
"content": f"最近{len(failure_elements)}条失败详情:"
},
*failure_elements
]
})
```
**技巧二:与监控告警联动**
如果你的测试失败是由于下游服务不稳定引起的,可以将飞书通知与监控系统(如Prometheus AlertManager)的Webhook结合。当监控告警触发时,除了通知运维,也可以让测试机器人发一条消息到测试群,提示“当前测试环境可能不稳定,近期失败率升高可能与XX服务抖动有关”。
**技巧三:@特定人员**
飞书卡片支持`at`元素。你可以在消息中`@`测试负责人或相关开发。但请注意,这需要你知道他们的Open ID,并且机器人需要拥有“获取用户ID”的权限。一种变通方法是,在自定义消息文本中手动输入`@姓名`,飞书通常能正确解析。
**常见问题与避坑指南**:
1. **消息发送失败,返回`19001`或`19021`**:这通常是签名校验失败或Webhook地址错误。请检查:
* 是否在机器人安全设置中开启了“签名校验”?如果开启了,你的请求头必须包含`X-Lark-Signature`和`X-Lark-Request-Timestamp`等字段,计算方式参考飞书文档。
* 是否复制了完整的Webhook地址?中间是否有空格或换行?
2. **消息发送成功但群内不显示**:
* 检查机器人是否被移出群聊。
* 检查是否设置了“自定义关键词”,而你的消息内容里没有包含这个词。
3. **卡片显示错乱或字段不对齐**:
* 检查`fields`中每个字段的`is_short`设置。`is_short: true`的字段会尝试并排显示(一行两个),但受屏幕宽度和内容长度影响,可能不会总是并排。不要过度依赖并排布局。
* 确保所有`content`中的Markdown语法是飞书支持的(`lark_md`)。避免使用过于复杂的嵌套。
4. **安全性问题**:
* **永远不要**将Webhook地址硬编码在代码中或提交到公开仓库。
* 使用环境变量、CI/CD系统的密钥管理功能或专门的密钥管理服务(如HashiCorp Vault)来存储。
* 如果条件允许,务必配置IP白名单。
在我自己的项目中,最初没有加入重试机制,结果有一次因为网络闪断导致测试通过却没发通知,团队误以为测试没跑。加上重试和更详细的日志后,这类问题就再没出现过。另一个经验是,卡片内容并非越多越好。初期我把所有环境信息、执行耗时、测试分支等都塞进去,卡片变得很长,关键信息反而不突出。后来我做了精简,只保留最核心的五六项,把其他信息放到了“查看详细报告”的链接里,卡片的可读性大大提升。