从零搭建接口自动化测试框架:pytest+requests+Allure实战
2026/9/20 11:20:46 网站建设 项目流程

接口自动化测试这件事,说简单也简单,一个 requests 发请求、一个 assert 断言就能跑起来;说复杂也复杂,当用例从 10 条涨到 500 条,当接口依赖从 1 个变成 20 个,当团队里五个人都在往里加代码的时候,没有一套像样的框架,维护成本会指数级上升。我从最早用 Postman 手动点、到写一堆散落的 Python 脚本、再到后来沉淀出一套 pytest + requests + Allure 的组合,中间踩过的坑足够写一本小册子。这篇内容就是把这套框架从零搭起来的过程完整拆开,包括目录怎么设计、请求怎么封装、数据怎么驱动、报告怎么生成、CI 里怎么跑,以及那些只有真正跑过几百条用例之后才会遇到的问题。

1. 为什么是 pytest + requests + Allure 这个组合

1.1 三个组件各自解决什么问题

先把这三个东西的定位说清楚,不然后面封装的时候容易职责混乱。

requests负责的是"发请求"这一层。它把 HTTP 协议里那些繁琐的东西——连接池、编码、重定向、Cookie 管理——全部封装成了几个方法调用。你写requests.get(url, params=params)的时候,底层帮你处理了 TCP 连接、HTTP 报文拼装、响应解码。接口自动化里它扮演的是"执行器"的角色,只关心请求发出去、响应拿回来。

pytest负责的是"组织用例"这一层。它管的是:哪些函数是测试用例、用例之间怎么共享前置条件、参数化怎么传、失败怎么重跑、哪些用例可以并行。它不关心你请求怎么发,只关心用例的生命周期。这是它和 unittest 最大的区别——pytest 是"框架管调度,你管逻辑",unittest 是"你既要管调度又要管逻辑"。

Allure负责的是"呈现结果"这一层。它把 pytest 跑出来的原始结果(一个 XML 文件)渲染成带步骤、带附件、带分类的可视化报告。测试报告的价值不在于好看,而在于当用例失败时,你能一眼看到是哪个请求、哪个参数、返回了什么,而不是对着一行AssertionError发呆。

三者组合起来的分工是:requests 干活,pytest 指挥,Allure 汇报。理解了这个分工,后面所有的封装设计都会顺理成章。

1.2 和 unittest、Postman 方案的真实对比

很多人一开始会纠结用 unittest 还是 pytest。我两个都用过,说几个实际感受:

对比维度unittestpytestPostman/Newman
用例写法必须继承 TestCase 类普通函数即可图形界面配置
参数化需要 ddt 库辅助原生@pytest.mark.parametrize需要写脚本
前置后置setUp/tearDown 层级固定fixture 按需组合pre-request script
失败重跑需插件pytest-rerunfailures内置
报告HTMLTestRunnerAllure 生态成熟自带但定制难
代码复用类继承,耦合高函数级 fixture,解耦好脚本片段复用差
调试体验一般--pdb直接断点界面调试方便

Postman 适合接口调试和少量冒烟,一旦用例上规模、需要和代码仓库一起版本管理、需要接入 CI,它的劣势就暴露了。unittest 不是不能用,但它的类继承模型在复杂 fixture 场景下会写得很别扭。pytest 的 fixture 机制是我最终选它的核心理由——它让"前置条件"变成了可组合、可复用、可作用域控制的积木。

1.3 这套框架适合什么样的团队

不是所有项目都需要这套框架。如果你的接口就十几个、每次上线前手动点一遍就行,那用 Postman 更划算。这套框架真正发挥价值是在这些场景:

  • 接口数量超过 50 个,且核心链路有依赖关系
  • 需要每天定时跑、或者每次提交代码自动跑
  • 测试数据需要多套(正常、边界、异常)反复验证
  • 团队多人协作,用例需要统一规范
  • 需要把测试结果沉淀下来做趋势分析

