接口测试数据驱动与断言封装_速查版 接口测试数据驱动 断言封装速查手册面向测试开发初学者的速查版。核心就三个动作数据写进 YAML、pytest 参数化加载、断言交给工具类。读完即可把硬编码脚本改造成工程化项目。完整逐行讲解版见文末链接。一、一句话搞懂数据驱动硬编码的痛一个接口 10 个场景要复制 10 个几乎一样的函数断言满屏if print失败只有一句assert 200 400。数据驱动的解法变化的部分参数 预期放进 YAML不变的部分发请求 跑断言抽成公共代码。新增用例 往 yaml 加一段数据代码零改动。改造后的项目结构新增data/和config/api_test_project/ ├── common/ # yaml_util 读数据 / http_client 发请求 / assert_util 断言 ├── data/ # 用例数据, 一模块一文件: login.yaml / users.yaml ├── cases/ # test_*.py 只做: 取数据→发请求→跑断言 └── config/config.yaml # base_url、公共请求头、多环境配置二、YAML 用例数据模板字段统一规范一条用例四个顶层字段tc_id编号/name用例名/request请求/expect预期# data/login.yaml —— 一条用例 列表里的一项-tc_id:login_001name:登录成功-正确账号密码request:method:POSTurl:/api/login# 只写相对路径, 运行时自动拼 base_urljson:username:adminpassword:123456# ⚠️ 纯数字必须加引号, 否则 YAML 解析成 intexpect:status_code:200# 第1层: HTTP 状态码code:200# 第2层: 业务码msg:登录成功equals:{data.username:admin}# 字段相等(点号路径)not_empty:[data.token]# 非空length:{data.token:{gt:8}}# 长度 8contains:{msg:登录}# 包含regex:{data.token:^[a-zA-Z0-9]$}# 正则-tc_id:login_002name:登录失败-缺失密码request:{method:POST,url:/api/login,json:{username:admin}}expect:{status_code:200,code:400,is_empty:[data]}expect 可用规则全家桶即断言工具类的白名单写 yaml 忘了就回来抄status_codecodemsgequalsfloat_equals浮点近似精度坑专用greater_thanless_thancontainsregexexistsnot_existsnot_emptyis_emptylength_equalslength支持{gt: n}。⚠️ 第一坑HTTP 状态码 ≠ 业务码。登录失败时 HTTP 仍是 200失败体现在响应体code400/401两层都要断只断状态码就会假绿。三、读 YAML 的公共方法绝对路径是保命点# common/yaml_util.pyimportos,yaml BASE_DIRos.path.dirname(os.path.dirname(os.path.abspath(__file__)))# 动态绝对路径DATA_DIRos.path.join(BASE_DIR,data)defread_case_data(name):ifnotname.endswith(.yaml):name.yamlwithopen(os.path.join(DATA_DIR,name),r,encodingutf-8)asf:# ① 显式 utf-8datayaml.safe_load(f)# ② 用 safe_load, 不用 loadreturndataifdataelse[]# ③ 空文件返回 [] 而非 None, 防止遍历报错三个要点绝对路径从任何目录运行 pytest 都不报 FileNotFound、safe_load防 yaml 注入、空文件兜底。四、pytest 参数化十组数据一个函数# cases/test_login.pyimportpytestfromcommon.yaml_utilimportread_case_datafromcommon.assert_utilimportAssertUtil casesread_case_data(login.yaml)# ids 把每条数据的 name 抽出来当用例名, 报告一眼看出跑的是哪条pytest.mark.parametrize(case,cases,ids[c[name]forcincases])deftest_login(client,case):reqcase[request]respclient.send_request(req[method],req[url],paramsreq.get(params),headersreq.get(headers),jsonreq.get(json))# client: base_urlSession超时的封装try:AssertUtil.run_assertions(resp,resp.json(),case[expect],case_namecase[name])exceptAssertionErrorase:raiseAssertionError(f{e}\n响应体:{resp.text[:500]})fromNone# 失败附响应体函数体固定三段式取数据 → 发请求 → 跑断言零 if/assert 硬编码。运行pytest -v先启动 mock 或真实接口。五、断言工具类核心直接抄# common/assert_util.py —— 精简核心版importreclassAssertUtil:EXPECT_KEYS{status_code,code,msg,equals,contains,regex,exists,not_exists,not_empty,is_empty,length_equals,length,float_equals,greater_than,less_than}staticmethoddefget_value(data,path):点号路径取值: data.list.0.username - 逐层取值(列表按下标, 字典按key)curdataforkeyinstr(path).split(.):curcur[int(key)]ifisinstance(cur,list)elsecur[key]returncurstaticmethoddef_raise(case,desc,expected,actual):raiseAssertionError(f【{case}】{desc}断言失败: 期望{expected!r}, 实际{actual!r})classmethoddefrun_assertions(cls,resp,body,expect,case_name):# ① 白名单校验: yaml 拼错字段(如 equas)不校验会导致假绿, 必须第一步拦截unknownset(expect)-cls.EXPECT_KEYSifunknown:raiseAssertionError(f【{case_name}】expect 非法字段{unknown})ifstatus_codeinexpectandresp.status_code!expect[status_code]:cls._raise(case_name,状态码,expect[status_code],resp.status_code)forfin(code,msg):# ② 顶层快捷字段iffinexpectandbody.get(f)!expect[f]:cls._raise(case_name,f字段{f},expect[f],body.get(f))forpath,expin(expect.get(equals)or{}).items():# ③ 字段相等try:actualcls.get_value(body,path)except(KeyError,IndexError):raiseAssertionError(f【{case_name}】字段{path}不存在)ifactual!exp:cls._raise(case_name,f字段{path},exp,actual)forpathinexpect.get(exists)or[]:# ④ 存在性try:cls.get_value(body,path)except(KeyError,IndexError):raiseAssertionError(f【{case_name}】字段{path}应存在)forpathinexpect.get(not_empty)or[]:# ⑤ 非空(0和False不算空!)vcls.get_value(body,path)ifvisNoneorvorv[]orv{}:raiseAssertionError(f【{case_name}】字段{path}应为非空)forpath,subin(expect.get(contains)or{}).items():# ⑥ 包含actualcls.get_value(body,path)ifsubnotinstr(actual):cls._raise(case_name,f字段{path}包含,sub,actual)forpath,specin(expect.get(length_equals)or{}).items():# ⑦ 数组长度actual_lenlen(cls.get_value(body,path))ifactual_len!spec:cls._raise(case_name,f字段{path}长度,spec,actual_len)# regex / length / float_equals / greater_than 等其余规则同理:# 遍历 expect - get_value 取实际值 - 比对, 完整实现见文末完整教程设计铁律 3 条① 只抛AssertionError——pytest 才记为 FAILED业务缺陷抛其它 Exception 会被记成 ERROR脚本故障报告口径就失真了② 报错四有用例名 / 字段路径 / 期望值 / 实际值一眼定位③ 白名单校验数据拼错立刻炸杜绝假绿。六、两个高频需求的现成方案token 跨接口传递conftest 里写会话级 fixture 登录一次拿 token → yaml 里写占位符Authorization: Bearer ${token}→ 发请求前用序列化 → 正则替换 → 反序列化渲染YAML 始终保持静态可维护不把会过期的 token 写死在数据里。多环境切换config.yaml按test:/pre:分节放 base_url、公共头、超时conftest 用pytest_addoption注册参数config request.config.getoption(--env)传给 client。运行pytest --envpre用例数据零改动。七、踩坑速查7 条必记#坑一句话对策1YAML 缩进报 ScannerError全文件禁 Tab统一 2 空格缩进2中文乱码文件存 UTF-8 open(encodingutf-8)响应乱码在 HttpClient 里加resp.encoding utf-83换目录跑报 FileNotFound用__file__动态算 BASE_DIR永不写死相对路径4浮点断言0.3 0.30000000000000004挂别用用float_equalsabs 差 1e-65数组整体相等偶发失败list 含元素顺序只关心条数用length_equals6空值误判别用if not x0/False 会被当空判空标准是 None/“”/[]/{}7千行 yaml 维护不动按模块拆分 tc_id 全局编号 锚点/*复用公共段八、面试三连背这三个答案为什么选 YAML支持注释缩进天然表达请求体嵌套纯文本git diff 能看清每次只改了什么。JSON 无注释、CSV 表达不了嵌套、Excel 无法版本对比。断言为什么封装统一报错口径只抛 AssertionError → FAILED、报错信息四有、白名单防假绿、点号路径统一取值。数据驱动带来什么新增用例 新增数据不碰代码换环境只改 base_url非开发人员改 yaml 就能维护用例。以上是备忘录——3 分钟复盘、照抄落地。逐行原理、完整断言实现正则/数组/浮点/异常兜底、Git pre-commit 提交前自动跑全部数据用例、12 道面试题详解见完整版教程(https://blog.csdn.net/2501_92632351/article/details/168553856?spm1001.2014.3001.5502)