ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Pytest+Allure+Excel搭建接口自动化测试框架实践

Pytest+Allure+Excel搭建接口自动化测试框架实践 做接口测试这些年我一直坚持一个朴素的判断一个接口自动化测试框架能跑起来不算本事能在团队里被“用起来”才算本事。今天要聊的这套接口自动化测试框架主技术栈就是 Pytest Allure Excel三者各司其职Pytest 负责用例组织与执行控制Allure 负责把执行结果渲染成直观漂亮的报告Excel 则承担测试数据的集中管理和维护。这套组合不是猎奇而是我踩过不少坑之后沉淀下来的一套可复现方案。它解决什么问题简单说就是让不会写代码的业务测试也能维护接口用例让会写代码的测试开发不用把大量时间耗在数据准备和报告整理上。你只需要在 Excel 里维护接口地址、请求参数、预期结果用例会自动跑起来测试报告自动生成跑完几乎不需要人工干预。适合想从手工测试往测试开发方向走的朋友也适合团队想从零搭建一套低门槛接口自动化体系的情况。1. 为什么最终选了 Pytest Allure Excel 这套组合1.1 接口自动化不是为了“接口能调通”我见过不少团队做接口自动化最后做成了“一堆脚本调通一堆接口”接口换了环境就挂断言基本只查状态码跑完报告没人看。这本质上是思路出了问题接口自动化的目标不是证明接口今天能通而是回归、拦截、沉淀业务规则。所以框架的第一要务不是“能调通”而是“可维护、可扩展、看得懂”。可维护指的是用例之间不互相污染改接口参数不需要改代码可扩展指的是新增接口用例只需要往测试数据里加一行看得懂指的是测试报告要能清晰告诉团队哪个模块挂了、挂在哪个流程、失败原因是什么。从这个基本盘出发去选型很多花里胡哨的框架会自动被过滤掉最终沉淀下来的就是 Pytest、Allure、Excel 这套务实组合。1.2 Pytest 相比其他框架赢在哪里先说 Pytest。它在 Python 测试生态里的地位基本相当于手机操作系统里的安卓——不是唯一选择但综合体验最平衡。对比 unittestPytest 的 fixture 机制灵活太多可以按函数、类、模块、会话四个维度控制前置和后置写起来也简洁断言直接用 Python 原生 assert报错信息还带上下文对比对比 Robot FrameworkPytest 不需要额外学一套关键字语法对于写代码的人来说心智负担更低而且 Robot Framework 的用例维护最终往往变成维护一堆自定义关键字复杂度是往后置的。还有一个容易被忽视的点Pytest 的插件生态。失败重跑、并发执行、依赖控制、超时控制装个插件就能用不需要自己造轮子。我们在框架里用到的 pytest-xdist、pytest-rerunfailures、pytest-assume 都属于这类。这相当于框架本身就带着一个“可扩展集市”不用每次都要自己动手实现。1.3 Allure 和 Excel 的选择逻辑Allure 被选中的原因报告好看只是表象真正有用的是它把测试报告做成了“可浏览的信息结构”Feature 拉出业务模块Story 区分功能点Step 记录操作步骤执行时间、失败原因、历史趋势一目了然。对比 HTMLTestRunner 这类简单报告Allure 的失败分类机制能直接告诉团队错误属于断言失败、接口异常还是环境问题省去了人工翻日志的麻烦。Excel 则是测试数据层的选择。有些人觉得用 YAML 或 JSON 更“现代”但对大多数团队来说Excel 有两个不能替代的优势第一业务测试维护成本极低打开就能改不用认识代码第二测试人员本来就习惯用 Excel 写测试用例把用例字段改造成数据驱动格式认知迁移成本几乎为零。这套组合拿给业务测试用对方不需要理解 Pytest 是什么只需要知道在 Excel 里怎么加一行数据框架就能自动执行这就是数据驱动最实在的价值。2. 框架整体设计与目录结构2.1 目录结构一眼看完全貌先上目录结构这是框架的骨架后续所有代码都落在这棵树上api_test_framework/ ├── common/ │ ├── __init__.py │ ├── excel_util.py # Excel 读取封装 │ ├── request_util.py # 请求发送封装 │ ├── assert_util.py # 断言封装 │ └── log_util.py # 日志模块 ├── config/ │ ├── __init__.py │ ├── settings.py # 全局配置环境地址、超时时间等 │ └── pytest.ini # Pytest 配置文件 ├── data/ │ └── api_cases.xlsx # 接口测试用例数据 ├── testcases/ │ ├── __init__.py │ ├── conftest.py # Fixture 与钩子函数 │ └── test_api.py # 通用测试入口 ├── report/ │ ├── allure-results/ # 执行产物 │ └── allure-report/ # 生成的 HTML 报告 └── requirements.txt这个结构来自一个很朴素的原则按“职责”分层而不是按“技术”堆文件。common 里是通用封装config 里是可变配置data 里是测试数据testcases 里是测试用例与执行入口report 是输出物。任何人接手这个框架从目录就能猜到每个文件是干什么的。2.2 一条用例从 Excel 到报告的完整链路框架的运行链路是这样的Pytest 在执行用例前先从 Excel 读取全部用例数据通过参数化机制把每一行测试数据变成一个独立的测试用例执行时调用请求封装发送 HTTP 请求拿到响应后交给断言工具校验执行过程中把请求信息、响应信息和断言结果写入日志与 Allure 结果文件最后通过 Allure 命令生成 HTML 报告。这条链路最核心的设计是“数据与代码分离”。测试数据只存在于 Excel 中测试代码只负责“怎么执行”和“怎么判断”两者通过字段名映射关系绑定。新增一个用例不需要新增任何代码改一个用例也不需要动测试文件大大降低了维护成本。因为代码里不出现具体接口地址和具体参数值所以代码本身是稳定的真正变化的数据全部收敛在 Excel 里。2.3 分层的核心思路框架分成四层数据层、核心封装层、用例层、报告层。数据层是 Excel 文件管“测什么”核心封装层是 common 目录管“怎么发请求、怎么做断言”用例层是 testcases 目录管“怎么组织执行”报告层是 Allure管“怎么展示结果”。这样分层的好处是每一层都可以独立替换。比如某天团队决定把测试数据从 Excel 迁到数据库只需要替换 ExcelUtil用例层的参数化逻辑只需要微调请求和断言完全不动再比如团队想引入接口覆盖率统计只需要在用例层加一个钩子不需要改核心封装。分层的本质是控制变化半径让每一次改动都局限在尽可能小的范围内。3. Excel 数据驱动把测试数据从代码里剥出来3.1 用例表字段怎么设计才够用Excel 用例表的字段设计非常关键字段太少表达不了复杂场景字段太多又会让维护者崩溃。我最终沉淀了一套适合大多数接口测试场景的字段组合字段名说明示例case_id用例唯一标识建议带模块前缀USER_001case_name用例名称会显示在报告中查询用户列表成功uri接口路径不含域名/api/v1/usersmethod请求方法GET / POST / PUT / DELETEheaders请求头JSON 字符串{Authorization: Bearer {token}}body请求体GET/DELETE 时作为查询参数{page: 1, size: 10}expected_code期望状态码200expected_msg期望响应中必须包含的关键内容successis_run是否执行Y/NY这套字段有几个设计细节值得说明。headers 和 body 用 JSON 字符串存储在 Excel 单元格里直观自然两个字段都允许为空不传就自动忽略expected_code 精确匹配状态码expected_msg 做“包含匹配”而不是全等匹配因为大多数响应体是动态的全等断言容易误伤is_run 字段用来临时跳过用例避免注释代码或删除用例。3.2 用 openpyxl 写一个轻量读取工具读取 Excel 我用的是 openpyxl选它的原因很直接只支持 xlsx 格式API 设计清晰读取速度快而且读取时能区分单元格为空还是值为空字符串这对接下来的字段处理很重要。工具类实现如下import os from openpyxl import load_workbook class ExcelUtil: def __init__(self, file_path, sheet_nameNone): self.file_path file_path if not os.path.exists(file_path): raise FileNotFoundError(f测试数据文件不存在: {file_path}) self.wb load_workbook(file_path, data_onlyTrue) self.sheet_name sheet_name or self.wb.sheetnames[0] def get_sheet_data(self): ws self.wb[self.sheet_name] rows list(ws.iter_rows(values_onlyTrue)) if not rows: return [] headers [str(h).strip() if h is not None else for h in rows[0]] data [] for row in rows[1:]: if all(cell is None for cell in row): continue case {} for idx, header in enumerate(headers): value row[idx] if idx len(row) else None case[header] value.strip() if isinstance(value, str) else value data.append(case) return data这里有几个容易踩的坑我提前说明白。load_workbook 必须传data_onlyTrue否则读到的可能是公式而不是计算结果我在初期就是因为忘了这个参数读出来的全是 Noneiter_rows(values_onlyTrue)直接返回单元格值不需要遍历 Cell 对象性能更好表头按顺序与每一行数据对齐时一定要处理“某一列没填”的情况因为 Excel 的空单元格返回 None直接用索引访问会越界。3.3 参数化绑定让 Excel 里的每行数据都变成用例读取数据只是第一步接下来要把每一行数据变成一个真实的 Pytest 用例。利用pytest.mark.parametrize可以完成这个绑定import pytest from common.excel_util import ExcelUtil from common.request_util import RequestUtil from common.assert_util import AssertUtil class TestApi: pytest.fixture(scopeclass, autouseTrue) def setup_class(self): # 类级前置比如初始化请求封装、准备测试数据 yield pytest.mark.parametrize(case, ExcelUtil(data/api_cases.xlsx).get_sheet_data()) def test_case(self, case): if case.get(is_run) ! Y: pytest.skip(f用例 {case.get(case_id)} 标记为不执行) resp RequestUtil().send_request(case) AssertUtil().assert_response(resp, case)这个写法简洁但有两个细节值得优化。第一如果不想在报告里看到大量 skip 用例可以在读取 Excel 时就过滤掉is_run ! Y的行这样未执行用例根本不会进入收集阶段第二parametrize 的 ids 参数可以指定用例名让报告里显示的不是test_case[case0]这种丑陋的名字而是USER_001或查询用户列表成功我会在实际项目中加上这个优化。3.4 维护 Excel 数据时那些“非代码”的坑框架本身没问题但团队同学在准备 Excel 用例数据时经常被困扰这些坑如果不提前说明很容易让人误以为是框架出了 bug。最常见的几个Excel “加载项被禁用”导致功能区功能缺失这种情况通常是插件崩溃或安全策略限制重启 Excel 或到“文件-选项-加载项”里重新启用即可粘贴失效CtrlV 没反应多半是剪贴板与其他软件冲突重启 Excel 进程就能恢复公式下拉不生效常见于设置了自动计算为手动或单元格被设置成文本格式调整计算选项、检查单元格格式可解决。这些问题的共性是它们都发生在“手工维护数据”这个环节不是自动化框架的职责范围但团队协作时必须提前培训。我通常会在项目文档里加一页“Excel 使用注意事项”把这些问题和解决办法列出来省得每次有人遇到都要重新排查一遍。另外强烈建议统一使用 xlsx 格式不要再用 xlsxls 是老格式openpyxl 不支持读取如果从旧项目迁移数据先另存为 xlsx 再说。4. 请求、断言、日志与 Fixture 的封装实践4.1 请求封装一次封装全场景复用请求发送是整个框架的发动机。我要求这个封装必须做到接口只用写一次全项目复用错误处理要统一不能有的地方超时崩溃、有的地方悄悄失败请求日志要自动记录。基于 requests 库的 Session 实现import json import logging import requests class RequestUtil: def __init__(self, base_url): self.session requests.Session() self.base_url base_url self.logger logging.getLogger(__name__) def send_request(self, case): method case.get(method, GET).upper() url self.base_url case.get(uri, ) headers self._parse_json(case.get(headers)) body self._parse_json(case.get(body)) if method in (GET, DELETE): resp self.session.request( method, url, paramsbody, headersheaders, timeout10 ) else: resp self.session.request( method, url, jsonbody, headersheaders, timeout10 ) self.logger.info(请求: %s %s, method, url) self.logger.info(请求头: %s, headers) self.logger.info(请求体: %s, body) self.logger.info(响应: %s %s, resp.status_code, resp.text[:500]) return resp staticmethod def _parse_json(value): if not value: return None if isinstance(value, dict): return value if isinstance(value, str): try: return json.loads(value) except json.JSONDecodeError: return value return value有几个细节解释一下。使用 Session 是为了自动复用连接特别是后续要处理登录态时在同一个 Session 里设置 cookie 或 token 就能做到全局生效GET 和 DELETE 的请求体参数作为 params 传递POST/PUT 作为 json 传递这是大家在接口测试里最容易搞混的地方用 json 参数而不是 data 参数发送是因为 Excel 里的 body 是 JSON 字符串json 参数会自动完成序列化和 Content-Type 设置用 data 反而需要手动处理编码问题。4.2 断言封装不只看状态码很多人做接口自动化只断言状态码这类用例的拦截能力非常弱。我见过接口返回 200 但业务实际失败的场景——响应体里 errorCode500页面报错用例却显示通过。所以断言必须支持“状态码 响应体内容”两层校验import json import requests class AssertUtil: def assert_response(self, resp, case): expected_code case.get(expected_code) if expected_code is not None: assert resp.status_code int(expected_code), ( f状态码校验失败: 期望 {expected_code}, 实际 {resp.status_code} ) expected_msg case.get(expected_msg) if expected_msg: assert expected_msg in resp.text, ( f响应内容校验失败: 未找到期望内容 {expected_msg} )真实项目中我会把断言做得更细一些比如支持 JSONPath 路径断言、支持列表长度断言、支持响应时间断言但泛用性越高代码越复杂。通用框架里我会保留两层核心断言必要时在具体用例中追加自定义断言。效率优先接口自动化的第一价值永远是先把大面积问题拦住而不是模拟所有边界条件。4.3 日志模块出了问题先别翻代码日志模块经常被忽视但排查问题时的作用无可替代。我给框架配了标准化的日志输出每次请求和响应都会自动记录包括状态码、耗时、响应体截断内容。日志级别设置为 INFO既能避免 DEBUG 日志太多也不会漏掉关键信息。真实项目的日志会同时输出到控制台和固定文件确保 Jenkins 上执行完也能追溯import logging import os from datetime import datetime def get_logger(nameapi_test): logger logging.getLogger(name) if not logger.handlers: logger.setLevel(logging.INFO) fmt logging.Formatter( %(asctime)s - %(levelname)s - %(name)s - %(message)s ) sh logging.StreamHandler() sh.setFormatter(fmt) logger.addHandler(sh) log_dir report/logs os.makedirs(log_dir, exist_okTrue) fh logging.FileHandler( os.path.join(log_dir, f{datetime.now().strftime(%Y%m%d_%H%M%S)}.log), encodingutf-8, ) fh.setFormatter(fmt) logger.addHandler(fh) return logger4.4 conftest.py 里放什么登录态、多环境、前置处理conftest.py 是 Pytest 特有的扩展点也是框架里最容易写乱的文件。我的建议是只放跨模块共用的 fixture 和钩子函数不要把所有用例的前置都堆进来。常见的放法是登录态获取、环境切换、以及全局的 Allure 附件信息import pytest import allure from common.request_util import RequestUtil from config.settings import BASE_URL pytest.fixture(scopesession, autouseTrue) def global_token(): 登录获取 token整个测试会话只执行一次 login_body {username: admin, password: 123456} resp RequestUtil(BASE_URL).session.post( f{BASE_URL}/api/v1/login, jsonlogin_body ) token resp.json()[data][token] return token pytest.fixture(scopesession, autouseTrue) def allure_environment(): 生成 Allure 环境信息 with open(report/allure-results/environment.properties, w, encodingutf-8) as f: f.write(fBaseURL{BASE_URL}\n) f.write(TestEnvtest\n)关于 fixture 的作用域选择我来分享一下踩过的教训登录 token 如果用 function 作用域每个用例都重新登录测试过程会多出一堆无意义的时间开销如果用 session 作用域则要小心 token 过期问题。我通常的做法是 session 作用域获取一次然后在请求封装里加入 token 过期自动重新登录的逻辑这样既不重复登录也不容易过期失败。另外不要在 fixture 内部写太复杂的业务逻辑fixture 应该保持简单、可复用、可组合。5. Allure 报告集成让结果自己会说话5.1 环境搭建与依赖安装Allure 的安装分成两部分Python 插件和命令行工具。插件通过 pip 安装命令行工具有多种安装方式macOS 可以直接brew install allureWindows 建议用scoop install allure或者从官网下载压缩包解压后配置 PATH 环境变量。安装完成后用allure --version验证# 安装 Python 依赖 pip install pytest allure-pytest requests openpyxl # 验证 allure 命令行 allure --version这里有个常见的坑allure-pytest 插件装好了但命令行工具没装执行 pytest 时能生成结果文件却无法生成 HTML 报告。所以安装完之后一定要先跑一次allure --version确认命令行工具可用再去跑测试否则你会在生成报告那一步卡住半天查来查去发现是环境变量的问题。5.2 用例标注体系报告里的信息层次Allure 真正强大的地方在于它对测试用例的“标注”能力通过这些标注报告会自动按业务模块、功能点、操作步骤组织起来。我的标注习惯是import allure import pytest from common.excel_util import ExcelUtil from common.request_util import RequestUtil from common.assert_util import AssertUtil allure.feature(用户管理模块) class TestApi: allure.story(查询用户) allure.title(APITEST_001_查询用户列表) allure.severity(allure.severity_level.CRITICAL) def test_case(self, case): with allure.step(发送请求): resp RequestUtil().send_request(case) with allure.step(校验响应): AssertUtil().assert_response(resp, case)Feature、Story、Title、Severity 各司其职Feature 标识业务模块对应报告中的“Behaviors”视图Story 标识功能点进一步拆分模块Title 取代默认的用例名显示更清晰Severity 标记用例级别测试经理可以按严重程度筛选。如果配合参数化还可以用allure.title({case[case_id]}_{case[case_name]})这种动态渲染方式让用例标题自动从 Excel 数据中生成这样每个用例都能在报告里显示出具体的业务场景而不是一个数字编号。5.3 报告生成与发布执行测试和生成报告是两个步骤先跑用例生成 results 文件再用命令行生成 HTML 报告# 执行测试结果写入 report/allure-results pytest --alluredirreport/allure-results --clean-alluredir # 基于 results 生成 html 报告 allure generate report/allure-results -o report/allure-report --clean # 本地预览报告 allure open report/allure-report--alluredir指定结果文件目录--clean-alluredir每次执行前清理旧结果防止历史数据干扰当前报告allure generate的--clean也是同理。如果接入 Jenkins只需要在构建命令里依次执行这两条命令然后在 Post-build Actions 里添加 Allure Report 插件指定报告目录即可。每次构建完成团队成员直接打开 Jenkins 页面就能看到最新的测试报告不需要每个人都装命令行工具。5.4 增强报告可用性的几个小技巧基础报告能用了但还缺少一些对排查问题更友好的信息。我给框架加了两个增强项一是 environment.properties 文件在 conftest.py 里自动写入被测环境地址、浏览器版本、构建号等信息报告首页会显示这些环境信息排查问题时一眼就能知道是哪个环境跑的二是 categories.json 文件把失败原因自动分类比如把 requests ConnectionError 归类为“环境异常”把断言失败归类为“业务校验失败”报告首页的失败图表就更有指导意义。还有一个接口测试特有技巧把每次请求的完整信息注入到 Allure 附件中。Allure 的测试步骤里可以附加文本、JSON、HTML 等类型附件我封装了一个allure.attach(请求参数, json.dumps(case), allure.attachment_type.JSON)的辅助方法用例失败时打开报告就能直接看到发出去的参数和返回的响应体不需要再去翻日志排查效率提升非常明显。6. 常见问题与避坑记录6.1 Excel 数据读取问题速查框架跑起来后最容易出问题的地方反而是数据读取环节这里把典型问题整理成速查表现象可能原因解决办法openpyxl 报错说格式不支持文件是 .xls 老格式另存为 .xlsx或改用 xlrd 读取读出来日期字段是一串数字单元格日期格式与 openpyxl 类型转换冲突读取时判断类型datetime 转字符串再处理参数值首尾多了空格导致请求失败Excel 单元格里有不可见空格读取工具里统一 strip数字类型不做处理读取数据为空但 Excel 显然有内容sheet 名称不匹配或表中只有表头没有数据行检查 sheet_name 参数明确指定目标 sheet6.2 Pytest 执行与用例收集问题用例收集问题最典型的是“明明写了用例却显示没有收集到”。Pytest 默认的文件名规则是test_*.py类名是Test*函数名是test_*三个条件任何一个不满足都会被静默忽略。我会在 pytest.ini 里显式配置python_files、python_classes、python_functions避免团队有人建了文件名api_test.py或类名TestCase不匹配导致用例丢失。参数化过程中有个隐蔽的坑parametrize 的参数名和 Excel 读取出来的字典 key 冲突。比如pytest.mark.parametrize(case, data)如果 data 里本身有一个字段叫casePytest 会有歧义甚至报错。另外 ids 参数和参数列表长度必须一致否则运行时会报 IndexError。我的建议是参数名用case_data这种不太可能出现在 Excel 表头里的名字避开冲突。6.3 Allure 报告生成问题Allure 相关的坑最常见的就是“执行完没有报告”。“无法生成报告”大多是因为allure命令不可用重新配置环境变量即可“报告内容为空”则是因为 results 目录里没有 JSON 结果文件可能是--alluredir参数写错路径也可能是 pytest 执行本身失败导致没有产出。还有一个容易被忽略的问题用--alluredir多次执行时残留的旧结果会和当前结果混在一起。虽然--clean-alluredir会清空目录但有些版本或某些 CI 场景下仍然可能出现历史数据干扰。我的习惯是在 generate 时配合--clean并且用日期或版本号区分不同构建的 results 目录报告里就能保留历史趋势数据新数据也不会污染旧数据。6.4 提升框架实用性的进阶建议框架跑顺了之后有几个性能与稳定性方向值得继续投入。并发执行用pytest-xdist-n 4指定 4 个进程并行跑执行时间能缩短一半以上但前提是用例之间没有数据依赖或者已经在 token 隔离层面做好了处理失败重跑用pytest-rerunfailures给脆弱的网络用例设置--reruns 2 --reruns-delay 1减少因为偶发网络问题带来的误报。接入 CI 时建议在 Jenkins 上配置定时触发比如每天凌晨跑全量回归提交代码触发跑冒烟用例。框架本身不解决流程问题但它能倒逼团队把测试流程规范起来。我的经验是先跑通一条完整链路再逐步加复杂度不要第一次就追求并发、重试、多环境完整版那样只会让排错成本高到把团队积极性消耗掉。我个人在实际搭建中体会最深的是“框架进化是迭代出来的不是设计出来的”。这套 Pytest Allure Excel 的组合最初只是一个能跑通单个接口的脚本后来慢慢长了 Excel 读取、日志、断言封装再后来接了 Allure 报告、Jenkins 定时任务一步步变成现在这个样子。每次新增能力都来自一个真实的痛点而不是拍脑袋想出来的。如果你现在也在搭接口自动化框架我的建议很直接先别急着建一堆模块先用最笨的方式把一条用例跑通再沿着“可维护、可扩展、看得懂”三条线慢慢补框架自然会成长成适合你们团队的样子。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进