pytest+allure+钉钉企微通知:接口自动化测试框架从零搭建实战

发布时间:2026/10/29 7:53:44
pytest+allure+钉钉企微通知:接口自动化测试框架从零搭建实战

简介:一套基于Python的接口自动化测试框架,面向测试开发工程师及需要系统化落地接口测试的团队,整合pytest执行、allure报告、log日志、yaml配置、mysql存储与钉钉企微通知能力,针对用例管理、配置分离、结果反馈等常见问题提供完整方案。压缩包共167个文件,约2.79MB,核心为63个py源码脚本,另含7个yaml配置、4个xlsx测试数据、27个png报告截图,以及pytest配置文件、项目说明文档、覆盖率文件等工程文件,目录结构清晰,便于按模块对照学习。框架贯通用例编写、数据驱动、日志记录、报告生成和即时通知全流程,可修改yaml接入自身环境,借助allure可视化结果快速定位问题,日志按日期归档也方便追溯运行记录。已有1162人学习,适合作为二次开发或搭建企业级接口自动化体系的实用参考。

1. 接口自动化框架怎么选:pytest+allure+通知这套组合的底气在哪

很多从零开始做接口自动化的团队,最纠结的往往不是怎么写用例,而是用例写完没人看、跑完没人信。pytest+allure+log+yaml+mysql+钉钉企微通知这套组合,本质上是把用例执行、数据管理、结果展示和消息推送串成一条流水线:pytest负责跑,allure负责把结果渲染成能直接发给领导的报告,yaml管配置,mysql存测试数据或业务校验数据,log留证据,最后钉钉或企微把结论推到该看的人手里。这套框架的适用场景很明确——中小型测试团队从零搭架子,或者一个人维护多条业务线的接口回归。它不追求大而全的平台化,而是让每个环节都能单独替换:今天用钉钉,明天换企微,只需改一个模块。这篇文章按我实际搭过的路子,从目录骨架讲到通知发送,最后把几处容易让人翻车的细节单独拎出来说透,照着复现即可。

2. 先把最小骨架立起来:环境、目录与pytest和allure的握手

不少新手在这个阶段就栽跟头:pytest装好了,allure也下载了,但一运行就报“allure不是内部或外部命令”;或者报告生成了一半,浏览器打开空白。其实这套组合的姿势没那么复杂,先把环境按顺序装对,目录结构想清楚,再跑通一条最简单的用例,后面的一切才有得谈。

2.1 环境准备:python、pytest、allure的安装顺序与版本坑

我习惯的安装顺序是:先装Python,再装pytest,最后装allure命令行工具。allure需要Java环境,所以JDK也得提前备好。Java版本建议8以上,allure 2.13+对JDK8兼容性最好,JDK11也不会有问题。装allure时别用pip装,那个不是官方工具,而是下载allure-commandline的zip包解压,把bin目录配到PATH里。macOS可以直接brew install allure,Windows用户手动配环境变量。

# 1. 确认python与pip python --version pip --version # 2. 安装pytest与相关插件 pip install pytest pytest-html pytest-rerunfailures pytest-xdist # 3. Windows下解压allure-commandline后配置PATH # 假设解压在 D:\allure\bin setx PATH "%PATH%;D:\allure\bin" # 4. 验证allure allure --version

逻辑说明:这里pytest-xdist用于并行跑用例,pytest-rerunfailures用于失败重试,这两个后面都会用到。setx配置的是用户环境变量,新开终端才会生效,这是不少人困惑的地方——配置完发现当前窗口还是找不到命令,重新打开一个终端就好。

参数说明:pytest-html是备用方案,当你还没搭好allure时先用它出报告应急。allure版本和pytest版本没有强耦合,但pytest版本尽量7.x以上,否则部分插件依赖的钩子接口对不上。

2.2 目录结构:按「配置、用例、数据、报告」四层拆

目录结构决定了后续的维护成本。我搭过不少项目,一开始图省事把所有.py文件堆在根目录,三个月后自己都找不着北。下面这套结构是我多次调整后的标准形态,适合接口自动化项目,不重不轻。