如果你符合其中两条以上,那投入时间搭这套框架是值得的。下面进入实操。

2. 环境搭建与项目骨架设计

2.1 Python 环境与依赖安装的坑

Python 版本建议 3.8 以上,3.10 和 3.11 都实测没问题。安装本身没什么好说的,但有几个细节值得提醒:

第一,务必用虚拟环境。我见过太多人把所有包装在全局环境里,结果 A 项目要 requests 2.25,B 项目要 requests 2.31,互相打架。用 venv 或者 conda 都行:

python -m venv venv # Windows venv\Scripts\activate # macOS/Linux source venv/bin/activate

第二,依赖版本要锁死。不要只写requests,要写requests==2.31.0。接口测试框架最怕的就是某天 CI 上突然因为某个依赖升级跑挂了。用pip freeze > requirements.txt生成锁定版本。

核心依赖清单:

pytest==7.4.3 requests==2.31.0 allure-pytest==2.13.2 pytest-rerunfailures==12.0 pytest-xdist==3.5.0 pytest-html==4.1.1 PyYAML==6.0.1 jsonschema==4.20.0 Faker==20.1.0

这里解释几个不那么常见的:pytest-xdist用来并行跑用例,用例多了之后串行跑太慢;jsonschema用来做响应结构校验,比一个个字段断言优雅得多;Faker用来生成随机测试数据,避免用例之间数据污染。

Allure 本身还需要装命令行工具,这个不是 pip 能搞定的。Windows 上可以用 scoop 装,macOS 用 brew,Linux 下载压缩包解压后配 PATH。装完执行allure --version能输出版本号就 OK。

2.2 目录结构:一开始就要想清楚

目录结构是框架的地基,改起来成本极高。我推荐这套分层:

api_test_framework/ ├── config/ # 配置层 │ ├── config.yaml # 环境配置 │ └── settings.py # 配置读取 ├── common/ # 公共层 │ ├── request_client.py # 请求封装 │ ├── assert_util.py # 断言封装 │ ├── logger.py # 日志 │ └── data_util.py # 数据处理 ├── testdata/ # 测试数据 │ ├── login.yaml │ └── order.yaml ├── testcases/ # 用例层 │ ├── conftest.py │ ├── test_login.py │ └── test_order.py ├── reports/ # 报告输出 ├── logs/ # 日志输出 ├── conftest.py # 全局 fixture ├── pytest.ini # pytest 配置 └── requirements.txt

这个分层的核心思想是关注点分离:配置归配置、请求逻辑归公共层、数据归数据、用例只写业务断言。很多人把请求代码直接写在用例里,一开始爽,后面改一个 header 要改几十个文件。

2.3 pytest.ini 的关键配置

pytest.ini是 pytest 的全局配置,几个必配项:

[pytest] testpaths = testcases python_files = test_*.py python_classes = Test* python_functions = test_* addopts = -v -s --alluredir=./reports/allure-results --clean-alluredir --reruns=1 --reruns-delay=2 markers = smoke: 冒烟用例 regression: 回归用例 p0: 最高优先级 p1: 高优先级

--reruns=1表示失败重跑一次,--reruns-delay=2表示重跑前等 2 秒。这个配置在接口测试里非常有用,因为网络抖动导致的偶发失败太常见了。但要注意,重跑只能掩盖环境问题,不能掩盖逻辑问题,如果一个用例稳定失败,重跑一百次也没用。

markers定义了自定义标记,后面可以用pytest -m smoke只跑冒烟用例。这是分层测试的基础。

3. 请求封装:让用例只关心业务

3.1 为什么不能直接用 requests

新手最容易犯的错就是在用例里直接写requests.post(url, json=data, headers=headers)。这样写有几个致命问题:

第一,重复代码爆炸。每个用例都要拼 URL、拼 header、处理 token,改一处要改一片。

第二,无法统一处理异常。网络超时、连接错误、SSL 错误,每个用例都要 try-except,代码丑陋且容易漏。

