☰
Python+Requests接口自动化测试框架搭建:从工具到代码的完整实践
2026/10/2 3:58:43 网站建设 项目流程

我早几年刚接触接口测试那会儿,习惯打开Postman对着接口文档一个个点,点完把响应复制到Excel里人工核对。等用例攒到三五十条就开始后悔:参数散在一个个请求里,项目一换环境就得手动改URL,跑完一轮也说不清哪些字段被改动影响到了。下定决心用Python+Requests自己糊一套基础接口自动化测试框架,跑顺之后舒服了很多——回归、新版本验证、问题定位都变得有迹可循。这篇就是把我整个搭建过程、设计思路、踩过的坑一次性理清楚,适合已经会用一点Python、正打算从工具转向代码方式做接口测试的朋友,也适合团队里准备从零铺自动化测试的同学当作起步参考。

这套东西本质上不复杂:Requests负责发HTTP请求,pytest负责组织和执行用例,再加上一层数据驱动、日志和报告。难点从来都不是“请求怎么写”,而是怎么让用例可持续维护、失败之后能快速定位。接下来我会按“为什么搭”“怎么设计”“怎么落地”“遇到问题怎么排”的顺序完整讲一遍。

1. 为什么要自己搭一套Requests框架,而不是拿来即用

很多人会问,Postman、JMeter、YApi这些现成工具都支持接口测试,为什么还要写代码?这个问题我每次做技术分享都会被问。先说结论:现成工具适合小规模验证,一旦用例数量上来、断言逻辑变复杂、需要和CI/CD流水线联动,代码方案的维护成本会明显低于纯手工维护工具条目。

1.1 接口自动化到底在解决什么问题

接口自动化测试的核心价值不是“省去手工点击”,而是把三类工作自动掉:第一类是回归验证,版本迭代时确保老接口没被改坏;第二类是契约检查,前后端联调阶段用自动化脚本快速发现字段名、类型、状态码对不上;第三类是数据准备和清理,很多业务场景需要先创建订单、再查询、再取消,这类多步骤接口调用用手工脚本执行既慢又容易出错。

我见过不少团队把接口自动化做成了“Postman集合+手动触发”,本质上还是一个半自动状态。真正跑起来之后,最值钱的部分其实是“每个用例的断言和日志”。断言告诉你哪个字段错了,日志告诉你当时发了什么、收回了什么,两者结合才能让失败可复现。代码框架里的请求对象、断言函数、日志记录器都是为这个目标服务的。

1.2 为什么选择Requests而不是Java体系

做接口自动化测试,主流语言其实就是两类:Python和Java。Java这边常见组合是RestAssured+TestNG+Jenkins,学习曲线明显陡一些,尤其在依赖管理和Maven/Gradle配置上会花掉不少时间。Python这边Requests库的API设计非常贴近自然语言,一个post请求只需要三四行,新人半天就能上手。

关键差别在调试成本。Python交互式环境里可以直接跑代码,拿到响应对象就能立即处理;Java每次改完都要走编译、打包、运行整套流程。对于接口测试这种“频繁改参数、频繁跑单条用例”的工作节奏,Python的实时反馈优势太明显了。再加上pytest的参数化和fixture机制,写数据驱动用例比Java生态里的做法简洁不少。

我个人的建议是:如果你的团队主语言是Java、测试开发人员也都是Java背景,那就选RestAssured;如果团队里有Python技术栈、或者测试团队想用最轻的代价起步,Requests这套方案会是更划算的选择。工具是服务目标的,不要为了技术栈的“统一”而牺牲开发效率。

1.3 基础框架先求实用,别一上来就套企业级模板

网上搜“接口自动化测试框架”,你会看到各种分层严密的设计:config层、contract层、model层、decorator层、自定义断言引擎、报告服务……这些设计在企业级大项目里确实有价值,但如果你一个人维护、用例只有几十条,过度设计会变成负资产。

我踩过的坑就是第一次搭框架时按公司级模板写了一大堆抽象类,结果每次加新接口至少要改四个文件。后来简化成“请求封装+接口对象+数据文件”三层,反而清爽得多。基础框架的第一原则是:新增一条用例的路径越短越好。你写框架,是为了让后面的人(包括几个月后的自己)快速加用例,而不是为了展示设计模式的运用。

2. 动手前的准备:环境、目录与依赖管理

这个章节看起来基础,但环境问题恰恰是新手放弃的第一道坎。我在本地和公司服务器上都部署过这套框架,遇到过各种诡异的环境问题,先讲清楚怎么避坑。