api_framework/ ├── config/ # 配置文件存放目录 │ ├── config.yaml # 环境、账号、mysql等全局配置 │ └── config.py # 读取yaml的封装 ├── common/ # 公共方法:日志、请求封装、数据库操作 │ ├── log.py # 日志模块 │ ├── request_util.py # requests会话封装 │ └── mysql_util.py # mysql连接池封装 ├── data/ # 测试数据 │ └── user.yaml # 接口入参数据(与用例分离) ├── testcases/ # pytest用例 │ ├── conftest.py # 夹具、钩子 │ └── test_user_api.py ├── reports/ # 报告输出目录(allure-results和html) ├── logs/ # 运行日志 └── pytest.ini # pytest配置

逻辑说明:把配置和代码分离是这套框架的核心思想之一。yaml文件放环境地址、数据库连接、账号密码,代码里不出现任何硬编码。需求变动只改yaml,不碰代码。common目录下的工具类都是无状态封装,供所有用例调用。

参数说明:pytest.ini里可以配置testpaths、addopts、标记等,后续会用到。reports和logs目录建议加入.gitignore,因为报告和日志属于生成物,不该进版本库。

2.3 第一个用例与allure报告生成:从pytest到allure的完整链路

环境装好、目录建好后,写第一个接口用例。以登录接口为例,把pytest的断言、allure的标签、日志输出全部先跑通,验证框架能正常出报告。

# testcases/conftest.py import pytest import allure @pytest.fixture(scope="session") def base_url(): """读取全局配置中的环境地址""" return "https://api.example.com" # testcases/test_login.py import allure import pytest import requests @allure.feature("用户模块") @allure.story("登录接口") @allure.title("测试登录接口-正确密码") def test_login_success(base_url): url = f"{base_url}/login" payload = {"username": "testuser", "password": "123456"} resp = requests.post(url, json=payload) assert resp.status_code == 200 assert resp.json().get("code") == 0
# 运行用例并生成allure结果 pytest testcases/test_login.py --alluredir=reports/allure-results # 本地预览报告 allure serve reports/allure-results

逻辑说明:conftest.py里的base_url是session级别的fixture,整个测试会话只初始化一次。allure.feature和story用于报告分组,title自定义用例展示名称。运行命令时,--alluredir把pytest生成的中间结果存到指定目录,allure serve会启动一个本地web服务展示最终报告。

参数说明:allure serve适合本地验证。如果想生成静态报告给其他人看,用allure generate reports/allure-results -o reports/allure-html --clean,然后allure open reports/allure-html。注意:allure-results是中间产物,allure-html才是最终静态站点,两者别混淆。

3. yaml配置与mysql数据层:把测试数据从代码里解放出来

接口自动化最难维护的不是用例逻辑,而是埋得到处都是的测试数据和环境地址。yaml负责解决配置和数据的“外置”,mysql负责解决“查询结果怎么校验”以及“测试数据怎么来”。这一章把这两块做厚,后面写用例就轻松很多。

3.1 yaml文件怎么建:格式要点与读取封装

yaml的写法比json更省眼力,但缩进规则让不少人吃过亏。我见过最典型的错误是Tab缩进——yaml规定只能用空格,而且层级必须一致。另外一个坑是读取yaml时遇到中文编码问题,需要在open时指定utf-8。

# config/config.yaml env: "test" base_url: "https://api.example.com" mysql: host: "127.0.0.1" port: 3306 user: "test_user" password: "Test@123" database: "api_test" charset: "utf8mb4" alarm: dingtalk: webhook: "https://oapi.dingtalk.com/robot/send?access_token=xxxx" secret: "SECxxxx" wecom: webhook: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx"
# config/config.py import yaml import os def load_yaml(path): """读取yaml文件并返回dict""" with open(path, "r", encoding="utf-8") as f: data = yaml.safe_load(f) return data CONFIG = load_yaml(os.path.join(os.path.dirname(__file__), "config.yaml"))

逻辑说明:yaml.safe_load是官方推荐方式,避免load方法执行任意对象构造的安全风险。CONFIG模块级变量在导入时初始化一次,整个测试进程都复用,不用反复读文件。mysql配置和钉钉企微webhook都放同一个yaml里,一处维护。