第三,日志和报告无法统一。Allure 报告里想看到每个请求的详情,散落的 requests 调用没法统一挂载。

第四,环境切换困难。测试环境、预发环境、生产环境(只读)的域名不同,散落的 URL 没法统一管理。

所以必须封装一层 RequestClient。

3.2 RequestClient 的完整实现

import requests import allure from common.logger import logger from config.settings import settings class RequestClient: def __init__(self): self.session = requests.Session() self.base_url = settings.BASE_URL self.timeout = settings.TIMEOUT self.session.headers.update(settings.DEFAULT_HEADERS) def _request(self, method, url, **kwargs): full_url = self.base_url + url if not url.startswith("http") else url kwargs.setdefault("timeout", self.timeout) # 把请求详情挂到 Allure 报告 allure.attach( f"{method} {full_url}\n" f"Headers: {kwargs.get('headers', {})}\n" f"Params: {kwargs.get('params', {})}\n" f"Body: {kwargs.get('json', kwargs.get('data', {}))}", name="请求详情", attachment_type=allure.attachment_type.TEXT ) logger.info(f"请求: {method} {full_url}") try: resp = self.session.request(method, full_url, **kwargs) except requests.exceptions.Timeout: logger.error(f"请求超时: {full_url}") raise except requests.exceptions.ConnectionError: logger.error(f"连接失败: {full_url}") raise logger.info(f"响应: {resp.status_code} {resp.text[:500]}") allure.attach( f"Status: {resp.status_code}\n" f"Body: {resp.text}", name="响应详情", attachment_type=allure.attachment_type.TEXT ) return resp 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)

这个封装有几个设计点值得展开说。

用 Session 而不是每次 requests.get。Session 会自动保持 Cookie、复用 TCP 连接。接口测试里经常需要先登录拿到 Cookie,后续请求自动带上,Session 天然支持。而且连接复用能显著提升大量用例的执行速度。

base_url 拼接逻辑。判断url.startswith("http")是为了兼容某些需要请求第三方完整 URL 的场景。日常用例只写相对路径,环境切换时只改配置。

Allure 附件挂载。这是让报告好用的关键。请求和响应都 attach 进去,失败时在报告里直接能看到发了什么、回了什么,不用去翻日志。

异常分类捕获。超时和连接错误分开处理,因为这两种错误的排查方向完全不同——超时可能是服务慢,连接错误可能是服务挂了或者域名配错了。

3.3 配置管理:一套代码跑多环境

配置用 YAML 管理,比 JSON 可读性好,比 ini 支持嵌套:

# config/config.yaml test: base_url: "https://test-api.example.com" timeout: 10 default_headers: Content-Type: "application/json" User-Agent: "AutoTest/1.0" staging: base_url: "https://staging-api.example.com" timeout: 15 default_headers: Content-Type: "application/json"

读取配置的 settings.py:

import yaml import os class Settings: def __init__(self): env = os.getenv("TEST_ENV", "test") config_path = os.path.join( os.path.dirname(__file__), "config.yaml" ) with open(config_path, encoding="utf-8") as f: all_config = yaml.safe_load(f) current = all_config[env] self.BASE_URL = current["base_url"] self.TIMEOUT = current["timeout"] self.DEFAULT_HEADERS = current["default_headers"] settings = Settings()

这样切换环境只需要export TEST_ENV=staging,代码一行不用改。CI 里通过环境变量注入,本地通过命令行设置,非常灵活。

注意:配置文件里绝对不要写生产环境的写操作地址。接口自动化测试应该只针对测试环境,生产环境最多做只读的监控探测,这是底线。

4. 用例组织与数据驱动

4.1 fixture 的正确打开方式

pytest 的 fixture 是这套框架的灵魂。先看一个典型的登录 fixture:

