ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

pytest接口测试框架进阶:数据驱动、fixture与持续集成实战

pytest接口测试框架进阶:数据驱动、fixture与持续集成实战 我们上次把 pytest 的基础用法和接口测试的简单用例串了一遍当时很多朋友留言说“不过瘾还想看更深入的”。确实光会写几个 test_ 开头的函数、跑一下 assert那只是入了门。真正的接口测试框架要解决的是用例组织、数据驱动、环境切换、报告集成、持续集成这一整套问题。这次我就接着往下写把 pytest 在接口测试里的进阶玩法、项目落地经验和踩过的坑一次性讲透。这篇文章适合谁已经会用 pytest 写简单用例、知道 requests 怎么发请求但想把项目从“脚本”升级成“框架”的测试开发同学。内容会比较干我尽量用实际项目里的代码片段来讲而不是堆概念。1. 接口测试框架的整体规划思路1.1 为什么要从“脚本”走向“框架”很多团队一开始做接口测试都是 Postman 里调通了然后复制成 Python 脚本写个循环跑一遍。前期用例少还好一旦接口数量上了百、参数组合多了脚本维护成本会迅速失控。我见过最夸张的项目一个 500 行的“接口测试总控脚本”里面全是 if-else 分支每次接口字段变动改起来像拆炸弹。pytest 真正厉害的地方不是“能跑测试”而是它的插件生态和 fixture 机制可以让用例代码和测试数据分离、让前置条件和后置清理变得规整、让报告可以直接给领导看。搭框架的意义在于让测试逻辑可以复用让数据改变不需要改代码让新人接手时不用从头读一遍所有脚本。1.2 适合 pytest 的接口测试项目结构以我常用的一个项目模板为例目录结构长这样api_test_project/ ├── api_pytest/ # 核心代码目录 │ ├── __init__.py │ ├── conftest.py # 全局 fixture │ ├── config.py # 环境配置、URL、超时等 │ ├── req/ # requests 封装的请求层 │ │ ├── __init__.py │ │ ├── base_req.py │ │ └── user_api.py │ ├── utils/ # 工具函数日志、读取yaml、加解密等 │ ├── testcases/ # 测试用例目录 │ │ ├── test_login.py │ │ ├── test_order.py │ │ └── test_user.py │ └── test_data/ # 测试数据目录yaml/json文件 │ ├── login_data.yaml │ └── order_data.yaml ├── reports/ # 测试报告输出目录 ├── pytest.ini # pytest 配置文件 └── requirements.txt这里有几个关键设计点apireq 目录放对接口的封装一个接口一个函数或一个类。测试用例不直接调 requests.get()而是调封装好的 user_api.get_user_info()。这样接口如果加了鉴权头、改了域名前缀只改封装层用例层不用动。test_data 用 yaml 而不是写死在代码里。用例里通过参数化读取数据变更只需要改 yaml 文件。conftest.py 放全局 fixture比如获取 token、数据库清理、日志初始化这些不需要每个用例文件都 import。注意conftest.py 的文件名是固定的pytest 会自动识别。它的作用域是目录级的放在哪个目录就对哪个目录下的用例生效。1.3 pytest.ini 的配置技巧pytest.ini 是 pytest 的配置文件很多新手会忽略它。我建议至少配置这几个参数[pytest] testpaths api_pytest/testcases addopts -v -s --tbshort --maxfail3 log_cli true log_cli_level INFO log_cli_format %(asctime)s [%(levelname)s] %(message)stestpaths指定测试用例目录这样在项目根目录直接敲 pytest 就能识别不用每次 cd。addopts里加上--tbshort失败的时候只看关键几行不会刷满屏。--maxfail3表示有 3 个用例失败就停节省调试时间。真正跑全量回归的时候可以再临时去掉。2. requests 封装与 pytest 的结合2.1 封装 requests 的常见思路requests 库本身封装得已经非常友好但在项目里我还是建议做一层“会话封装”。它能解决接口测试中的三个共性问题统一加鉴权头部不需要每个用例手动带 token。统一日志输出方便排查问题。统一响应断言入口比如判断 status_code 和业务 code。我来写一个基础封装大家直接参考# api_pytest/req/base_req.py import requests import logging import json logger logging.getLogger(__name__) class BaseReq: def __init__(self, base_url, tokenNone): self.base_url base_url.rstrip(/) self.session requests.Session() self.token token self.default_headers { Content-Type: application/json, User-Agent: api-test/1.0 } def _handle_headers(self, headersNone): h dict(self.default_headers) if self.token: h[Authorization] fBearer {self.token} if headers: h.update(headers) return h def request(self, method, url, **kwargs): full_url f{self.base_url}{url} headers self._handle_headers(kwargs.pop(headers, None)) logger.info(f请求: {method.upper()} {full_url}, 参数: {kwargs}) resp self.session.request(method, full_url, headersheaders, timeout10, **kwargs) logger.info(f响应: {resp.status_code} {resp.text[:500]}) return resp def get(self, url, **kwargs): return self.request(GET, url, **kwargs) def post(self, url, **kwargs): return self.request(POST, url, **kwargs)有几个细节说明一下用requests.Session()而不是每次直接 requests.get()能复用底层连接跑大批量用例时性能更好如果接口有 Cookie 态的保持也会更省心。token 通过类属性传入请求时自动加头用例里完全不用关心鉴权这件事。timeout10必须写。接口测试最怕的就是请求挂住不返回白白等几十秒超时能尽快暴露问题。日志里只打resp.text[:500]避免接口返回体过大时把日志文件撑爆。真实项目中大报文接口很常见这个习惯建议从第一天就养成。2.2 按模块拆分 API 封装每类接口单独建一个文件继承 BaseReq。比如用户模块# api_pytest/req/user_api.py from .base_req import BaseReq class UserApi(BaseReq): def get_user_info(self, user_id): return self.get(f/api/v1/user/{user_id}) def update_user(self, user_id, payload): return self.post(f/api/v1/user/{user_id}/update, jsonpayload)这样的好处是当一个接口变了我只改这一个文件里的一个函数用例层几乎不动。如果后端一次性改了 30 个接口你去改 30 个 api 文件总比在几百个用例里全局替换要清晰得多。实际项目里还可以用functools.lru_cache或类变量来存储 token避免每个测试类都重新登录一遍。我一般是把BaseReq的实例做成 fixture 放在 conftest.py 里pytest.fixture(scopesession) def req_obj(): token get_token_from_login_api(admin, 123456) return UserApi(BASE_URL, tokentoken)scopesession意味着整个测试会话只创建一次实例token 也只请求一次。几百个用例跑下来登录接口只调一次速度和资源占用都会好很多。3. 数据驱动用参数化代替重复代码3.1 pytest 参数化的基础用法pytest 的pytest.mark.parametrize是接口测试里用得最多的装饰器。我来做个对比如果你有 5 组测试数据最原始的写法是def test_login_success(): resp api.login(admin, 123456) assert resp.json()[code] 0 def test_login_wrong_password(): resp api.login(admin, wrong) assert resp.json()[code] 1001这种写 5 个用例可能存在两个问题一是代码重复二是数据混在代码里。用参数化改写import pytest from req.user_api import UserApi # 假设有这样一个实例 pytest.mark.parametrize(username,password,expected_code, [ (admin, 123456, 0), (admin, wrong, 1001), (, 123456, 1002), (normal_user, password123, 0), (admin, , 1002), ]) def test_login(username, password, expected_code): resp api.login(username, password) assert resp.json()[code] expected_code这样数据一目了然新增用例只需要在列表里加一行元组不用再新增函数。用 pytest 跑的时候每个参数组合都会显示为一个独立的用例-v模式下可以清楚看到哪一组挂了。3.2 从 yaml 文件读取测试数据参数写死在代码里算是第二阶段的进步但真实项目里测试数据的量往往很大而且用例设计人员可能不会 Python这时候就需要把数据挪到文件里。我的习惯是用 yaml原因是它比 json 更可读支持注释层级缩进写起来很顺手。下面是login_data.yaml的内容示例# 登录接口测试数据 test_login: - username: admin password: 123456 expected_code: 0 desc: 正确账号密码 - username: admin password: wrong expected_code: 1001 desc: 密码错误 - username: password: 123456 expected_code: 1002 desc: 用户名为空读取 yaml 并配合参数化的通用做法# api_pytest/utils/data_loader.py import yaml from pathlib import Path def load_yaml_data(file_name): file_path Path(__file__).parent.parent / test_data / file_name with open(file_path, r, encodingutf-8) as f: return yaml.safe_load(f)用例里这样写import pytest from utils.data_loader import load_yaml_data login_data load_yaml_data(login_data.yaml)[test_login] pytest.mark.parametrize(data, login_data, ids[d[desc] for d in login_data]) def test_login_from_yaml(data): resp api.login(data[username], data[password]) assert resp.json()[code] data[expected_code]ids参数的作用非常实用跑 pytest 的时候默认每个参数化用例显示为一长串数据值你根本不知道挂的是哪组。加上ids后用例 ID 就是 “正确账号密码”、“密码错误”这样一眼能看懂的名字。提示yaml 文件里放中文字段描述没有问题但文件必须存成 UTF-8 编码否则 Windows 上读取可能乱码。3.3 数据驱动时如何做断言接口测试的断言不能只断言 status_code。很多项目 HTTP 状态码永远是 200真实业务结果在 body 的 code 字段里。我建议至少断言三个层级网络层status_code 等于期望值一般接口约定 200 或 201。业务层body 里的 code、msg 或具体字段值。数据层如果测试环境有数据库权限关键的写操作可以把数据库里的真实落库数据也查出来比对。对于复杂 JSON 结构可以封装几个常用的断言工具函数def assert_json_value(resp_json, json_path, expected_value): 简单支持通过点号取深层字段如 data.user.name obj resp_json for key in json_path.split(.): obj obj[key] assert obj expected_value, fJSON路径 {json_path} 期望 {expected_value}, 实际 {obj}实际用例里写assert_json_value(resp.json(), data.order.status, PAID)比写一长串的resp.json()[data][order][status]可读性强得多。4. conftest 与 fixture 的深度实战4.1 fixture 的作用域和依赖fixture 的 scope 常见的有四个function每个用例跑一次、class每个类跑一次、module每个模块跑一次、session整个 pytest 进程只跑一次。接口测试里我做这几个常用的# api_pytest/conftest.py import pytest import logging from req.user_api import UserApi from utils.config import BASE_URL, USERNAME, PASSWORD pytest.fixture(scopesession) def api(): 整个测试会话共用的 API 对象 return UserApi(BASE_URL) pytest.fixture(scopesession) def auth_token(api): 登录获取 token整个会话只登录一次 resp api.login(USERNAME, PASSWORD) assert resp.status_code 200 token resp.json()[data][token] return token pytest.fixture() def user_api(auth_token): 带 token 的 API 对象每个用例单独一个实例 return UserApi(BASE_URL, tokenauth_token)注意这里我故意把user_api的 scope 定义成默认的 function。因为虽然 token 是复用的但有的用例会在请求时往 API 对象上挂临时状态如果多个用例共享同一个对象可能会互相污染。4.2 用 fixture 做前置数据准备接口测试不像单元测试很多接口需要前置数据才能用。比如测试“取消订单”接口你需要先有一个已创建的订单测试“确认收货”你需要先有已发货的订单。传统做法是在用例里先调几个接口把数据准备出来这会导致用例很长、且每条用例都在重复造数据。更好的做法是把“造数据”放进 fixture并为不同场景设计不同的 fixturepytest.fixture() def created_order(user_api, auth_token): 创建一个已支付订单返回订单ID resp user_api.create_order(payload{...}) order_id resp.json()[data][order_id] user_api.pay_order(order_id) yield order_id # 后置清理将订单状态改为已取消或删除测试数据 user_api.cancel_order(order_id)用例就变成了def test_cancel_created_order(created_order): resp api.cancel_order(created_order) assert resp.json()[code] 0这里yield之前的代码是前置准备yield之后是后置清理。好处是用例只关注自己要测的那一步造数和清理逻辑被复用。4.3 conftest 里写 hooks 做全局处理除了 fixtureconftest 里还可以写 pytest 的钩子函数。比如我想在每个用例失败时自动截图App 测试常用或自动保存响应日志pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: # 这里可以记录失败时的请求和响应信息 logging.error(f用例 {item.name} 执行失败) logging.error(f失败信息: {report.longrepr})如果你在 conftest 里拿到当前测试用例的request或fixture缓存还可以把关联的响应数据写到报告里。这属于锦上添花但调试复杂接口时非常有用。5. Allure 报告与 Jenkins 持续集成5.1 Allure 报告的配置pytest 自带的报告就是一段文本适合开发者自己看。但你要把接口测试结果同步给产品、开发、领导那就得用 Allure 或类似的可视化报告工具。Allure 的配置步骤很简单pip install allure-pytest运行时加参数pytest --alluredir./allure-results # 生成报告页面 allure serve ./allure-results如果希望用例有更好的报告展示效果可以给用例加描述和 severityimport allure allure.title(取消已创建订单) allure.severity(allure.severity_level.CRITICAL) allure.description(验证已支付订单可以被取消且取消后状态为CANCELLED) def test_cancel_created_order(created_order): resp api.cancel_order(created_order) assert resp.json()[data][status] CANCELLED运行之后Allure 报告里就能看到每个用例的标题、描述、优先级还能通过环境信息区分测试环境。5.2 接口日志对接 Allure我实际项目中会在 BaseReq 的 request 方法里把请求和响应信息 attach 到 Allure 上import allure def request(self, method, url, **kwargs): ... resp self.session.request(method, full_url, headersheaders, timeout10, **kwargs) # 写入 allure 报告 allure.attach( bodyf### 请求\n\n{method.upper()} {full_url}\n请求头: {headers}\n请求体: {kwargs.get(json)}\n\n### 响应\n\n{resp.text[:1000]}\n, namef{method.upper()} {url}, attachment_typeallure.attachment_type.MARKDOWN ) return resp这样每个用例点开都能看到完整的请求报文和响应报文排查问题的时候不再需要去翻控制台日志。5.3 Jenkins 任务配置要点接口测试最终是要在 CI/CD 里跑的。我用 Jenkins 的 freestyle 项目加虚拟环境跑接口测试关键配置点构建步骤选“Execute shell”内容cd $WORKSPACE python3 -m venv venv source venv/bin/activate pip install -r requirements.txt pytest --alluredir./allure-results --clean-alluredir构建后操作选 “Allure Report”Report path 填allure-results。触发策略可以定时跑比如每天晚上 22 点 代码变更时跑webhook 触发。实测下来一个中等规模项目的接口用例300 条左右在普通配置的 Jenkins slave 上跑大约 3~5 分钟完全可以接受。6. 接口测试里的常见问题与排错笔记接口测试和单元测试最大的不同是它测的系统是分布式的出问题时原因可能不在被测代码本身可能是数据问题、环境问题、上游依赖问题、甚至 DNS 问题。我把自己多年踩过的坑整理成一个速查表现象可能原因排查手段所有用例超时测试环境网络不通/服务未启动curl 先探一下健康检查地址偶发超时上游服务不稳定、数据库慢查询看监控重试一次看是否恢复401 大量失败token 过期或未正确传递检查登录逻辑和 token 存储方式返回 500后端异常可能数据未清理干净查看后端日志检查 test data 残留字段值不稳定并发测试数据互相影响改用独立测试账号/随机用户名本地通过CI 上挂环境变量/配置文件不同检查 .env 文件和 CI 的环境变量配置yaml 数据读取格式错误缩进不对或非法字符用 Python 直接打印读取结果定位除了逐条排查我还有一个习惯每个用例都设计成“幂等可重跑”。测试跑一次和跑十次结果必须一致。如果做不到幂等大多是测试数据没有隔离好。最简单的解法是随机生成用户名、手机号、订单号或使用 pytest 的 reset 机制定期清理测试库。再分享一个经验接口测试里mock 很重要但别滥用。团队里有专门的 mock 服务来模拟支付、短信、三方登录这类外部依赖但大部分内部接口都建议打真实环境因为集成问题是接口测试的核心价值所在。mock 用太多测试报告一片绿实际上掩盖了上下游的兼容性问题。最后再分享一点实际工作中的体会接口测试框架的建设是个持续演进的过程不是一次搭完就一劳永逸。最开始可能是几十个用例的脚本跑一次要人工看一下跑了几百个用例就要开始想怎么生成让业务方看得懂的报告跑了一千个用例才会意识到性能、稳定性、环境隔离才是最大的瓶颈。pytest 的灵活性给这些演进留了足够的空间它不限制你怎么写也不逼你用某一种模式。关键是你要在项目逐步变大的过程中持续重构你的 fixture、抽取公共逻辑、完善数据管理——别等到一团乱麻再动手那时候改框架的成本是几倍的。
返回列表