参数说明:env字段用来区分test、staging、prod环境,后面可以扩展成根据环境变量自动切换配置。钉钉的secret字段在加签模式下必填,企微不需要secret,这个区别后面通知章节细聊。

3.2 mysql连接的封装:单例模式与查询结果处理

接口测试里mysql主要干两件事:一是从库中取测试数据,比如构造一个已存在的用户;二是断言接口返回的字段和数据库一致。连接不能每次都新建,必须做连接池或单例复用。这里我用一个简单的封装,用pymysql实现。

# common/mysql_util.py import pymysql from config.config import CONFIG class MysqlUtil: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._connect() return cls._instance def _connect(self): cfg = CONFIG["mysql"] self.conn = pymysql.connect( host=cfg["host"], port=cfg["port"], user=cfg["user"], password=cfg["password"], database=cfg["database"], charset=cfg["charset"], cursorclass=pymysql.cursors.DictCursor ) def query(self, sql, params=None): """查询返回列表字典""" with self.conn.cursor() as cursor: cursor.execute(sql, params) return cursor.fetchall() def execute(self, sql, params=None): """执行增删改,自动提交""" with self.conn.cursor() as cursor: cursor.execute(sql, params) self.conn.commit() return cursor.rowcount

逻辑说明:单例模式保证整个测试进程只有一个数据库连接,避免频繁握手。DictCursor让查询结果以字典列表返回,处理起来比元组直观。query方法适合select,execute方法适合insert、update、delete,并自动提交事务。

参数说明:pymysql的cursorclass参数很关键,默认是元组,指定DictCursor后代码里可以写result[0]["user_id"],否则只能写result[0][0]。另外,单例连接有一个隐患:断线后不会自动重连,后面避坑章会专门讲。

3.3 用yaml管理mysql连接信息与用例数据分离

有了mysql封装,把用例数据和数据库断言结合起来。比如测试“添加用户”接口,先从yaml读入参,跑完接口后用mysql查询数据库确认记录真的落库了。

# data/user.yaml add_user: - case: "正常添加用户" payload: username: "zhangsan" email: "zhangsan@test.com" assert_db: table: "user" field: "email" expect: "zhangsan@test.com"
# testcases/test_user_api.py import allure import pytest import requests from common.mysql_util import MysqlUtil from config.config import load_yaml user_data = load_yaml("data/user.yaml") @allure.feature("用户模块") @allure.story("添加用户") @allure.title("正常添加用户落库校验") def test_add_user(base_url): payload = user_data["add_user"][0]["payload"] assert_db = user_data["add_user"][0]["assert_db"] resp = requests.post(f"{base_url}/user/add", json=payload) assert resp.status_code == 200 mysql = MysqlUtil() result = mysql.query(f"SELECT {assert_db['field']} FROM {assert_db['table']} WHERE username=%s", (payload["username"],)) assert result[0][assert_db["field"]] == assert_db["expect"]

逻辑说明:测试数据和用例代码完全分离。新增用例只需在yaml里加一条记录,代码逻辑不用动。数据库断言放在接口断言之后,先确认接口返回成功,再查库验证真实状态。query方法的参数化用%s占位符,避免SQL注入,也避免字符串拼接的引号问题。

参数说明:这里的base_url来自conftest.py的fixture。user_data在模块导入时加载,如果用例运行过程中yaml被修改,需要重启pytest才会生效。实际项目中我还会封装一个“数据清理”函数,在用例执行后把插入的数据删掉,保证可重复执行。

4. 日志系统与allure报告:让失败用例有据可查

接口测试最痛苦的场景是什么?凌晨发来钉钉报警说“新增用户失败”,你打开报告发现只有一行“断言失败”,既不知道请求发出去的是什么,也不知道响应到底返回了什么。日志系统解决的就是这个“可回溯”问题,allure报告解决的是“怎么直观呈现给领导”。

4.1 log日志的十行配置:控制台和文件双输出