# conftest.py import pytest from common.request_client import RequestClient @pytest.fixture(scope="session") def client(): return RequestClient() @pytest.fixture(scope="session") def token(client): resp = client.post("/api/login", json={ "username": "autotest", "password": "test123456" }) assert resp.status_code == 200 return resp.json()["data"]["token"] @pytest.fixture(scope="function") def auth_client(client, token): client.session.headers.update({"Authorization": f"Bearer {token}"}) return client

这里的关键是scope 的选择tokensession级别,整个测试会话只登录一次,避免每个用例都登录导致接口被限流。auth_clientfunction级别,保证每个用例拿到的都是干净的 header 状态。

我踩过的一个坑:早期把 token 设成 function 级别,结果 200 条用例登录了 200 次,直接把测试环境的登录接口打挂了,触发了风控。后来改成 session 级别,登录次数降到 1 次。这个教训很深刻——fixture 的 scope 不只是性能问题,有时候是可用性问题

4.2 参数化:一套逻辑跑多组数据

pytest 的参数化是它相比 unittest 最大的优势之一。看一个实际例子:

import pytest @pytest.mark.parametrize("username,password,expected_code,expected_msg", [ ("autotest", "test123456", 200, "success"), ("autotest", "wrongpwd", 401, "密码错误"), ("", "test123456", 400, "用户名不能为空"), ("nonexist", "test123456", 401, "用户不存在"), ]) def test_login(auth_client, username, password, expected_code, expected_msg): resp = auth_client.post("/api/login", json={ "username": username, "password": password }) assert resp.status_code == expected_code assert resp.json()["msg"] == expected_msg

一个函数覆盖了正常、密码错误、参数缺失、用户不存在四种场景。Allure 报告里会显示成四个独立的用例,每个都有自己的参数。

但参数化数据写在代码里有个问题:数据量大了之后代码会很长,而且非技术人员没法维护。所以更好的做法是把数据外置到 YAML:

# testdata/login.yaml - case_name: "正常登录" username: "autotest" password: "test123456" expected_code: 200 expected_msg: "success" - case_name: "密码错误" username: "autotest" password: "wrongpwd" expected_code: 401 expected_msg: "密码错误"

然后用一个工具函数读取:

import yaml def load_yaml(path): with open(path, encoding="utf-8") as f: return yaml.safe_load(f) @pytest.mark.parametrize("case", load_yaml("testdata/login.yaml"), ids=lambda c: c["case_name"]) def test_login(auth_client, case): resp = auth_client.post("/api/login", json={ "username": case["username"], "password": case["password"] }) assert resp.status_code == case["expected_code"]

ids参数让 Allure 报告里显示的是"正常登录"而不是一长串参数,可读性提升明显。

4.3 接口依赖:用 fixture 串联业务流

真实业务里接口是有依赖的。比如下单要先登录、要先有商品、要先有库存。这种依赖用 fixture 串联最自然:

@pytest.fixture def product_id(auth_client): resp = auth_client.post("/api/product/create", json={ "name": "测试商品", "price": 99.9, "stock": 100 }) pid = resp.json()["data"]["id"] yield pid # 用例结束后清理 auth_client.delete(f"/api/product/{pid}") @pytest.fixture def order_id(auth_client, product_id): resp = auth_client.post("/api/order/create", json={ "product_id": product_id, "quantity": 1 }) return resp.json()["data"]["order_id"] def test_order_detail(auth_client, order_id): resp = auth_client.get(f"/api/order/{order_id}") assert resp.status_code == 200 assert resp.json()["data"]["order_id"] == order_id

注意product_id里用了yield,这是 fixture 的清理机制——yield 之前是前置,yield 之后是后置。这样创建的商品在用例结束后会自动删除,不会污染测试环境。测试数据自清理是接口自动化的基本素养,否则跑几次之后测试环境就全是垃圾数据了。

5. 断言与响应校验的进阶做法

5.1 从硬编码断言到 schema 校验

新手写断言通常是这样的:

assert resp.json()["data"]["name"] == "张三" assert resp.json()["data"]["age"] == 25 assert resp.json()["data"]["email"] == "zhangsan@example.com"