2.1 Python版本与虚拟环境选择

请直接使用Python 3.8及以上版本,目前Requests库对低版本3.6的兼容性已经不如从前,某些新版依赖(比如urllib3 2.x)对Python版本有硬性要求。Windows下安装时记得勾选“Add Python to PATH”,不然命令行里敲python会弹出微软商店的提示。

然后是虚拟环境,这个几乎是我每次都要强调的。不要图省事直接pip install到全局环境。一台开发机上往往有多个项目,每个项目的依赖版本不同,全局安装会把环境搅乱。我平时习惯在项目根目录执行:

python -m venv .venv

Windows下激活:

.venv\Scripts\activate

macOS/Linux下激活:

source .venv/bin/activate

激活后命令行前面会多出(.venv),这时候再装依赖就不会污染全局环境。

2.2 项目目录结构设计

我最终落地的目录结构比较朴素,但扩展性足够:

api_test_framework/ ├── api/ # 接口对象层 │ ├── __init__.py │ ├── client.py # 统一请求封装 │ └── user_api.py # 用户相关接口 ├── data/ # 测试数据 │ ├── login.json │ └── user_info.json ├── tests/ # 测试用例层 │ ├── conftest.py # pytest fixture │ ├── test_login.py │ └── test_user.py ├── utils/ # 工具类 │ ├── logger.py │ └── file_reader.py ├── config/ │ └── settings.py # 环境配置 ├── report/ # 测试报告输出 ├── logs/ # 日志输出 ├── requirements.txt └── pytest.ini

api层放接口封装,data层放请求数据和期望值,tests层放测试逻辑,utils层放公共工具。config目录保存不同环境的base_url和账号信息,避免把环境相关配置硬编码在用例里。这个结构讲白了就是“接口和用例分离,数据与代码分离”,维护时改接口只动api层,改参数只动data层。

2.3 requirements.txt与依赖安装

requirements.txt内容参考:

requests==2.31.0 pytest==7.4.3 pytest-html==4.1.0 openpyxl==3.1.2

pytest-html用来生成测试报告,openpyxl用于后续读取Excel文件。安装时用国内镜像源可以明显提升速度:

pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

顺带提一个我常遇到的问题:pip安装时报“exceeded retry limit, last status: 429 too many requests”,这个通常就是pip默认源或者taobao等镜像被限流了,换一个源多试几次基本能解决。这不是代码问题,是网络问题,稍后在第五节我把完整的排查思路再展开。

3. 从第一个请求到可复用的接口封装

框架的核心是请求层。这里我从最原始的Requests请求讲起,一步一步把代码演进到“可复用、可配置、可诊断”的程度。你会看到每一步都有明确的动机,而不是我一开始就写好最终版让你背。

3.1 Requests库最简单的请求写法

先看一段最基础的POST请求:

import requests resp = requests.post( url="http://127.0.0.1:8000/api/login", json={"username": "admin", "password": "123456"}, headers={"Content-Type": "application/json"}, timeout=10 ) print(resp.status_code) print(resp.json())

这段代码能跑,但离“测试框架”还差得远。至少存在四个问题:URL写死在代码里;每个请求都要手写headers;没有超时保护的话,遇到网络问题测试会一直挂着;还有响应内容没有做统一记录,失败时看不清现场。

很多初学者在这段代码上原地踏步,因为“能跑”反而阻碍了继续封装。接下来我逐个解决这些问题。

3.2 为什么一定要使用Session而不是裸调requests.get/post

Requests库提供了Session对象,我第一次接触时没当回事,后来才明白这是整个框架的根基。Session有两个核心能力:自动维持Cookie和默认配置持久化。

登录类系统时,先调用登录接口拿到cookie或者token,之后所有请求都得带上。用裸的requests.get/post每次都要手动组装headers,而Session只需要在初始配置时设置一次:

session = requests.Session() session.base_url = "http://127.0.0.1:8000" # 注意Session本身没有base_url,这里要自己维护 session.headers.update({"Content-Type": "application/json"})

Session还能自动复用底层TCP连接,减少握手次数,这是大量请求时性能稳定的关键。构建框架时,一律通过Session发请求,别再用模块级函数。

因为我做的是“基础框架”,所以不引入过于复杂的依赖注入,直接用类的属性存base_url就够用。

3.3 统一请求封装层:Client类

我写了一个client.py作为所有请求的入口:

import requests from urllib3.util.retry import Retry from requests.adapters import HTTPAdapter class ApiClient: def __init__(self, base_url): self.base_url = base_url.rstrip("/") self.session = requests.Session() self.session.headers.update({"Content-Type": "application/json"}) self._init_retry() def _init_retry(self): retry = Retry( total=2, connect=2, read=2, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504] ) adapter = HTTPAdapter(max_retries=retry, pool_connections=10, pool_maxsize=10) self.session.mount("http://", adapter) self.session.mount("https://", adapter) def request(self, method, path, **kwargs): if kwargs.get("timeout") is None: kwargs["timeout"] = 10 url = self.base_url + path resp = self.session.request(method, url, **kwargs) return resp

这个类做了什么?第一,统一管理base_url,环境切换时只需要改一个配置;第二,内置重试机制,面对服务端临时性的500错误可以自动重试;第三,设置默认超时10秒。值得说明的是重试机制,Retry的backoff_factor表示重试间隔按指数退避计算,第一次隔0.5秒,第二次隔1秒,把这个值设得太大容易拖慢整体回归时间。

使用的时候很简单:

client = ApiClient("http://127.0.0.1:8000") resp = client.request("POST", "/api/login", json={"username": "admin", "password": "123456"})

3.4 接口对象层:把URL和参数变成可以调用的方法

如果所有的测试用例里都直接用client.request("POST","/api/login",...),那接口路径还是散落的。我增加了一个api层,每个业务模块对应一个类:

class UserApi: def __init__(self, client): self.client = client def login(self, username, password): return self.client.request( "POST", "/api/login", json={"username": username, "password": password} ) def get_user_info(self, user_id): return self.client.request("GET", f"/api/users/{user_id}") def update_user(self, user_id, payload): return self.client.request("PUT", f"/api/users/{user_id}", json=payload)

这一步提供的好处很明显:测试用例里调用login(),即使接口路径变了,只有user_api.py一个文件要改;即使接口路径需要动态拼接,也被封装在方法里了。接口对象层的命名原则是“动词+名词”,一眼就能看出操作意图。

至此,从Requests裸调用到统一的请求入口和对象封装已经完成。这套架构虽然简单,但足以覆盖大多数接口测试场景,包括登录态传递和带token的鉴权请求(token可以先登录再注入到client.session.headers里)。

3.5 数据驱动:JSON与Excel怎么选

用例数据不要写在Python代码里,这是框架设计的重要原则。维护数据文件和修改Python代码的门槛完全不同,数据驱动之后,不会写代码的测试人员也能添加用例。

我最常采用的是JSON数据文件,结构清晰且天然支持嵌套:

{ "cases": [ { "id": "TC001", "name": "正常登录", "request": { "username": "admin", "password": "123456" }, "expected": { "status_code": 200, "code": 0, "message": "success" } }, { "id": "TC002", "name": "密码错误", "request": { "username": "admin", "password": "wrong_pass" }, "expected": { "status_code": 200, "code": 1001, "message": "invalid password" } } ] }

读取和参数化的代码:

import json, pytest from api.client import ApiClient from api.user_api import UserApi def load_cases(path): with open(path, encoding="utf-8") as f: data = json.load(f) return data["cases"] class TestLogin: @pytest.fixture(autouse=True) def setup(self): self.client = ApiClient("http://127.0.0.1:8000") self.user_api = UserApi(self.client) @pytest.mark.parametrize("case", load_cases("data/login.json")) def test_login(self, case): resp = self.user_api.login( case["request"]["username"], case["request"]["password"] ) body = resp.json() assert resp.status_code == case["expected"]["status_code"] assert body["code"] == case["expected"]["code"]

Excel作为数据文件的优势是业务人员编辑方便,适合用例量特别大且需要多人协作的场景。但Excel读取需要openpyxl,还要处理日期格式、合并单元格、空行等问题,编码和格式的坑很多。我的建议是:团队里有非技术成员维护用例,就用Excel;否则一律用JSON,简单直接,在Git里diff也方便。

3.6 登录态与Token处理

大多数测试项目绕过不了一个问题:测试受保护接口时必须先登录。在fixture里做登录并注入token,是比较标准的做法:

@pytest.fixture(scope="session", autouse=True) def global_login(): client = ApiClient("http://127.0.0.1:8000") user_api = UserApi(client) resp = user_api.login("admin", "123456") token = resp.json()["data"]["token"] client.session.headers.update({"Authorization": f"Bearer {token}"}) return client