logging模块是Python标准库,不用额外安装。关键是配置两个handler:一个输出到控制台,一个写文件。日志格式里带上时间、模块名、级别、消息。我给接口测试的日志定了两个约定:每个用例开始打“请求URL和参数”,结束后打“响应内容”,断言失败时单独打error日志。

# common/log.py import logging import os import time def get_logger(name="api_test"): logger = logging.getLogger(name) logger.setLevel(logging.DEBUG) if logger.handlers: return logger fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s") # 控制台 sh = logging.StreamHandler() sh.setLevel(logging.INFO) sh.setFormatter(fmt) # 文件 log_dir = "logs" if not os.path.exists(log_dir): os.makedirs(log_dir) fh = logging.FileHandler(f"{log_dir}/run_{time.strftime('%Y%m%d')}.log", encoding="utf-8") fh.setLevel(logging.DEBUG) fh.setFormatter(fmt) logger.addHandler(sh) logger.addHandler(fh) return logger LOG = get_logger()

逻辑说明:logger对象是全局复用的,如果重复调用get_logger会导致handler叠加,日志重复打印,所以加了if logger.handlers判断,已有处理器就直接返回。文件日志按天切割,文件名带日期,方便排查问题按天检索。控制台级别设INFO,文件级别设DEBUG,保证调试信息只落文件不刷屏。

参数说明:encoding="utf-8"在Windows下必须指定,否则log文件里的中文是乱码。如果你想按大小切割日志,改用RotatingFileHandler,设置maxBytes=1010241024,backupCount=5。

4.2 pytest钩子把日志和allure报告串起来

日志有了,怎么让每一次请求和响应自动记录,不需要在每条用例里手动调用?用pytest的钩子函数,在conftest.py里拦截请求和响应。这里用requests的Session + hooks,同时利用allure的attach把关键信息挂到报告里。

# common/request_util.py import requests import allure from common.log import LOG class RequestUtil: _session = None @classmethod def get_session(cls): if cls._session is None: cls._session = requests.Session() cls._session.hooks["response"] = [cls._log_response] return cls._session @staticmethod def _log_response(resp, *args, **kwargs): """响应日志钩子,allure附件""" LOG.info(f"接口地址: {resp.url}") LOG.info(f"状态码: {resp.status_code}") LOG.info(f"响应体: {resp.text[:500]}") allure.attach(resp.text[:500], name="response", attachment_type=allure.attachment_type.TEXT) @classmethod def post(cls, url, **kwargs): LOG.info(f"发送POST: {url}, 参数: {kwargs.get('json')}") resp = cls.get_session().post(url, **kwargs) return resp
# testcases/conftest.py import pytest from common.request_util import RequestUtil @pytest.fixture def request_util(): return RequestUtil

逻辑说明:Session的hooks参数里挂response回调,每次请求返回后自动执行_log_response,日志和allure附件都塞进去了。这样用例里只用request_util.post(url, json=payload),不用自己写日志。请求参数通过logging记录,响应内容通过allure.attach作为文本附件展示,allure报告里能看到每条用例对应的响应正文。

参数说明:resp.text截取前500字符,防止响应体过大把报告撑爆。如果需要完整响应,可以调整切片长度。Logging和allure.attach是双写,一个给开发排查用,一个给测试报告用。

4.3 allure报告参数化展示:动态标题、附件与严重级别

很多用例是参数化的,比如同一接口不同入参跑5遍。如果allure报告里全是“测试登录接口-参数1”“参数2”,根本分不清谁是谁。用allure.dynamic.title动态设置用例标题,把业务关键字(比如手机号、场景名)拼进去。

import allure import pytest from common.request_util import RequestUtil login_cases = [ {"username": "zhangsan", "expect": 0, "scenario": "正确密码"}, {"username": "zhangsan", "password": "wrong", "expect": 1001, "scenario": "错误密码"}, {"username": "", "password": "", "expect": 1002, "scenario": "参数缺失"}, ] @allure.feature("用户模块") @allure.story("登录接口") @pytest.mark.parametrize("case", login_cases, ids=lambda x: x["scenario"]) def test_login(case, base_url, request_util): allure.dynamic.title(f"登录接口-{case['scenario']}") if "password" not in case: case["password"] = "123456" resp = request_util.post(f"{base_url}/login", json=case) assert resp.json().get("code") == case["expect"]