这种写法的问题是:字段一多就写不完,而且接口新增字段时断言不会失败(这其实是好事,但结构变化时又发现不了)。

更好的做法是分层校验:状态码 + 关键业务字段 + 整体结构。整体结构用 jsonschema:

from jsonschema import validate USER_SCHEMA = { "type": "object", "required": ["code", "msg", "data"], "properties": { "code": {"type": "integer"}, "msg": {"type": "string"}, "data": { "type": "object", "required": ["id", "name", "email"], "properties": { "id": {"type": "integer"}, "name": {"type": "string"}, "email": {"type": "string", "format": "email"} } } } } def test_get_user(auth_client): resp = auth_client.get("/api/user/1") assert resp.status_code == 200 validate(instance=resp.json(), schema=USER_SCHEMA) assert resp.json()["data"]["id"] == 1

schema 校验能保证接口返回的结构符合契约,字段类型正确、必填字段存在。这在接口联调阶段特别有用,能第一时间发现后端改了字段类型或者漏了字段。

5.2 封装一个顺手的断言工具

把常用断言封装成工具函数,用例里调用更简洁:

class AssertUtil: @staticmethod def assert_code(resp, expected=200): assert resp.status_code == expected, \ f"状态码错误: 期望{expected}, 实际{resp.status_code}" @staticmethod def assert_json_value(resp, key_path, expected): actual = resp.json() for k in key_path.split("."): actual = actual[k] assert actual == expected, \ f"字段{key_path}错误: 期望{expected}, 实际{actual}" @staticmethod def assert_schema(resp, schema): validate(instance=resp.json(), schema=schema)

用例里就变成:

def test_create_order(auth_client): resp = auth_client.post("/api/order", json={...}) AssertUtil.assert_code(resp, 200) AssertUtil.assert_json_value(resp, "data.status", "created")

断言失败时的报错信息要足够清晰,这是很多人忽略的点。assert actual == expected默认的报错信息很模糊,加上自定义 message 之后,报告里一眼就能看出哪个字段不对。

5.3 数据库校验:接口测完再查库

有些场景光看接口返回不够,还要确认数据真的落库了。比如下单接口返回成功,但订单表里到底有没有这条记录?这时候需要连数据库校验:

import pymysql @pytest.fixture(scope="session") def db(): conn = pymysql.connect( host=settings.DB_HOST, user=settings.DB_USER, password=settings.DB_PASSWORD, database=settings.DB_NAME, charset="utf8mb4" ) yield conn conn.close() def test_order_created_in_db(auth_client, db, order_id): cursor = db.cursor() cursor.execute("SELECT status FROM orders WHERE id = %s", (order_id,)) result = cursor.fetchone() assert result is not None, "订单未落库" assert result[0] == "created"

数据库校验是接口自动化的进阶能力,能发现一些接口返回正常但数据没写对的隐蔽 bug。但要注意,数据库校验会让用例和表结构强耦合,表结构一变用例就挂。所以只对核心业务表做校验,不要滥用。

6. Allure 报告的定制与优化

6.1 让报告说人话:feature、story、step

Allure 报告默认只显示用例名,信息量不够。加上装饰器之后,报告会变成按业务模块组织的结构:

import allure @allure.feature("用户模块") @allure.story("登录功能") @allure.title("正常登录-返回token") @allure.severity(allure.severity_level.CRITICAL) def test_login_success(auth_client): with allure.step("步骤1: 发送登录请求"): resp = auth_client.post("/api/login", json={...}) with allure.step("步骤2: 校验状态码"): assert resp.status_code == 200 with allure.step("步骤3: 校验返回token"): assert "token" in resp.json()["data"]

featurestory是两级分类,报告里会显示成树状结构。step会把用例执行过程拆成步骤,失败时能精确定位到哪一步挂了。severity标记优先级,报告里可以按优先级筛选。

