多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

接口自动化测试框架从零搭建:Python+pytest+Allure实战全流程

接口自动化测试框架从零搭建:Python+pytest+Allure实战全流程 HCLA第六次学习作业终于出来了这次训练计划的核心目标是让我独立完成一次接口自动化测试的全流程。作业要求并不复杂给一个模拟的图书借阅管理系统写一套可复用的自动化接口测试框架要求覆盖核心业务链路、异常场景、环境切换并且输出一份能说明问题的测试报告。乍一看平平无奇真正动手之后才发现这种开放式作业比想象中难得多——没有明确的技术栈约束没有现成脚本可以抄从零到一全得自己拍板。这篇文章就把我这次作业的完整过程拆开写一遍包括需求拆解、技术选型、框架搭建、用例设计、踩坑实录和最终交付给同样在做这类作业的人一个可参考的路线。1. 拿到作业之后需求拆解与技术选型1.1 作业原题与我的理解作业原题其实只有一句话针对模拟项目X一个图书借阅管理系统编写接口自动化测试框架覆盖登录、图书管理、借阅管理三类核心接口最终提交测试代码和测试报告。要求里额外说明了几点代码必须能跨环境执行测试数据需要自管理报告要能直观反映问题。第一眼觉得不难细细一想全是坑。“跨环境执行”意味着配置不能写死接口地址、账号、数据库状态都要和环境解耦。“测试数据自管理”意味着我不能依赖目标系统预置数据得自己在用例执行前造数据、执行后清数据。“报告要直观”意味着我不光要把用例跑绿还要让团队看一眼就知道哪些接口、哪些场景出问题。我花了差不多一晚上把需求翻译成具体任务清单搭一个最小可用的接口自动化框架能够独立运行、定时触发封装API调用层让用例编写者不用关心HTTP细节设计一套覆盖正常流程和异常分支的用例集数据准备与清理独立于业务用例输出可阅读、可归档的测试报告。1.2 为什么选Python requests pytest这套组合技术选型没有标准答案但有一个朴素标准团队里谁都能上手维护。这次作业我选的是Python 3.10 requests pytest Allure理由很直接。Python在接口测试领域基本属于默认选项requests库封装的语义贴近HTTP协议本身几乎没有学习成本。pytest的优势在于fixture机制和参数化能力fixture可以用来做环境初始化、token获取、数据清理参数化可以让一条用例跑多组数据正好命中作业“数据自管理”的要求。Allure报告这边我犹豫过要不要换成pytest-html。pytest-html部署简单、零配置但Allure的好处是历史趋势、失败分类、步骤日志这些信息组织得更清晰。考虑到作业要求“报告直观”最终选了Allure代价是本地要装Java运行时环境后面也踩了环境变量相关的坑。Docker当时也被纳入选型范围想着把整个测试环境容器化但权衡之后放弃了。原因很简单——作业本身是训练测试能力把时间花在Dockerfile和CI编排上反而冲淡了核心目标。我最后只在代码里预留了环境配置接口换环境只需要改一个配置文件暂时不上容器。1.3 环境准备比想象中容易踩坑的一步环境准备我分了三块Python依赖、Allure命令行工具、项目初始化。难度不高但确实遇到一个让我折腾半小时的问题。Python依赖我用requirements.txt管理核心就几样requests2.31.0 pytest8.0.0 allure-pytest2.13.2 python-dotenv1.0.0简单吧但安装完成之后运行pytest直接报错找不到allure命令。查了一圈发现allure-pytest只是pytest插件它和Allure命令行工具完全是两个东西——插件负责收集执行数据、生成结果目录而命令行工具负责把结果目录渲染成HTML报告。这俩一个都不能少。注意网上很多教程直接把安装allure-pytest当作安装Allure这其实是个信息差。你需要单独安装Allure命令行工具并且在系统环境变量里配好allure的路径才能正常生成报告。环境这块我的建议很简单Windows用户直接用Scoop或下载zip包解压然后把bin目录配进PATHmacOS或者Linux用户直接用Homebrew或apt安装省心很多。配完之后在终端敲一下allure --version确认版本可用再做下一步。2. 框架搭建让用例层只关心业务其他都交给封装2.1 目录结构设计与分层思路自动化测试框架最忌讳把所有代码堆在一个文件里这次作业我对目录结构做了认真规划分层思想是参照实际项目中常用的“配置-接口-用例-数据-工具”五段式。hcla_assignment_6/ ├── config/ │ ├── __init__.py │ ├── settings.py │ └── environments/ │ ├── dev.ini │ └── test.ini ├── api/ │ ├── __init__.py │ ├── base_client.py │ ├── auth_api.py │ ├── book_api.py │ └── borrow_api.py ├── tests/ │ ├── __init__.py │ ├── conftest.py │ ├── test_auth.py │ ├── test_book_manage.py │ └── test_borrow_flow.py ├── data/ │ ├── login_data.json │ ├── book_data.json │ └── borrow_data.json ├── utils/ │ ├── __init__.py │ ├── data_cleaner.py │ ├── db_helper.py │ └── logger.py ├── reports/ ├── requirements.txt └── pytest.ini分层思路说白了就是一句话上层只关心业务下层把复杂度吃掉。用例层不用关心token怎么存、请求怎么发、数据怎么清理那些都是下层封装的事。这样一来后续任何人新加用例只需要调用api模块对应方法再加断言半小时能上手。2.2 API客户端的封装让用例层只关注业务框架的核心在base_client.py这个类承担了HTTP请求的公共逻辑。设计的时候我反复想一个点requests库本身就是很好的封装了我到底要包什么包的东西至少得有三块价值——统一处理请求头、自动管理token、统一异常处理和日志记录。import requests import logging from config.settings import ENV_CONFIG logger logging.getLogger(__name__) class BaseClient: def __init__(self): self.base_url ENV_CONFIG[base_url] self.session requests.Session() self.token None def set_token(self, token: str): self.token token def _update_headers(self): if self.token: self.session.headers.update({Authorization: fBearer {self.token}}) def request(self, method: str, url: str, **kwargs): self._update_headers() full_url f{self.base_url}{url} logger.info(f请求 {method} {full_url} - 参数 {kwargs.get(params, )} - 数据 {kwargs.get(json, )}) try: response self.session.request(method, full_url, timeout10, **kwargs) logger.info(f响应 {response.status_code} - 内容 {response.text[:500]}) return response except requests.Timeout: logger.error(f请求超时: {method} {full_url}) raise except requests.RequestException as e: logger.error(f请求异常: {str(e)}) raise def get(self, url, **kwargs): return self.request(GET, url, **kwargs) def post(self, url, **kwargs): return self.request(POST, url, **kwargs) def put(self, url, **kwargs): return self.request(PUT, url, **kwargs) def delete(self, url, **kwargs): return self.request(DELETE, url, **kwargs)这里有一个关键设计我用了requests.Session()而不是每次调用requests.request()。Session在底层会复用TCP连接一组用例跑下来能省掉大量握手开销同时自动维护Cookie对登录状态保持有很大帮助。token我采用显式set的方式登录之后把token赋给客户端后续所有请求自动带上Authorization头。这个设计后来被证明是对的因为模拟项目X的登录接口返回的token有效期只有120分钟如果每次请求都手动传token用例会写得又啰嗦又容易断。封装好之后用例层完全感知不到token的存在。2.3 conftest.py中的关键fixturepytest的fixture机制是这套框架的另一个支柱。我写了三个核心fixtureclient、logged_in_client、clean_test_data。import pytest from api.base_client import BaseClient from api.auth_api import AuthAPI from utils.data_cleaner import cleanup_borrow_records, cleanup_books, cleanup_users pytest.fixture(scopesession) def client(): base_client BaseClient() return base_client pytest.fixture(scopesession) def logged_in_client(client): auth_api AuthAPI(client) token auth_api.login() client.set_token(token) return client pytest.fixture(autouseTrue) def clean_test_data(): # 用前置清理的方式避免历史脏数据影响本次执行 cleanup_borrow_records() cleanup_books() cleanup_users() yield # 后置清理保证下一次执行环境干净 cleanup_borrow_records() cleanup_books() cleanup_users()scopesession的fixture在整个测试会话中只执行一次登录接口不会被打爆token只取一次后续所有用例复用。autouseTrue的fixture则每个用例前后自动执行清理避免了“上一个用例留下的数据干扰下一个用例”这种经典问题。这个设计对应了作业里“测试数据自管理”的要求而且我把清理放在前向和后向各做一次。前向清理防止历史脏数据后向清理保证执行完不留垃圾。某些团队只做后置清理遇到前天失败的测试留下半条脏数据第二天跑批就随机性失败定位问题浪费半天。2.4 配置管理与环境隔离配置管理这块我用settings.py加载环境配置文件环境名通过命令行参数传入。每个环境一个ini文件里面放base_url、超时时间、账号信息等。import configparser import os ENV_NAME os.getenv(TEST_ENV, dev) _config configparser.ConfigParser() _config.read(fconfig/environments/{ENV_NAME}.ini) ENV_CONFIG { base_url: _config.get(server, base_url), timeout: _config.getint(server, timeout), username: _config.get(account, username), password: _config.get(account, password), }执行的时候很简单TEST_ENVtest pytest就切到测试环境默认跑dev环境。配置和代码完全解耦换环境不需要改一行代码。这个做法虽然简单但解决了个真实痛点我见过太多团队在测试代码里写死http://localhost:8080交付之后要连到CI环境就必须改一坨代码改完又不敢保证没改出别的问题。3. 用例设计从接口文档到可重复执行的测试场景3.1 从接口文档抽出的核心场景拿到模拟项目X的接口文档后我没有直接埋头写用例而是先画了一份业务流程图把所有接口的调用关系和状态流转理清楚。这是一个很值得推荐的步骤——接口文档只是一堆端点列表真正有价值的用例必须建立在业务链路上。模拟项目X的核心链路是登录获取token查询图书列表创建图书借阅图书归还图书删除图书。六个动作形成一条完整的数据生命周期。我围绕这条链路设计了20条用例按模块分成三组覆盖了正常流转、参数异常、权限异常、数据冲突四类场景。拿图书管理模块举例我最终设计的核心用例包括正常创建图书成功返回图书ID创建重复ISBN图书返回业务错误码未登录状态创建图书返回401借阅不存在的图书返回404借阅库存为0的图书返回库存不足错误码同一用户重复借阅同一本在借图书返回冲突错误码。3.2 参数化驱动让一条用例覆盖多组数据pytest的参数化是我觉得这次作业里最出彩的部分。写接口用例最枯燥的是同一逻辑要跑不同数据如果每条数据写一个test函数代码量爆炸且维护困难。参数化能让数据与逻辑分离一条函数覆盖一组数据新增测试数据只需要改数据文件。import pytest from api.book_api import BookAPI pytest.mark.parametrize(book_data,expected_code, [ ({title: 测试图书A, isbn: 978-7-111-11111-1, stock: 5}, 200), ({title: , isbn: 978-7-111-11111-2, stock: 5}, 400), ({title: 测试图书B, isbn: invalid_isbn, stock: 5}, 400), ({title: 测试图书C, isbn: 978-7-111-11111-3, stock: -1}, 400), ], ids[normal_create, empty_title, invalid_isbn, negative_stock]) def test_create_book(logged_in_client, book_data, expected_code): book_api BookAPI(logged_in_client) response book_api.create_book(book_data) assert response.status_code expected_code参数化配合ids参数还能生成可读性极好的测试用例名Allure报告里一眼能看到每条数据对应的场景名称而不是笼统的test_create_book[data2]。这一点在交付报告时很加分老师或组长看报告不需要去翻代码。3.3 数据准备、执行与清理保证用例可重复运行测试数据自管理这个要求我用了“前置准备后置清理”的方式实现。创建图书之前我先通过数据库直连查询确认不存在相同ISBN的记录存在则先删掉借阅之前先确认图书库存大于0不够就通过接口补足。这样每次执行都从确定状态开始。这里有个值得说的小细节数据清理分三条路径执行——接口路径、数据库路径和专门的清理工具类。大部分团队习惯完全依赖接口做数据清理但这有一个隐患如果接口本身有bug导致数据创建成半残状态再用接口去清理同样会失败。数据库直连清理是兜底方案权重更高。import pymysql from config.settings import ENV_CONFIG DB_CONFIG { host: ENV_CONFIG[db_host], port: ENV_CONFIG[db_port], user: ENV_CONFIG[db_user], password: ENV_CONFIG[db_password], database: ENV_CONFIG[db_name], } def cleanup_books(): conn pymysql.connect(**DB_CONFIG) cursor conn.cursor() cursor.execute(DELETE FROM book WHERE title LIKE 测试图书%) conn.commit() cursor.close() conn.close()直接用LIKE 测试图书%而不是删除全部数据是刻意的安全设计。万一误操作指向生产环境这个语句只会删除带测试前缀的数据不至于酿成大祸。这种“删除条件永远加前缀过滤”的习惯看起来多此一举遇到真实事故时能保命。4. 跑批实录六个反复出现的问题及处理过程4.1 token失效session复用带来的隐性问题第一轮跑批我用了session级别的登录fixture本意是让整场测试只登录一次提升效率。结果用例跑完第13条的时候大量失败突然出现报错清一色是401。开始以为是token过期查了时间才发现程序才跑了不到3分钟远没到120分钟的过期时间。继续排查发现根因在另外一个地方session级别的fixture会复用同一个session对象token本身没问题但模拟项目X的后端做了一处特殊处理——同一个token在前一次请求返回401之后会自动失效。也就是说只要第13条用例触发了401我故意设计的未登录场景用例整个session后续全部跟着失效。这个行为在后端层面可能是为了安全但在自动化测试里会造成连锁失败。解决方案是调整fixture范围把client和logged_in_client都改成function级也就是每条用例都用独立的session。代价是登录接口被多调用几次但网络开销完全可接受换来的是用例之间彻底隔离。这个调整让我意识到接口自动化测试中用例隔离是第一优先级任何可能的串联影响都应该提前避免。4.2 断言不稳定时间戳、排序、随机值第二轮跑批相对顺利但出现了三个偶发失败而且失败用例每次还不一样。这类不稳定断言是自动化测试里最讨厌的问题比稳定失败的bug难查一百倍。逐个定位后三个问题分别是我自己造成的第一个用例断言了返回数据里的create_time字段我直接assert了精确到秒的字符串但接口实际返回值精确到毫秒偶尔同一秒内执行就相等跨秒就失败第二个用例查询图书列表后断言第一本书的ID但列表没有固定排序数据稍有变化就断言错位第三个是接口偶尔在返回体里带上一个随机生成的内部编码我当时没意识到这个字段是随机的。修复思路很明确时间类断言全部改成“解析为datetime后误差在30秒内”列表数据先按ID排序再断言随机值只断言字段存在和类型正确不去比对具体内容。这类断言问题本质上不是被测系统的bug而是测试代码的过度约束。写断言之前一定要先确认哪些字段是确定性的哪些不是。4.3 多条用例失败后的连锁反应第三轮跑批我故意在一个用例里埋了“创建图书后不清理数据”的雷想看系统会不会出现连坐问题。果不其然后面的用例连续失败原因是创建重复ISBN图书的用例失败导致库存状态被污染后续所有依赖库存数量的用例全部异常。这个现象在实际项目里太常见了。一个人写的几条用例产生脏数据把整批搞得没法看最后大家分不清是系统bug还是测试互相干扰。这次作业之后我给自己定了一条铁律每条用例的清理逻辑必须覆盖成功和失败两条路径任何一条用例在finally块里都要尝试恢复现场。4.4 环境切换时的配置遗漏换到测试环境跑批的时候新增了一批失败都是连接被拒绝。我第一反应是目标环境服务挂了登录上去一看服务正常运行。后来发现是配置文件里数据库端口写错了——dev环境的MySQL在3306端口test环境在3307我复制dev.ini改了几个字段唯独漏改了端口。这个坑逼着我在配置管理上做了补充校验新增一个config_check()函数启动时自动读配置检查base_url通不通、数据库连不连得上、账号密码能不能登录任何一个环节不通直接打印醒目的错误并停止执行。宁可启动时多花3秒做自检也不要跑完20条用例才发现环境配错了。5. 测试报告把数据变成能看懂的信息5.1 Allure报告的定制与处理Allure默认生成的报告信息量很大但也有一些干扰项。我针对作业验收场景做了一些定制尽量让报告一眼能看到核心结论。报告里我重点加了两类信息一是每个用例的步骤日志Allure支持with allure.step()包装步骤这样点击任意一条用例都能看到它调了哪些接口、传了什么参数、断言哪一步出了问题。二是在套件描述里写清楚执行范围、环境信息和数据准备情况让看报告的人不用猜。下面是这样包装的步骤日志import allure from api.book_api import BookAPI allure.step(创建图书标题 {title}ISBN {isbn}) def create_book_step(client, title, isbn): return BookAPI(client).create_book({title: title, isbn: isbn, stock: 5})步骤日志的另一个好处是问题复现的成本大幅降低。之前排查失败用例要看日志文件找请求参数现在直接在报告里点开步骤参数、返回体、耗时全都有省了来回沟通的时间。5.2 覆盖率统计里的意外数据作业没有硬性要求统计覆盖率但我自己用coverage跑了一下得到的结果有点意外。接口覆盖率很容易到100%——所有接口都被至少调用过一次但分支覆盖率只有61%。这个差距来自异常分支。我的20条用例里正常场景占比偏高很多接口的错误分支没有覆盖到比如库存为0时的借阅、权限不足时的删除、并发场景下的重复借阅。这些分支恰恰是上线时最容易出问题的部分。我调整了用例分布把异常场景用例比例从30%提升到接近50%分支覆盖率从61%拉到78%。这个调整也改变了我的测试观念覆盖率高不等于测试质量高盲目追求接口覆盖率没有意义真正的价值在分支和边界。5.3 这次作业我给自己打分最后给自己做个复盘如果满分10分我给这次作业8分。扣掉的2分一分是因为并发场景没有覆盖——模拟项目X有一个接口专门处理并发借阅我因为时间原因只做了单线程测试这是明确的知识缺口另一分是数据库直连的账号密码直接写在配置文件里虽然这是模拟环境但我清楚真实项目不能这么干后续要接入密钥管理。如果时间再多一周我大概率会做三件事补充并发场景用例把框架打包进Docker做一个可一键启动的测试环境以及把报告自动发送到团队群。可惜作业有deadline这些只能放到下次迭代。不过换个角度想恰恰是这次作业让我真正把接口自动化的完整链路走了一遍。理论背一百遍不如亲手从选型、搭框架、写用例、修坑、出报告这一个完整闭环里跑一遍。很多文档里不会写的东西——session级别的连锁问题、断言不稳定的坑、配置漏改的代价——只有自己踩过才会长记性。这也算这次作业最大的收获。
返回列表