逻辑说明:pytest.mark.parametrize的ids参数让测试用例在pytest收集阶段的显示名变成“正确密码”“错误密码”,allure.dynamic.title进一步定制报告里的标题。这里的password缺失场景故意不传值,体现用例数据构造的灵活性。

参数说明:parametrize的ids接收函数,lambda x: x["scenario"]将每条数据映射为可读名称。allure.dynamic.title必须在测试函数内部调用,声明式的@allure.title是静态的,无法根据参数动态组合。

5. 钉钉企微通知与常见排查:结果主动找人,不让人找结果

自动化框架跑完后没人看结果,等于白跑。通知模块就是把“报告生成”和“结果触达”打通:跑完自动发消息到钉钉群或企微群,告诉当前批次是绿是红、失败率多少、失败用例列表。这一章讲透两种机器人推送的实现,以及通知链路里最容易踩的坑。

5.1 钉钉机器人的签名算法与消息构造

钉钉机器人分两种安全设置:关键字和加签。加签模式最常用,因为不用在消息里硬塞关键词。加签逻辑是:把timestamp和secret拼接成字符串,用HmacSHA256加密,再base64编码,最后urlencode拼到webhook地址上。

# common/notify.py import time import hmac import hashlib import base64 import urllib.parse import requests from config.config import CONFIG def dingtalk_notify(content, at_mobiles=None): cfg = CONFIG["alarm"]["dingtalk"] timestamp = str(round(time.time() * 1000)) secret = cfg["secret"] sign_str = f"{timestamp}\n{secret}".encode("utf-8") hmac_code = hmac.new(secret.encode("utf-8"), sign_str, digestmod=hashlib.sha256).digest() sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) url = f"{cfg['webhook']}&timestamp={timestamp}&sign={sign}" payload = { "msgtype": "markdown", "markdown": { "title": "接口自动化测试", "text": f"#### 接口自动化测试结果\\n{content}" }, "at": {"atMobiles": at_mobiles or [], "isAtAll": False} } resp = requests.post(url, json=payload) return resp.json()

逻辑说明:钉钉加签的timestamp必须和请求参数里的timestamp保持一致,而且是一次性的,重放会报错。hmac.new的密钥是secret本身,text是“timestamp换行secret”的字节串。最终签名要经过urlencode,因为base64结果里可能含有+和/。

参数说明:msgtype用markdown,这样群里显示效果更好。at_mobiles传入需要@的人列表,如果留空也可以。isAtAll设置True就@所有人,生产环境慎用。

5.2 企微机器人的webhook发送

企微比钉钉简单,没有加签,只要webhook地址不泄露就行。消息格式支持text和markdown,但markdown语法比钉钉弱,只支持部分标签。

# common/notify.py def wecom_notify(content): cfg = CONFIG["alarm"]["wecom"] payload = { "msgtype": "markdown", "markdown": { "content": content } } resp = requests.post(cfg["webhook"], json=payload) return resp.json()

逻辑说明:企微的markdown字段名是content而不是text,发错了消息类型直接报错。企微没有签名机制,只要知道webhook地址就能发送,所以webhook一定不要提交到公开仓库。如果开通了“群机器人”的IP白名单,记得把执行机的IP加进去。

参数说明:企微机器人单条消息长度有限制,markdown格式建议控制在2048字节以内。如果报告描述很长,先截断再发送,或者拆成多条。

5.3 通知模块的5个常见坑

按“现象→原因→解决”写,这些是我真实踩过的,新手基本都躲不掉。

坑一:钉钉报“robot code 310000”或“sign not match”现象:请求返回错误,提示签名不匹配。 原因:签名拼接顺序错了,有人把secret放前面,timestamp放后面;或者timestamp用的字符串和请求参数里的不一致。 解决:严格按“timestamp换行secret”的顺序,并且timestamp变量只生成一次,同时用于签名和URL参数。