这套装饰器用下来,报告的可读性会有质的提升。测试报告是给团队看的,不是给自己看的,让开发和产品能看懂报告,是自动化测试价值的体现。

6.2 失败截图与请求日志的自动挂载

前面 RequestClient 里已经挂了请求和响应详情。但还可以更进一步——失败时自动挂载更多上下文:

@pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: # 失败时把用例的 fixture 数据也挂上去 if "auth_client" in item.funcargs: client = item.funcargs["auth_client"] allure.attach( str(client.session.headers), name="失败时会话Headers", attachment_type=allure.attachment_type.TEXT )

这个 hook 会在用例失败时自动执行,把当前会话的 header 挂到报告里。排查问题时特别有用——有时候失败就是因为 token 过期了,看一眼 header 就知道了。

6.3 报告生成与查看的完整流程

pytest 跑完之后,结果在reports/allure-results目录里,是一堆 JSON 文件。要生成 HTML 报告需要执行:

allure generate ./reports/allure-results -o ./reports/allure-report --clean allure open ./reports/allure-report

--clean会清空旧报告,避免历史数据混淆。allure open会启动一个本地服务打开报告。

如果要在 CI 里跑,通常是生成报告后打包成 artifact 上传,或者部署到静态服务器。Allure 报告是纯静态的 HTML + JS,扔到 nginx 里就能访问。

提示:allure-results目录不要提交到 git,它是每次跑都会变的中间产物。在.gitignore里加上reports/logs/

7. 那些只有跑过几百条用例才会遇到的问题

7.1 用例之间的数据污染

这是最常见也最隐蔽的问题。比如用例 A 创建了一个用户名为 "test001" 的用户,用例 B 也创建 "test001",如果 A 没清理,B 就会因为用户名重复而失败。更坑的是,这种失败是顺序相关的——单独跑 B 能过,A 和 B 一起跑就挂。

解决方案有三个层次:

第一,数据自清理。前面说的 fixture yield 机制,用例结束自动删数据。

第二,数据唯一化。用 Faker 或者时间戳生成唯一数据:

from faker import Faker fake = Faker("zh_CN") def test_create_user(auth_client): username = f"auto_{fake.user_name()}_{int(time.time())}" resp = auth_client.post("/api/user", json={"username": username})

第三,用例隔离。每个用例用独立的测试账号,或者用独立的租户空间。这个成本最高,但最彻底。

我的经验是:核心用例用方案三,普通用例用方案二,配合方案一兜底

7.2 接口限流与重试策略

测试环境通常有限流,跑几百条用例很容易触发 429。前面提到的--reruns能缓解,但治标不治本。更根本的做法是:

  • 登录等高频接口用 session 级 fixture,只调一次
  • 用例之间加适当延迟,time.sleep(0.1)有时候是必要的
  • 用 pytest-xdist 并行时控制并发数,-n 4而不是-n auto
  • 对 429 响应做特殊处理,等待后重试
import time def _request_with_retry(self, method, url, retries=3, **kwargs): for i in range(retries): resp = self.session.request(method, url, **kwargs) if resp.status_code == 429: wait = int(resp.headers.get("Retry-After", 2 ** i)) logger.warning(f"触发限流,等待{wait}秒后重试") time.sleep(wait) continue return resp return resp

这个重试逻辑要谨慎使用,只对 429 和 5xx 重试,不要对 4xx 重试,因为 4xx 是客户端错误,重试多少次都一样。

7.3 并行执行带来的新问题

pytest-xdist 能把执行时间从 10 分钟压到 3 分钟,但会引入新问题:

  • session 级 fixture 在每个 worker 里都会执行一次。如果登录接口有限流,4 个 worker 就是 4 次登录。
  • 用例执行顺序不再确定。依赖执行顺序的用例会随机失败。
  • 共享资源竞争。多个 worker 同时操作同一条数据会冲突。

所以并行之前,先确保用例是无状态、无顺序依赖的。做不到就先别并行,或者只对独立模块并行。