这个fixture的scope是session,整个测试会话只登录一次,大幅减少重复登录带来的时间和服务器压力。如果你测试的系统对登录态有时效限制,再把scope改成class或者function都行,根据业务来调整。

4. 断言、日志、报告:让测试结果讲人话

用例能跑通只是第一步,真正体现框架质量和排障效率的是断言、日志和报告。很多自建框架死在这个环节:用例跑完一片红,但没有人知道为什么红。

4.1 断言:不要只检查HTTP状态码是200

我见过太多“接口自动化”其实就是“状态码检查器”,断言永远只有一行:assert resp.status_code == 200。这远远不够。举个例子,接口返回200但业务字段code=1001表示密码错误,这时候测试通过了吗?当然没有。接口自动化的核心是业务断言,至少包含三层:

  • 第一层是HTTP状态码,确认请求有没有被正确处理;
  • 第二层是业务状态码,确认业务逻辑是否符合预期;
  • 第三层是关键字段值,确认数据内容是否正确。

所以我在数据文件里设计expected结构时,就要求把status_code、code、message、甚至data里面的关键字都存在里面,这样断言逻辑自动扩展。如果你的校验逻辑特别复杂,比如要校验一个数组里所有元素的格式,那就在用例代码里补写一段自定义断言,不要强行塞进数据文件。

另外建议在断言失败时输出上下文。pytest的assert本身能打印表达式值,但接口测试失败往往需要看到请求地址和响应原文,所以我在断言前会把请求信息打出来,这个小习惯能省很多排查时间。

4.2 日志记录:关键时刻要有完整现场

测试失败不可怕,可怕的是失败之后没有日志,只能盲猜。接口测试框架的日志至少需要记录:请求方法、请求路径、请求参数、响应状态码、响应内容、耗时。我自己封装的logger用法很简单:

import logging def get_logger(name): logger = logging.getLogger(name) if not logger.handlers: handler = logging.FileHandler("logs/api_test.log", encoding="utf-8") formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.INFO) return logger

然后在ApiClient.request方法里加上日志埋点:

logger = get_logger("api_client") logger.info(f"Request: {method} {url}, params={kwargs.get('json') or kwargs.get('params')}")

加日志的时机要克制,不是每行代码都打。请求发出前打一次,响应回来后打一次,只有出错时打error级。日志文件建议按天或者按大小轮转,不然跑半个月就得几个GB。

4.3 HTML报告:pytest-html还是Allure

报告这件事,基础框架我不建议一上来就上Allure。Allure很强大,界面好看,但需要安装Java运行时、下载Allure命令行工具,还要求和pytest-allure插件配合,环境成本高。基础阶段用pytest-html就足够了:

pytest -s -v --html=report/api_test_report.html --self-contained-html

--self-contained-html参数会把CSS和JS都内嵌到一个文件里,方便直接发送给别人。pytest-html生成的报告会列出每个用例的通过/失败状态、耗时、错误信息,还能在用例里通过注释添加描述。

如果你对报告有更高的要求,比如要历史趋势、分类统计、失败截图、步骤回放,后续可以平滑迁移到Allure。迁移成本并不高,因为测试代码里只需要在用例函数上加一句:

import allure @allure.title("正常登录") @allure.description("验证正确账号密码可以返回成功信息")

其他架构不需要变动。我个人的建议是:报告是给人看的,先解决“有”的问题,再解决“好看”的问题。团队对结果可视化有明确需求时再上Allure。

4.4 把这些东西串起来:一条用例的完整生命周期

最后用一条用例来展示整个框架的协作路径:读取data/login.json里的TC001,pytest解析参数后调用UserApi.login,ApiClient通过Session发出POST请求,请求前后写入日志,响应返回后执行三段断言,最终pytest汇总结果并生成HTML报告。

这个过程里,假如TC002失败,日志文件里能看到请求体的用户名密码,报告里能看到断言表达式和实际返回体的code值,日志与报告互为补充,定位问题的路径是清晰的:先看报告里失败的是哪一个断言,再对照日志看当时发的具体报文。做到这一步,这套框架就已经不是“能跑脚本”,而是一套能支撑日常回归工作的基础测试设施。

5. 常见问题与排查技巧实录

这部分整理的是我在这套框架使用过程中真正遇到过的坑,每一个都花了不少时间才发现原因。列出来给大家做一个速查表。

5.1 429 too many requests:真是限流吗

