使用Postman集合来探索API确实是一个很好的方法,但将其用于存储测试用例却并不合适。
大多数团队都是通过惨痛的经历才意识到这一点的。有人会将Postman集合导出出来,将其中的请求转换成测试代码,然后就不再管了。六个月后,这些测试用例就会出现错误,没有人再信任它们,因此在测试流程中就被直接跳过了。其实,进行这种转换本身并不是最困难的部分,真正麻烦的是如何让这些测试用例持续保持有效状态。
本教程将指导您如何将Postman集合转换为能够正常运行的pytest测试套件,并确保它在下个季度依然可以正常使用。首先,我们会分析为什么转换后的测试用例会失效,然后介绍四种能够让它们保持有效状态的原理。示例代码规模较小,因此您今天就可以在自己的Postman集合上尝试这些方法。
目录
开始之前
要跟随本教程进行操作,您需要准备以下条件:
-
Python 3.10或更高版本,并已安装pytest和httpx插件(执行命令
pip install pytest httpx即可)。 -
一个想要转换的Postman集合,以及该集合所对应的环境配置信息(基础URL和访问令牌)。
-
具备基本的pytest使用知识,了解测试 fixture的工作原理,以及如何通过命令行运行
pytest。 -
如果您想尝试持续集成流程,可以准备一个GitHub仓库。不过这部分内容也可以跳过,依然可以完成后续的学习。

该示意图展示了整个转换过程的两个阶段。左侧显示的是将Postman集合中的请求和环境配置转换为pytest测试套件的步骤,这一部分其实非常简单。
这项工作所实现的功能就是右侧所示的“可维护性层”——它将最初的草稿转化为一套可靠、可信赖的系统:系统中的各个组件都是通过配置文件来设置的,而非被硬编码在代码中;测试机制用于验证系统的响应是否符合预期的规范,而不仅仅是检查响应状态码是否为200;每项测试都是独立的;而且每当有人对代码进行修改并提交后,这套测试系统就会自动运行,从而实现持续集成。
为什么转换后的测试会失效
当你将Postman中的请求进行一对一的转换时,往往会养成以下四种习惯,这些习惯在初期看起来没什么问题,但到了后期就会带来麻烦:
-
每个测试中都会硬编码基础URL和令牌,因此从测试环境切换到生产环境时,就需要逐一修改这些代码。
-
测试会按照固定的顺序执行,因为第二个请求依赖于第一个请求设置的结果,所以一旦某个请求失败,就会引发连锁反应。
-
唯一的验证条件就是状态码是否为200,即使响应内容不正确,这个条件也会通过。
-
所有的测试代码中都会复制相同的配置信息,因此只要对认证方式稍作修改,就需要编辑二十个文件。
以上每一个问题都会增加维护的难度,而这些问题加在一起,就会导致整个测试套件被弃用。下面介绍如何避免这些问题的发生。
原则1:将环境配置与测试代码分离
Postman的测试集会将环境配置信息保存在单独的文件中,例如基础URL、令牌等变量。在pytest中也可以采用同样的方法:在某个固定配置函数中读取这些值,然后让每个测试函数自行获取这些值。
# conftest.py
import os
import httpx
import pytest
@pytest.fixture(scope="session")
def base_url():
return os.environ["API_BASE_URL"]
pytestfixture scope="session")
def auth_headers():
return {"Authorization": f"Bearer {os.environ['API_TOKEN']}"}
pytestfixture()
def http():
with httpx.Client(timeout=10) as client:
yield client
现在,任何测试代码都不会直接引用URL或令牌:
def test_get_user(base_url, auth_headers, http):
response = http.get(f"{base_url}/users/1", headers=auth_headers)
assert response.status_code == 200
从测试环境切换到生产环境,现在只需要修改一个环境变量即可,而无需遍历整个测试套件进行修改。
原则2:验证响应的内容结构而不仅仅是状态码
状态码为200只说明服务器作出了响应,并不能证明响应内容是正确的。导致API出现故障的最常见原因就是所有的测试都只检查了状态码。
应该验证响应内容的结构以及调用者所依赖的字段是否正确。
def test_user_shape(base_url, auth_headers, http):
response = http.get(f"{base_url}/users/1", headers=auth_headers)
assert response.status_code == 200
body = response.json()
assert set(body) >= {"id", "email", "created_at"}
assert isinstance(body["id"], int)
assert "@" in body["email"]
并不是每个接口都需要严格的字段结构规范。只要对一些关键字段进行简单检查,就能发现许多通过状态码检查无法发现的错误。
原则3:让每个测试独立运行
每个测试都应该能够独立运行,不应该受到其他测试结果的影响。
在Postman中,一个请求触发另一个请求是正常的。但在测试套件中,这种耦合关系反而会带来问题:如果重新排列测试顺序、单独运行某项测试,或者丢失了最初的请求,那么后续的所有测试都会受到影响。
应为每个测试提供它所需的环境条件。如果某个测试需要创建用户账户,那么系统就会自动创建相应的用户。
def test_delete_user(base_url, auth_headers, http):
created = http.post(
f"{base_url}/users",
headers=auth_headers,
json={"email": "temp@example.com"},
)
user_id = created.json()["id"]
response = http.delete(f"{base_url}/users/{user_id}", headers=auth_headers)
assert response.status_code == 204
独立的测试可以以任何顺序运行,也可以并行执行。当某个测试失败时,问题只会出现在那个具体的测试中,而不会引发一连串的故障。
原则4:从第一天起就将测试套件纳入持续集成流程
如果一个测试套件只在你的笔记本电脑上运行,那么一旦你停止关注它,这个套件很快就会过时。在编写第二个测试之前,就将其集成到你的开发流程中,这样每次代码提交后,这个测试套件都能保持正常状态。
# .github/workflows/tests.yml
name: API tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: pytest -v
env:
API_BASE_URL: ${{ secrets.API_BASE_URL }}
API_TOKEN: ${{ secrets.API_TOKEN }}
一旦这样的机制建立起来,当某个测试失败时,我们只需要在拉取请求中讨论这个问题,而不会在生产环境中遇到意外情况。
让工具来处理繁琐的任务
以上提到的所有内容都是值得你关注的重点。将每个请求转化为测试用例这个过程其实很机械,而这类机械性工作完全适合自动化处理。
我维护了一个开源工具,专门用于完成这一步骤,它的名字是postman2pytest。这个工具可以读取Postman中的测试数据,然后生成可执行的pytest测试文件。这样你就可以把精力放在测试代码的可维护性上,而不用花费时间去编写重复性的代码。当Postman中的测试数据发生变化时,你只需要重新生成测试文件即可,而不需要手动修改原有的代码。
你可以在这里找到这个工具:https://github.com/golikovichev/postman2pytest
总结
将Postman中的测试数据转化为实际的测试用例其实非常简单。真正困难的是如何确保这些测试用例的可靠性,而这关键在于养成一些良好的习惯:不要让环境因素影响测试结果,要关注测试逻辑本身而不仅仅是状态码;确保每个测试都是独立的;从一开始就采用持续集成流程来运行所有的测试。
如果做到了这些,那么你这周生成的测试套件,明年依然会是你可以信赖的测试工具。