ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

接口自动化测试框架实战:pytest+yaml+ddt+allure搭建指南

接口自动化测试框架实战:pytest+yaml+ddt+allure搭建指南 简介基于 pythonpytestyamlddtallure 打造的接口自动化测试框架源码包面向具备一定 Python 基础、希望快速落地接口自动化测试的测试工程师。作者结合码尚 VIP 课程与公司项目改造升级实测可用使用者只需将接口用例数据填入 yaml 中即可运行降低公司项目中的适配成本。资源共 126 个文件压缩包约 1.02MB包含 12 个 Python 源码文件、14 个 yaml 用例文件、52 个 json 数据文件以及 csv、html、配置文件等覆盖测试用例编写、数据驱动、报告生成及环境配置等环节。目前已有 4337 人学习下载。通过本框架可掌握 pytest 测试框架与 yaml 分离用例、ddt 驱动执行、allure 报告展示的完整工程实践附带的测试用例规范文档能帮助你统一团队用例编写风格适合作为公司级接口自动化项目的脚手架或学习模板。 我落地过好几个接口自动化项目从最早的requestsunittest写到后期这版pythonpytestyamlddtallure的组合可以说这是我目前用着最顺手的一套框架形态。测试数据、测试逻辑、执行调度、报告展示各管一摊互不干扰。所以想把这套框架从零到一的搭建思路、核心代码、还有踩过的坑一次性完整分享出来给正在选型和准备重构测试框架的同学做个参考。1. 技术选型背后为什么是这四个组件打配合先说清楚这套框架的定位它面向的是中小型项目的接口自动化测试重点解决数据维护成本高、用例编写门槛高、报告展示不直观这三个痛点。在选型之前我先明确了一个原则框架里的每一个组件都必须有不可替代的职责而且要能和其他组件无缝衔接。单独看这四个技术栈其实都不算新但把它们组合在一起恰好形成了一条完整的分工链路pytest挑起了测试执行和用例组织的担子yaml负责把测试数据和配置从代码里剥离出来ddt解决数据驱动的问题allure则把执行结果渲染成一份适合给团队和管理层看的报告。先拆开说说每个组件在我这儿的定位。pytest不用多说它是整个框架的骨架fixture机制做前后置处理非常方便conftest.py可以在不同层级共享夹具还有明确的断言报告。yam是目前测试数据承载的最佳形式读写简单、层级清晰、注释友好比写在Python文件里省去了一堆dict套dict的嵌套结构也比Excel好用得多因为Excel在git里没法做diff多人协作时简直是灾难。ddt的选择值得展开说一下。现在很多pytest项目直接用parametrize装饰器其实parametrize本身就是一种数据驱动的实现方式那为什么还要引ddt呢我的判断是parametrize适合参数比较少、逻辑简单的场景ddt的优势在于完整的装饰器语义——ddt、data、unpack这一套组合在可读性上更直白尤其是测试数据从yaml里读出来的时候配合file_data装饰器可以做到一行代码直接指定数据文件不用手动写读取和展开的逻辑。当然这里要说明一点我这个框架里并不是二选一的关系而是根据实际场景混用复杂逻辑的用例用ddt简单的参数组合用parametrize。allure的接入算是点睛之笔。之前用过HTMLTestRunner也自写过简单报告模板但是allure的报告信息层级、历史趋势、失败重试记录、步骤标注这些能力是其他报告组件没法比的。allure把执行结果变成了可以被浏览和追踪的产物而不只是一堆绿色红色的文本输出。这样四个组件各有主责再加上requests和pytest-assume这些配套库整个框架总共依赖不超过十个包维护成本很低。2. 目录设计与yaml文件的一体化承载这块内容很关键因为很多刚开始搭框架的人往往是把目录结构搭好了却没想清楚每一层是干什么的结果代码写到后来数据文件和配置文件混在一起用例函数越写越长最后不得不推倒重来。我目前使用的目录结构如下api_test_framework/ ├── config/ │ ├── __init__.py │ └── setting.py ├── data/ │ ├── login_data.yml │ └── user_data.yml ├── utils/ │ ├── __init__.py │ ├── read_data.py │ ├── log_utils.py │ └── request_utils.py ├── testcases/ │ ├── __init__.py │ ├── conftest.py │ ├── test_login.py │ └── test_user.py ├── reports/ │ └── allure_results/ ├── pytest.ini ├── requirements.txt └── run.py这个结构里有一个容易被忽视的设计思路config目录里放的是框架运行环境相关的常量比如环境地址、超时时间、数据库连接串data目录里放的是接口测试数据而yaml文件是贯穿这两者的。也就是说yaml并不仅仅是测试数据的仓库它还承担了环境配置的承载功能。在setting.py里我统一维护了环境信息的读取逻辑import yaml import os class Settings: def __init__(self, envtest): self.env env base_path os.path.dirname(os.path.dirname(os.path.abspath(__file__))) config_path os.path.join(base_path, config, env_config.yml) with open(config_path, r, encodingutf-8) as f: self.env_config yaml.safe_load(f)[self.env] property def base_url(self): return self.env_config[base_url] property def app_id(self): return self.env_config[app_id] property def timeout(self): return self.env_config[timeout] settings Settings()这里要把环境做成参数化的原因很简单接口测试最怕的就是环境切换时代码里到处是if else判断环境地址改一个环境要动十几个文件。用yaml统一管理之后切换环境只需要修改注入的env变量就行。至于测试数据类的yaml我列出最近在用的登录接口数据文件这样大家就明白格式如何设计了# 登录接口测试数据 login_normal: description: 正常登录场景 method: post url: /api/login data: username: admin_test password: e10adc3949ba59abbe56e057f20f883e expect_code: 200 expect_result: success login_error_pwd: description: 错误密码登录 method: post url: /api/login data: username: admin_test password: wrong_pass expect_code: 200 expect_result: fail注意这里的url用的是相对路径base_url统一由配置文件读取。绝对地址一旦写死在数据文件里换环境的时候就要全局替换这种坑我踩过不止一次。顺带回答一个大家常问的问题yaml文件怎么创建其实就是一个.yml或.yaml后缀的纯文本文件用任何编辑器都能写关键是缩进不能混用tab和空格。yaml对缩进极其敏感必须用空格缩进而且同一层级缩进必须保持一致。习惯用vscode的话装个yaml插件写起来就有语法校验了。3. 核心封装从yaml到测试用例的关键一环目录和数据文件都准备好了接下来就是框架的神经系统如何读取数据、如何发起请求、如何断言结果。这是框架搭建过程里代码量最大、也是逻辑最核心的部分。首先是读取yaml的工具类主要解决编码和路径解析的问题import os import yaml def read_yaml(file_path): 读取yaml文件返回解析后的Python对象 :param file_path: yaml文件的绝对或相对路径 :return: dict/list with open(file_path, r, encodingutf-8) as f: data yaml.safe_load(f) return data def get_test_data(data_file, case_name): 获取指定yaml文件中的指定用例数据 base_path os.path.dirname(os.path.dirname(os.path.abspath(__file__))) full_path os.path.join(base_path, data, data_file) all_data read_yaml(full_path) return all_data[case_name]为什么用safe_load而不是load这个是比较重要的一个点。yaml.load在不指定Loader的情况下会使用yaml.Loader这存在反序列化任意Python对象的安全隐患。如果yaml文件来源不可控恶意代码可能在解析时被执行。safe_load只支持基础数据类型虽然功能少了一点但在测试框架这个场景下完全够用而且安全。接下来是请求封装。我基于requests库做了二次封装统一了请求日志和返回值的处理import requests import json from utils.log_utils import logger class RequestUtils: def __init__(self, base_url, timeout10): self.base_url base_url self.timeout timeout self.session requests.Session() def request(self, method, url, **kwargs): full_url self.base_url url kwargs.setdefault(timeout, self.timeout) logger.info(f请求地址: {full_url}) logger.info(f请求方法: {method.upper()}) if data in kwargs: logger.info(f请求数据: {json.dumps(kwargs[data], ensure_asciiFalse)}) resp self.session.request(method, full_url, **kwargs) logger.info(f响应状态码: {resp.status_code}) try: resp_json resp.json() logger.info(f响应内容: {json.dumps(resp_json, ensure_asciiFalse)}) return resp except Exception: logger.error(f响应非JSON格式: {resp.text}) return resp请求封装这一层值得反复打磨的地方是日志。没有日志的框架在排查问题时等于盲人摸象接口一旦报错不知道发了什么、回了什么根本无从下手。我在这里把请求的地址、方法、数据和响应状态都打进了日志后面出问题直接看日志效率极高。最后是测试用例和ddt的结合。这里贴一下真实用例的样子import unittest from ddt import ddt, data, file_data from utils.request_utils import RequestUtils from config.setting import settings ddt class TestLogin(unittest.TestCase): classmethod def setUpClass(cls): cls.req RequestUtils(settings.base_url, settings.timeout) file_data(../data/login_data.yml) def test_login(self, description, method, url, data, expect_code, expect_result): 登录接口测试 resp self.req.request(method, url, datadata) self.assertEqual(resp.status_code, 200) resp_data resp.json() self.assertEqual(resp_data[code], expect_code) self.assertEqual(resp_data[msg], expect_result)ddt的file_data装饰器会自动读取yaml文件里的每一个用例并且把用例名作为子用例展示在测试报告中。每个场景的description放在最前面报告里一眼就能看出这条用例验证的是什么。这里有一个细节很多人没注意file_data读取yaml后传入参数的方式是按字典的key匹配函数参数名的。所以yaml里每个字段的key必须和函数参数名保持一致否则会报missing argument。这是ddt比较好也是最容易出现低级错误的地方写的时候要多留心。4. allure报告接入与pytest配置细节框架的骨架搭好之后报告就是测试结果的最终呈现。allure的接入其实比很多人想象的简单核心就两件事装好allure命令行的本机环境然后在pytest执行时指定报告输出目录。先给出一份我在requirements.txt里的依赖清单requests2.31.0 pytest7.4.3 pyyaml6.0.1 ddt1.6.0 allure-pytest2.13.2 pytest-assume2.4.3allure-pytest是连接pytest和allure报告服务的桥梁它会把pytest的执行结果转换成allure能识别的json文件。所以流程是pytest执行用例时通过allure-pytest插件生成result文件然后再由allure命令行把result文件渲染成html报告。pytest.ini的配置如下[pytest] testpaths testcases python_files test_*.py python_classes Test* python_functions test_* addopts -v -s --alluredir./reports/allure_results --clean-alluredir这里需要注意几个关键配置项--alluredir指定allure的result输出目录--clean-alluredir会在每次执行前把目录清理干净避免跟上次的结果混在一起。我用的是相对路径./reports/allure_results执行的时候一定要在项目的根目录下运行pytest否则路径会指向别的地方。如果不想手动执行命令行可以在run.py里封装一层启动入口import os import pytest if __name__ __main__: # 清理历史报告 os.system(rm -rf ./reports/html) pytest.main([-v, -s, --alluredir./reports/allure_results]) # 生成allure报告 os.system(allure generate ./reports/allure_results -o ./reports/html --clean)run.py里的做法是执行完用例后立刻通过allure命令生成最终的html报告。allure命令行的安装我多说一句它依赖Java环境所以机器上必须预先装好JDK。装好之后把allure安装目录下的bin路径配到系统环境变量里终端里执行allure --version能输出版本号就说明安装成功。报告里为了让每条用例能显示有意义的标题而不是test_login_1这种冷冰冰的函数名我习惯在用例函数上加allure的描述装饰器import allure allure.title(正常用户名密码登录) allure.description(验证正确凭据可以登录成功且返回正确信息) allure.severity(allure.severity_level.BLOCKER) def test_login_normal(self): ...最后生成的allure报告首页会按模块展示通过率和测试套件分布点进具体用例还能看到步骤日志和请求数据。对项目组的人来说这种报告比打印一堆pytest文本输出友好得多。5. 环境搭建与新手容易踩的坑最后这部分主要是经验向的。我见过太多同学在学习这套技术栈组合的时候卡在环境搭建环节还没写一行用例就放弃了。这里把常见坑集中列一下。第一个坑是allure命令行和allure-pytest插件的版本匹配问题。allure-pytest只是生成result数据的插件真正渲染报告的是allure命令行。如果两者的版本差异过大可能出现result文件生成了但命令行无法解析的情况。我的经验是allure-pytest用2.13.xallure命令行用2.20以上的版本稳定用的组合是allure-pytest 2.13.2配合allure 2.24.1。第二个坑是JDK环境变量没配好。allure命令行依赖Java运行时如果启动时提示找不到java就在系统环境变量里新增JAVA_HOME并把JDK的bin目录也加进PATH。这个是所有allure相关问题的最高频原因。第三个坑是pytest和ddt同时使用时用例收集的重复执行问题。这个问题比较隐蔽当测试类同时继承unittest.TestCase而且测试函数以test_开头时pytest默认的unittest模式会尝试收集ddt的file_data也会展开用例可能出现一条用例被视为两条执行的情况。解决方法是在pytest.ini里加上一行addopts -v -s -p no:cacheprovider --alluredir./reports/allure_results --clean-alluredir不对这个不是解决重复的。正确解法是pytest从7.0开始默认会在unittest.TestCase子类上使用unittest模式ddt的用例展开结果会和pytest的收集器产生冲突导致用例重复。处理方式有两种要么不用unittest.TestCase基类直接定义类依赖pytest的类收集规则要么在pytest.ini里设置python_classes Test*让pytest按自己的规则收集类而不走unittest模式。我自己实际项目里早期用的是ddt模式下继承unittest.TestCase后期重构时把基类去掉了直接用pytest的fixture做前后置整体更清爽。虽然parametrize覆盖了大部分数据驱动场景但ddt在某些遗留场景仍有价值所以保留了这个组合。第四个坑是yaml文件里的中文编码问题。在Windows环境下如果yaml文件里有中文注释或中文参数读取时可能会报乱码错误。解决方案是我上面写的所有文件操作都显式指定encodingutf-8以及文件本身保存时选择UTF-8编码。还有一个细节yaml文件里用中文做键名不是不行但强烈不建议尽量用英文字段名中文只放在description这种描述性字段里。第五个坑和requests接口请求有关很多接口的响应内容含有非标准JSON内容比如带有BOM头或多余的空白字符直接resp.json()可能报错。我在请求封装里已经做了try except兜底返回原始resp对象后用例里再用json.loads(resp.text)二次解析也不迟。6. 把框架跑起来的完整步骤上面是一些经验复盘和说明接下来这篇内容的最后给出一份可以直接照着操作的步骤清单方便大家按顺序落地整个框架。我以Linux或macOS终端环境为前提Windows用户注意路径和命令上的差异。第一步准备Python环境。推荐用Python 3.10以上版本安装的时候勾选Add Python to PATH。终端验证安装成功可以执行python --version输出版本号即是正确安装。如果系统提示找不到python命令排查环境变量里是否能找到Python的安装目录。第二步创建项目目录并在项目根目录下创建虚拟环境mkdir api_test_framework cd api_test_framework python -m venv venv # 激活虚拟环境Linux/macOS source venv/bin/activate # 激活虚拟环境Windows venv\Scripts\activate第三步安装依赖。准备好requirements.txt之后执行pip install -r requirements.txt安装完之后可以执行pip list确认所有包都已经安装。这里要提醒一下pip下载慢的话可以换成国内镜像源但不建议在公共文档里写死某个源毕竟不同网络环境下的可用性不一样。第四步安装allure命令行。这里给两种方式macOS用户可以用brew install allureLinux用户可以直接下载allure的zip包解压并配置PATHWindows用户去allure官方GitHub的releases页面下载zip包解压后把bin目录加入环境变量。安装完成后执行allure --version确认。第五步把前文中的目录结构、setting.py、request_utils.py、测试用例文件依次创建出来。创建完先跑一遍冒烟用例确认框架能正常执行后再逐步补充更多业务接口的用例。第六步执行测试并生成报告python run.py报告生成在reports/html目录下浏览器打开index.html就能看到完整的allure报告。我个人的建议是把这套框架作为项目初期的固定模板沉淀下来后续新增接口时只需要在data目录新增对应的yaml文件然后在testcases里新增一个测试类业务代码和框架代码完全分离。等到用例数量多了之后还可以在这个基础上增加pytest-xdist并行执行、pytest-rerunfailures失败重跑等插件扩展性很好。回看这个框架的搭建过程最大的体会是框架不是越复杂越好也不是插件越新越好。把数据、配置、用例、报告四层拆清楚每个组件只做好自己的事这套组合已经能覆盖绝大多数接口自动化的需求。以后如果项目规模变大需要引入环境变量管理或者CI/CD流水线这套结构也能很顺畅地扩展出去不会有推倒重来的风险。本文还有配套的精品资源点击获取
返回列表