坑二:消息发送成功但群里看不到,或者只有机器人头像没有内容现象:requests返回errcode为0,但群里没有消息。 原因:消息体markdown格式错误,比如```标签没配对;或者content里含有非法字符。 解决:先用钉钉官方的“测试机器人”工具直接发一遍,确定markdown没问题再塞进代码。重点检查换行符是否被转义。

坑三:Windows下日志文件中文乱码现象:日志里中文变成问号或乱码。 原因:FileHandler没有指定encoding,Windows默认gbk编码。 解决:写日志文件时显式传encoding="utf-8",同时确保代码文件第一行就是UTF-8编码。

坑四:mysql连接在一段时间后失效,pytest运行中途报“MySQL server has gone away”现象:用例跑到后段突然全部连接失败,错误信息是“server has gone away”。 原因:mysql的wait_timeout默认8小时,如果pytest跑得久,单例连接空闲超过这个值就被服务端断开,但客户端不知道,仍拿旧连接去查询。 解决:在query方法前增加连接可用性检查,捕获异常后重连。或者每次执行前ping一下,比如self.conn.ping(reconnect=True)。

坑五:钉钉机器人被风控限流,报“超过每分钟20次限制”现象:并发跑完大量用例,通知发送失败,提示频率超限。 原因:钉钉限制每个机器人每分钟最多20条消息。 解决:通知不要逐条发,全部跑完后汇总成一条。如果确实要多条,在通知函数里加一个时间锁,Sleep 1秒控制频率。

6. 进阶验证:失败重试、allure报告合并与Jenkins定时调度

框架跑通基础链路后,下一步要解决的是“稳定性”和“无人值守”。失败重试能降低网络抖动导致的误报,allure报告合并能让并行执行的结果归一,Jenkins定时调度让你不用每天手动敲命令。

失败重试用pytest-rerunfailures插件,在pytest.ini里配置即可:

# pytest.ini [pytest] addopts = -s --alluredir=reports/allure-results --clean-alluredir --reruns=2 --reruns-delay=1

参数说明:--reruns=2表示失败后重试2次,--reruns-delay=1表示每次间隔1秒。注意:如果用例内部涉及数据状态,重试前要保证数据能重置。allure的--clean-alluredir在每次执行前清空上次结果目录,避免新旧数据混在一起。

并行执行时,用pytest-xdist的-n参数让用例多进程跑。此时每个worker都会生成各自的allure-results子目录,需要用allure合并:

pytest testcases -n 4 --alluredir=reports/allure-results # 从子目录合并结果 allure generate reports/allure-results -o reports/allure-html --clean # 推送结果显示 allure open reports/allure-html

合并的要点是:--alluredir指定的必须是同一目录,pytest-xdist会自动写入worker名后缀,allure generate时会把所有子目录合并解析。如果合并后报告缺数据,检查是不是用了相对路径导致多进程写串。

Jenkins调度我一般这样配:新建free style项目,源码管理选Git,构建触发器选H 2 * * *(每天凌晨2点),构建步骤执行一条shell:

cd $WORKSPACE python -m pytest testcases --alluredir=reports/allure-results --clean-alluredir python -m common.notify # 或者通过pytest钩子调用通知模块

在构建后操作里,配置Allure Report插件,路径填reports/allure-results,这样Jenkins页面直接显示报告趋势图。通知发送建议放在pytest的pytest_sessionfinish钩子里,根据汇总的成功率决定钉钉消息颜色和文字,而不是跑完再额外执行一个脚本。

最后说一个我自己的教训:千万别把配置文件里的mysql密码和webhook secret写死在yaml里直接提交到Git仓库。我在公司吃过一次亏,后来把敏感字段全部改成从环境变量读取,yaml里只留占位符。具体做法是:config.py里优先读os.environ,取不到再用yaml里的默认值。

这套框架走到这一步,已经从“能跑”进化到了“能维护、能自动跑、结果有人看”的状态。你搭的过程中一定会碰到比上面更奇葩的问题,但只要环境版本对、目录清晰、日志和报告双保险,就没什么查不出来的。希望帮到你。

本文还有配套的精品资源,点击获取