有段时间我的脚本跑着跑着就报“exceeded retry limit, last status: 429 too many requests”。一开始以为是被测试系统的防刷机制拦了,各种找运维调配额。后来才发现,429不一定是服务端主动限流,还可能是批量请求触发了网关侧的QPS限制,或者本地pip等工具请求第三方源被限流。

排查步骤很关键:先把报错里的完整信息读一遍,确认是哪个域名、哪个请求;如果是自己系统的接口,就先降低并发、在用例之间加延时;如果连续触发,就要看是不是测试环境前面挂了WAF,需要联系测试环境负责人把压测来源加入白名单。同时,通过Request的Retry机制配合backoff_factor,可以让重试避免二次触发限流。

现象 | 可能原因 | 排查思路 429响应 | 服务端/网关限流 | 先确认是否单条触发还是批量触发;检查日志的请求来源和UA;联系环境负责人确认是否有WAF策略 pip安装失败 | pip源限流 | 换镜像源;改用可信内部源;增加重试次数

5.2 连接超时与连接池泄漏

另一个高频问题是requests.exceptions.ConnectionError: HTTPSConnectionPool,绝大多数原因是没有设置timeout或者超时太短。如果你用一个Session对象反复请求,连接池满了也会报这类错误。解决办法:设置timeout、调用session.close()释放连接。

resp = client.request("GET", "/api/user/list", timeout=15)

还要注意,Requests的重试机制只在连接失败时生效,如果服务端一直响应超时,重试不会自动解决,那就要检查服务端性能了。这时候少见但具体的原因可能是Request库和urllib3版本不兼容,比如旧requests带新urllib3会出现参数解析问题,尽量把两个库升级到兼容版本。

5.3 响应中文乱码与JSON解析失败

接口返回带中文时,有时候resp.json()能解析但中文是乱码,这是编码判断错误。Requests库会猜测编码,但猜错时你需要显式指定:

resp.encoding = "utf-8" print(resp.text)

还有种情况是resp.json()抛JSONDecodeError,原因是返回的不是JSON格式,可能是HTML错误页、空响应或者纯文本。写代码时不要想当然地认为接口一定返回JSON:

try: body = resp.json() except ValueError: logger.error(f"非JSON响应: {resp.text[:500]}") raise

这种防御式处理,在排障时能让你第一时间看到原始响应内容,而不是一堆堆栈信息。

5.4 参数化用例失败时无法定位数据

pytest的parametrize在用例失败时默认只显示参数值,如果你的用例有几十组数据,肉眼很难看出是哪一组出的问题。解决办法:在用例内部自己打印case id:

def test_login(self, case): logger.info(f"执行用例: {case['id']} - {case['name']}") # 后续逻辑

运行结果里加一行日志,一秒定位。这个习惯我是吃了两三次亏之后才养成的,现在所有数据驱动用例第一行必打用例ID。

5.5 环境切换时测试一锅端

框架搭好后,你会发现最怕的是生产环境或测试环境地址配置错了,导致用例全部跑挂。我的方案是把base_url独立到config/settings.py:

# settings.py import os ENV = os.getenv("API_ENV", "test") BASE_URL_MAP = { "test": "http://127.0.0.1:8000", "staging": "http://staging.example.com" } def get_base_url(): return BASE_URL_MAP.get(ENV, BASE_URL_MAP["test"])

运行的时候:

API_ENV=staging pytest -s

这样一来环境配置清晰,代码里不出现环境相关硬编码,也不会出现“本地跑过,测试环境一跑全挂”的尴尬。

一点私货:这套框架真正让我受益的地方

最后说点个人体会。框架本身不复杂,真正让我长期受益的是它迫使我把接口测试的“业务逻辑”沉淀成了代码和数据。以前我在Excel里维护接口用例,接口路径稍微一变,就得手动找对应行去改,而且改没改对要跑一遍才知道。现在路径封装在api层,数据放在json里,接口路径变了改一个文件,参数变了改数据文件,逻辑基本不动。

另一个感受是,接口自动化框架的“性能”不是看你跑得有多快,而是看排障效率有多高。加了日志和结构化报告之后,线上反馈一个接口问题,我可以用这套框架复现一遍,几分钟就能确认是环境问题、数据问题还是代码问题。这个价值远超“省掉手工点击”这个原始出发点。

如果你正准备搭自己的框架,我的建议是先照着我这个结构走一遍,过程中把目录和封装往简单里做。等用例真的过了一百条之后,你自然会知道哪里需要加一层抽象、哪里需要引入更高级的工具,到时候再做演进不迟。基础框架最大的价值是跑通循环,是让你有一个可以持续迭代的起点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询