7.4 断言失败信息的可读性

assert resp.json()["data"]["id"] == 1失败时,报错是AssertionError,没有上下文。改成:

actual = resp.json()["data"]["id"] assert actual == 1, f"用户ID错误: 期望1, 实际{actual}, 完整响应: {resp.text}"

把完整响应带上,排查时不用再去翻日志。这个习惯看起来小,但在用例多了之后能省大量时间。

8. 接入 CI 与日常维护

8.1 Jenkins/GitLab CI 里的执行脚本

CI 里的执行脚本要处理几件事:装依赖、跑用例、生成报告、处理退出码。

#!/bin/bash set -e python -m venv venv source venv/bin/activate pip install -r requirements.txt # 跑用例,失败不中断,继续生成报告 pytest --alluredir=./reports/allure-results || true # 生成报告 allure generate ./reports/allure-results -o ./reports/allure-report --clean # 根据结果决定构建状态 if grep -q '"status":"failed"' ./reports/allure-results/*.json 2>/dev/null; then echo "存在失败用例" exit 1 fi

|| true是为了让 pytest 失败后脚本继续执行,否则报告生成不了。最后再根据结果决定构建是否失败。

8.2 用例分层与执行策略

不是每次都要跑全部用例。合理的分层是:

层级用例范围执行时机耗时
冒烟核心链路 20 条每次提交1 分钟
回归全量用例每日定时10 分钟
全量含边界异常发版前30 分钟

用 marker 区分:

pytest -m smoke # 只跑冒烟 pytest -m "not smoke" # 跑非冒烟 pytest -m "p0 or p1" # 跑高优先级

8.3 框架本身的维护

框架代码也是代码,需要维护。几个建议:

  • 公共层要有单元测试。RequestClient、AssertUtil 这些工具函数自己也要测,否则工具出 bug 会污染所有用例。
  • 定期清理失效用例。接口下线了,对应用例要删掉,不要留着当僵尸。
  • 报告要有趋势。Allure 支持历史趋势,把每次的报告结果保留下来,能看到通过率的变化曲线。
  • 文档要跟上。新人接手时,一份 README 能省很多沟通成本。

9. 一些零散但重要的经验

写到这里,框架的主体已经完整了。最后分享几个零散但很实用的点。

关于 token 过期。session 级 fixture 的 token 如果过期了,后续所有用例都会失败。可以在 RequestClient 里加一个拦截:如果响应是 401,自动重新登录刷新 token。但这会让框架变复杂,简单项目手动控制 token 有效期更省事。

关于测试数据准备。有些接口依赖特定的数据状态,比如"已支付的订单才能退款"。这种数据用接口创建成本高,可以直接在数据库里 insert。但要注意,直接操作数据库的数据,测试环境重建后会丢失,所以要有数据初始化脚本。

关于环境健康检查。跑用例之前先探测一下测试环境是否可用,不可用就直接跳过,避免一堆无意义的失败。可以在 conftest 里加一个 session 级的健康检查 fixture。

关于日志级别。本地调试用 DEBUG,CI 里用 INFO。日志太多会拖慢执行速度,太少又排查不了问题。RequestClient 里请求和响应都打 INFO,详细的 header 和 body 打 DEBUG。

关于用例命名test_login_successtest_001好一百倍。用例名要能表达测试意图,报告里一眼就能看出测的是什么。中文用例名也可以,pytest 支持,Allure 显示也没问题。

关于版本控制。测试数据和用例代码一起提交,配置文件的敏感信息(密码、密钥)用环境变量注入,不要硬编码。.gitignore里排除 reports、logs、venv、__pycache__

这套框架我从最初的几十行脚本,迭代到现在支撑上千条用例,中间重构过三次。最大的体会是:框架的价值不在于用了多少高级特性,而在于它能不能让写用例的人只关心业务逻辑。当新增一个接口的测试只需要写一个 YAML 文件加一个函数的时候,这套框架就成功了。

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

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

立即咨询