2026/10/29 7:53:40

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

pytest+allure+钉钉企微通知:接口自动化测试框架从零搭建实战 简介一套基于Python的接口自动化测试框架面向测试开发工程师及需要系统化落地接口测试的团队整合pytest执行、allure报告、log日志、yaml配置、mysql存储与钉钉企微通知能力针对用例管理、配置分离、结果反馈等常见问题提供完整方案。压缩包共167个文件约2.79MB核心为63个py源码脚本另含7个yaml配置、4个xlsx测试数据、27个png报告截图以及pytest配置文件、项目说明文档、覆盖率文件等工程文件目录结构清晰便于按模块对照学习。框架贯通用例编写、数据驱动、日志记录、报告生成和即时通知全流程可修改yaml接入自身环境借助allure可视化结果快速定位问题日志按日期归档也方便追溯运行记录。已有1162人学习适合作为二次开发或搭建企业级接口自动化体系的实用参考。1. 接口自动化框架怎么选pytestallure通知这套组合的底气在哪很多从零开始做接口自动化的团队最纠结的往往不是怎么写用例而是用例写完没人看、跑完没人信。pytestallurelogyamlmysql钉钉企微通知这套组合本质上是把用例执行、数据管理、结果展示和消息推送串成一条流水线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 allureWindows用户手动配环境变量。# 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(scopesession) 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, jsonpayload) assert resp.status_code 200 assert resp.json().get(code) 0# 运行用例并生成allure结果 pytest testcases/test_login.py --alluredirreports/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: Test123 database: api_test charset: utf8mb4 alarm: dingtalk: webhook: https://oapi.dingtalk.com/robot/send?access_tokenxxxx secret: SECxxxx wecom: webhook: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyxxxx# config/config.py import yaml import os def load_yaml(path): 读取yaml文件并返回dict with open(path, r, encodingutf-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( hostcfg[host], portcfg[port], usercfg[user], passwordcfg[password], databasecfg[database], charsetcfg[charset], cursorclasspymysql.cursors.DictCursor ) def query(self, sql, paramsNone): 查询返回列表字典 with self.conn.cursor() as cursor: cursor.execute(sql, params) return cursor.fetchall() def execute(self, sql, paramsNone): 执行增删改自动提交 with self.conn.cursor() as cursor: cursor.execute(sql, params) self.conn.commit() return cursor.rowcount逻辑说明单例模式保证整个测试进程只有一个数据库连接避免频繁握手。DictCursor让查询结果以字典列表返回处理起来比元组直观。query方法适合selectexecute方法适合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: zhangsantest.com assert_db: table: user field: email expect: zhangsantest.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, jsonpayload) assert resp.status_code 200 mysql MysqlUtil() result mysql.query(fSELECT {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(nameapi_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, encodingutf-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保证调试信息只落文件不刷屏。参数说明encodingutf-8在Windows下必须指定否则log文件里的中文是乱码。如果你想按大小切割日志改用RotatingFileHandler设置maxBytes1010241024backupCount5。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], nameresponse, attachment_typeallure.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, jsonpayload)不用自己写日志。请求参数通过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, idslambda 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, jsoncase) 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_mobilesNone): 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, digestmodhashlib.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, jsonpayload) 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], jsonpayload) 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没有指定encodingWindows默认gbk编码。 解决写日志文件时显式传encodingutf-8同时确保代码文件第一行就是UTF-8编码。坑四mysql连接在一段时间后失效pytest运行中途报“MySQL server has gone away”现象用例跑到后段突然全部连接失败错误信息是“server has gone away”。 原因mysql的wait_timeout默认8小时如果pytest跑得久单例连接空闲超过这个值就被服务端断开但客户端不知道仍拿旧连接去查询。 解决在query方法前增加连接可用性检查捕获异常后重连。或者每次执行前ping一下比如self.conn.ping(reconnectTrue)。坑五钉钉机器人被风控限流报“超过每分钟20次限制”现象并发跑完大量用例通知发送失败提示频率超限。 原因钉钉限制每个机器人每分钟最多20条消息。 解决通知不要逐条发全部跑完后汇总成一条。如果确实要多条在通知函数里加一个时间锁Sleep 1秒控制频率。6. 进阶验证失败重试、allure报告合并与Jenkins定时调度框架跑通基础链路后下一步要解决的是“稳定性”和“无人值守”。失败重试能降低网络抖动导致的误报allure报告合并能让并行执行的结果归一Jenkins定时调度让你不用每天手动敲命令。失败重试用pytest-rerunfailures插件在pytest.ini里配置即可# pytest.ini [pytest] addopts -s --alluredirreports/allure-results --clean-alluredir --reruns2 --reruns-delay1参数说明--reruns2表示失败后重试2次--reruns-delay1表示每次间隔1秒。注意如果用例内部涉及数据状态重试前要保证数据能重置。allure的--clean-alluredir在每次执行前清空上次结果目录避免新旧数据混在一起。并行执行时用pytest-xdist的-n参数让用例多进程跑。此时每个worker都会生成各自的allure-results子目录需要用allure合并pytest testcases -n 4 --alluredirreports/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点构建步骤执行一条shellcd $WORKSPACE python -m pytest testcases --alluredirreports/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里的默认值。这套框架走到这一步已经从“能跑”进化到了“能维护、能自动跑、结果有人看”的状态。你搭的过程中一定会碰到比上面更奇葩的问题但只要环境版本对、目录清晰、日志和报告双保险就没什么查不出来的。希望帮到你。本文还有配套的精品资源点击获取