接口自动化是不是已经“过时”了?这是很多测试开发同学在 2026 年回头看时容易产生的误判。如果只看招聘 JD,接口测试通常只是“要求列表里的一行”;但真正面试或带项目时,问法往往非常具体:Requests 怎么和 Pytest 结合?登录态怎么在多个用例之间复用?测试报告怎么让团队看得懂?用例怎么在持续集成里跑起来?
很多人并不是不会 Python,也不是没写过接口请求,而是卡在“从会发一个请求”到“能搭建一套可维护的接口自动化框架”之间的这条路上。网上的资料大多只教requests.get()和pytest的 hello world,一旦进入真实项目,涉及登录鉴权、数据驱动、环境切换、CI 报告,代码就开始失控。
这篇文章的目标很明确:用一条 6 小时的学习路径,把Python + Requests + Pytest + 持续集成这条链路完整走通。读完你会知道接口自动化测试框架怎么搭建,也会有一套可以直接复制到本地跑的示例工程。重点不是背 API,而是搞清楚每层代码解决什么问题,以及真正容易踩的坑在哪里。
1. 为什么接口自动化值得系统学习
1.1 它是自动化测试里“投资回报率”最高的部分
UI 自动化需要处理元素定位、等待策略、浏览器兼容,投入大、稳定性容易受前端改动影响。单元测试要求代码有很好的可测试性,往往由开发主写。唯独接口自动化处于一个很舒服的位置:被测对象是 HTTP 接口,只要后端服务在,就能稳定执行;用例运行速度快,失败原因也更容易定位。
从测试金字塔看,接口层属于“中间层”,向上可以覆盖大多数业务场景,向下比 UI 测试更接近代码逻辑。放到工程协作里,前后端分离后,接口一旦约定好,测试就可以和前端开发并行,不必等 UI 完成。所以很多团队的第一套自动化,不是 UI 自动化,而是接口自动化。
1.2 Requests + Pytest 为什么是这个组合
做接口自动化的语言和工具很多:Java 有 RestAssured,Python 有 Requests,也有用户直接用 Postman 的 Collection Runner。但在 Python 技术栈里,Requests 加 Pytest 几乎是最朴素也最稳定的组合。
Requests 负责“发请求”,它封装了 HTTP 的细节:Session 复用、Token 请求头、超时控制、重试。Pytest 负责“组织用例”:断言、前置条件、参数化、测试报告、CI 集成。两者分开看都很简单,合在一起就是一套标准的接口自动化测试框架。
判断一个技术栈好不好,不是看它新不新,而是看三个问题:团队招人是否容易上手?运行环境是否需要特殊依赖?出问题时网上资料是否足够多?Requests 和 Pytest 在这三点上都表现稳定。即便 2026 年出现更多封装好的工具,底层理解这些能力仍然不过时。
1.3 什么样的读者应该读这篇文章
最应该读的是这几类人:
- 会一点 Python 基础,但是没系统做过接口自动化测试;
- 已经在用 Postman 手工点接口,希望把重复回归变成自动化用例;
- 用 Pytest 写过单个用例,但不会组织登录态、不会做数据驱动、不会接 CI;
- 准备面试测试开发岗位,需要一套能讲清楚“框架怎么设计”的项目经验。
如果你已能熟练编写 Requests 脚本,并已经能维护一套 Pytest 工程,那么这篇文章对你来说会偏基础。但第 7 章的 CI 配置和第 9 章的工程实践,仍值得扫一遍。
2. 6 小时学习路径:先建立整体认知
2.1 不建议按“从入门到精通”的方式蛮学
很多人学接口自动化容易进入一个误区:先花一周看完一本 Requests 的文档,再花一周看 Pytest 文档,最后发现自己写项目时还是无从下手。
这是因为接口自动化的关键不只是语法,而是“怎么把一条手工接口调用,变成一条可持续回归的测试用例”。学习顺序应该是:先理解原理,再跑通最小用例,最后逐步补上工程化能力。
6 小时的拆解建议如下:
| 时间段 | 学习内容 | 完成标志 |
|---|---|---|
| 第 1 小时 | HTTP 基本原理 + 用工具或代码调通一个真实接口 | 能说出 Method、Header、Body、Status Code 的作用 |
| 第 2 小时 | 学习 Requests 基础,完成手动调用接口 | 能用requests.post/requests.get获取并打印响应 |
| 第 3 小时 | 理解 Session、Token、鉴权流程 | 能实现“先登录拿到 Token,再带 Token 请求业务接口” |
| 第 4 小时 | 学习 Pytest 用例组织方法 | 能写断言,运行pytest -v看到 PASSED/FAILED |
| 第 5 小时 | 学习 fixture 和参数化 | 能将登录操作提取为 fixture,并用多组数据运行同一用例 |
| 第 6 小时 | 接入报告与持续集成 | 能生成 HTML 报告;理解 GitLab CI 或 Jenkins 的基本触发方式 |
这个路径不是线性的“看完所有 API”,而是每个阶段都产出可运行结果。第 6 小时结束时,你已经不是“学过”,而是能交付一套小工程。
2.2 必须先补的 HTTP 基础
接口自动化测试中,HTTP 知识不需要很深,但下面几个点必须清楚:
- Method:GET 一般用于查询,POST 用于提交数据,PUT/DELETE 对应更新和删除。
- Header:包括 Content-Type、Authorization、Accept 等。请求体是 JSON 时,通常需要
Content-Type: application/json。 - Body:POST/PUT 请求中的传输内容,可能为 JSON、表单或纯文本。
- Status Code:200 表示成功,401 表示未认证,403 表示无权限,404 表示资源不存在,500 表示服务端异常。
- 接口文档:很多公司没有规范的 OpenAPI 文档,但至少要能从文档或抓包中知道请求地址、请求参数和响应结构。
初学者常犯的错误是把“登录成功”的判定条件写成status_code == 200。实际上很多业务系统即使业务失败,也会返回 200,只有响应体里的code或success字段才表示业务是否成功。所以断言要看两层:HTTP 状态码,加上业务状态码。
2.3 Requests 和 Pytest 在整个框架中的角色
接口自动化测试框架怎么搭建,可以抽象成四层:
- 接口请求层:用 Requests 封装通用的 GET、POST、PUT、DELETE。可以统一设置超时、统一处理 Token、统一打印日志。
- 测试用例层:用 Pytest 编写测试函数,对不同接口场景做断言。
- 数据层:用
conftest.py中的 fixture 管理登录态、测试数据和全局配置;用@pytest.mark.parametrize做参数化。 - 执行与报告层:Pytest 负责发现用例并运行,pytest-html 或 Allure 生成结果报告,Jenkins 或 GitLab CI 负责定时或提交时触发。
很多人一开始就把所有代码写在一个test_all.py里,结果用例几十个以后,函数之间的依赖乱成一团。理解了四层边界后,再写工程代码会清晰很多。
3. 环境准备与项目初始化
3.1 Python 版本与系统要求
首先确认 Python 版本。现在 Python 2 早已停止维护,建议使用 Python 3.10 及以上版本。在命令行输入:
python --version如果还没有安装 Python,可以从 Python 官网下载对应系统版本:
- Windows:下载安装包时勾选 “Add Python to PATH”。
- macOS:也可以用 Homebrew 安装,例如
brew install python3。 - Linux(如 Ubuntu/Debian):使用系统包管理工具安装,例如
sudo apt update && sudo apt install python3 python3-venv python3-pip。
为了避免系统 Python 环境被项目依赖污染,强烈建议在每个项目里使用虚拟环境。虚拟环境是 Python 工程化的基本习惯。
3.2 创建虚拟环境与安装依赖
打开终端,进入想要创建项目的目录。按以下命令操作:
mkdir api_test_project cd api_test_project python -m venv venvWindows 下激活虚拟环境:
venv\Scripts\activatemacOS / Linux 下激活虚拟环境:
source venv/bin/activate激活后,命令行前面会出现(venv)标识。接下来创建requirements.txt:
requests>=2.31 pytest>=8.0 pytest-html>=4.0 flask>=3.0其中 Flask 不是必须的。它在这里用于在本地搭建一个模拟被测接口的服务,方便你在没有公司测试环境的情况下先跑通全流程。如果你已经能直接调用公司测试环境接口,可以把 Flask 去掉。
执行安装:
pip install -r requirements.txt安装完成的验证方式:
python -c "import requests; print(requests.__version__)" python -c "import pytest; print(pytest.__version__)"能正常输出版本号,说明环境没问题。要注意的是:这里写的版本范围是“最低版本”,具体安装出来的版本可能比演示更新。只要没有破坏性变更,示例代码可以正常运行。
3.3 目录结构设计
项目初始化时,建议按下面的目录结构维护,这也是一个很常见的接口自动化工程雏形:
api_test_project/ ├── venv/ # 虚拟环境 ├── requirements.txt # 依赖清单 ├── pytest.ini # Pytest 配置 ├── mock_server.py # 本地 Mock 服务(演示用) ├── tests/ │ ├── __init__.py # 标记为 Python 包,避免导入问题 │ ├── conftest.py # 共享 fixture │ ├── test_login.py │ └── test_user.py └── reports/ # 测试报告输出目录pytest.ini让 Pytest 只从tests目录发现用例:
[pytest] testpaths = tests addopts = -ra-ra的意思是运行结束后显示所有结果的概要,包括 skipped 和 xfailed。先把基础工程搭好,后续写用例时不会出现明明写了用例,Pytest 却提示 “no tests ran” 的情况。
4. 用 Requests 跑通接口:先构建本地 Mock 服务
4.1 为什么需要 Mock 服务
真实业务接口往往依赖数据库、Redis、第三方服务,在教程演示时不一定方便使用。更常见的问题是:公司测试环境数据不稳定,或者接口文档不完整,导致新手连“接口到底返回什么”都搞不清楚。
Mock 服务的意义不是逃避真实系统,而是让我们能主动控制接口返回值,集中学习接口自动化的核心链路:登录、Token、业务查询、断言。项目中的mock_server.py就是模拟被测服务,它包含两个接口:
POST /api/login:接收用户名密码,校验通过后返回一个 Token;GET /api/users/<user_id>:需要请求头中带 Authorization,否则返回未授权。
4.2 Mock 服务完整代码
创建mock_server.py:
from flask import Flask, jsonify, request app = Flask(__name__) USERS = { "admin": {"password": "123456", "user_id": 1001, "role": "admin"}, "tester": {"password": "abc123", "user_id": 1002, "role": "guest"}, } def find_username_by_id(user_id): for username, user in USERS.items(): if user["user_id"] == user_id: return username return None @app.post("/api/login") def login(): data = request.get_json(force=True) username = data.get("username") password = data.get("password") user = USERS.get(username) if user and user["password"] == password: token = f"token-{username}-{user['user_id']}" return jsonify({ "code": 0, "message": "success", "data": {"token": token} }) return jsonify({ "code": 1001, "message": "用户名或密码错误" }), 401 @app.get("/api/users/<int:user_id>") def get_user(user_id): auth = request.headers.get("Authorization", "") if not auth.startswith("Bearer token-"): return jsonify({ "code": 401, "message": "未登录或token失效" }), 401 username = find_username_by_id(user_id) if username is None: return jsonify({ "code": 2001, "message": "用户不存在" }), 404 user = USERS[username] return jsonify({ "code": 0, "data": { "user_id": user_id, "username": username, "role": user["role"] } }) if __name__ == "__main__": app.run(host="127.0.0.1", port=5000, debug=False)启动 Mock 服务:
python mock_server.py正常输出末尾应该是Running on http://127.0.0.1:5000。另外开一个终端窗口,接下来会用它执行 Requests 脚本和 Pytest 用例。
这里有一个容易踩的坑:Flask 默认只能从运行它的机器访问。如果你把host写成127.0.0.1,CI 服务器上容器之间访问会遇到网络不通。不过本地学习用127.0.0.1已经足够。
4.3 用 Requests 完成第一个登录请求
在项目根目录下创建一个临时脚本try_requests.py,先不做任何封装,只是验证“能通”:
import requests BASE_URL = "http://127.0.0.1:5000" resp = requests.post( f"{BASE_URL}/api/login", json={"username": "admin", "password": "123456"}, timeout=5 ) print("HTTP Status:", resp.status_code) print("Response Body:", resp.json())运行:
python try_requests.py预期输出类似:
HTTP Status: 200 Response Body: {'code': 0, 'message': 'success', 'data': {'token': 'token-admin-1001'}}这一步的关键是resp.json()把响应体解析成 Python 字典。真实项目中,接口返回的 JSON 结构可能嵌套很深,初学者建议先打印几次,观察实际结构后再写断言。
4.4 带 Token 请求业务接口
回到接口自动化最常见的真实场景:登录成功后拿到 Token,后续查询用户、订单等接口需要把这个 Token 放到请求头里。
如果重新用requests.post再requests.get,Token 只能手动从一个响应中取出来再塞到另一个请求里,很繁琐。Requests 中建议使用Session对象,它可以跨请求保持某些状态:
import requests BASE_URL = "http://127.0.0.1:5000" session = requests.Session() # 1. 登录,拿到 token login_resp = session.post( f"{BASE_URL}/api/login", json={"username": "admin", "password": "123456"}, timeout=5 ) token = login_resp.json()["data"]["token"] print("token:", token) # 2. 更新会话的请求头,之后所有请求都会自带 Authorization session.headers.update({"Authorization": f"Bearer {token}"}) # 3. 查询用户信息 user_resp = session.get( f"{BASE_URL}/api/users/1001", timeout=5 ) print("user:", user_resp.json())Session 背后的逻辑是:如果你用同一个session对象发出多个请求,它会在内部复用 TCP 连接,并自动保存服务器返回的 Cookie。在接口自动化场景中,最常用的是session.headers.update()统一设置请求头,避免每个请求都重复写。
这段代码证明了登录态可以“一次登录,多次使用”。这也是后面 Pytest fixture 的设计基础:登录操作不应该出现在每个测试函数里,而应该提取为公共前置步骤。
4.5 Requests 必学的正确姿势:超时和异常
真实接口请求经常出现“服务没挂但很慢”的情况。如果不设置timeout,脚本可能一直挂在那里。建议每个请求都设置超时,并捕获异常避免 Python 直接抛出堆栈中断执行:
import requests from requests.exceptions import RequestException try: resp = requests.get( "http://127.0.0.1:5000/api/users/1001", headers={"Authorization": "Bearer token-admin-1001"}, timeout=(3, 5) ) print(resp.json()) except RequestException as e: print("请求失败:", e)timeout=(3, 5)表示连接超时 3 秒,读取超时 5 秒。不用太纠结这个元组,只要知道比不设超时更健壮就可以。在测试用例中,捕获异常并断言失败,总比进程卡死更好排查。
5. 用 Pytest 组织用例:断言、Fixture 与参数化
5.1 Pytest 收集用例的基本规则
如果把上面的 Requests 脚本直接保存为.py文件运行,它只能算“脚本”,还谈不上自动化测试用例。自动化测试需要让程序知道:哪些步骤是测试动作,哪些结果是预期结果。
Pytest 认为,满足下面条件的文件会被自动收集为测试用例:
- 文件名以
test_开头,或以_test.py结尾; - 文件中的函数名以
test_开头; - 类名以
Test开头,类内方法名以test_开头。
例如下面这个文件会被识别为包含一个测试用例:
# tests/test_login.py import requests def test_login_success(): resp = requests.post( "http://127.0.0.1:5000/api/login", json={"username": "admin", "password": "123456"}, timeout=5 ) assert resp.status_code == 200 assert resp.json()["code"] == 0 assert resp.json()["data"]["token"] != ""运行方式:
pytest -v-v参数会显示每个用例的 PASSED 或 FAILED。
5.2 断言不能只断 HTTP 状态码
上面的用例中断言了三个层次:
status_code == 200:网络层通不通;code == 0:业务是否成功;token != "":关键数据是否缺失。
Pytest 断言的本质是 Python 原生的assert。如果断言失败,Pytest 会打印表达式两侧的值,方便定位。但不要只写assert resp.json(),这种断言缺少具体判断意义;也不要断言整个响应体和期望字典完全相等,因为只要接口新增一个耗时字段,用例就会失败,十分脆弱。更合理的做法是只断言本次用例关心的字段。
5.3 fixture:把登录前置提取出来
继续上面的例子,如果每个用例都需要 Token,每个用例里都写一遍“登录并更新请求头”,代码冗余不说,登录逻辑一变,所有用例子都要跟着改。
Pytest fixture 是解决这个问题的标准手段。把“已经登录好的 Session”定义成conftest.py中共享的 fixture:
# tests/conftest.py import os import pytest import requests BASE_URL = os.getenv("BASE_URL", "http://127.0.0.1:5000") @pytest.fixture(scope="session") def base_url(): return BASE_URL @pytest.fixture(scope="session") def api_session(base_url): session = requests.Session() login_resp = session.post( f"{base_url}/api/login", json={"username": "admin", "password": "123456"}, timeout=5 ) token = login_resp.json()["data"]["token"] session.headers.update({"Authorization": f"Bearer {token}"}) return sessionscope="session"表示整个测试会话只执行一次。也就是说,即使有 20 个用例使用api_session,登录请求也只会发出一两次,而不是每个用例都登录。
测试函数中只需要声明参数名,Pytest 会自动注入:
# tests/test_user.py def test_get_user_success(api_session, base_url): resp = api_session.get(f"{base_url}/api/users/1001", timeout=5) assert resp.status_code == 200 assert resp.json()["data"]["username"] == "admin"很多初学者不理解为什么测试函数没有实例化api_session却可以调用,这是 Pytest 的依赖注入机制。fixture 就是“你声明需要什么,Pytest 就给你准备什么”。
5.4 参数化:用一组数据跑同一用例
登录接口除了验证正确密码,还需要验证错误密码、不存在的用户等。如果每种情况都写一个测试函数,代码会大量重复。@pytest.mark.parametrize可以让你用多组数据执行同一段测试逻辑:
# tests/test_login.py import pytest import requests @pytest.mark.parametrize( "username,password,expected_code", [ ("admin", "wrong", 401), ("nobody", "123456", 401), ("", "123456", 401), ] ) def test_login_with_invalid_param(username, password, expected_code): resp = requests.post( "http://127.0.0.1:5000/api/login", json={"username": username, "password": password}, timeout=5 ) assert resp.status_code == expected_code运行后,Pytest 会把这三组数据当成三个独立用例。从测试报告上看,它们会显示为同一个函数名后追加参数信息。
真正在工作中,参数化还可以配合外部数据文件使用,例如从 CSV、Excel 或 YAML 中读取多组测试数据。先掌握parametrize的基础方式,再逐步扩展数据源会更顺。
6. 完整实战:登录态业务链路测试工程
在讲完最小知识点之后,我们来搭一套完整可运行的接口自动化测试工程。这套工程包含登录用例、业务查询用例、共享登录 fixture、统一运行配置,是最接近真实团队的落地形态。
6.1 文件目录与用途
在项目根目录下准备这些文件:
api_test_project/ ├── requirements.txt ├── pytest.ini ├── mock_server.py ├── tests/ │ ├── __init__.py │ ├── conftest.py │ ├── test_login.py │ └── test_user.py └── reports/mock_server.py已经在前文创建;conftest.py存放共享 fixture;test_login.py验证登录接口正向和反向;test_user.py验证需要 Token 的业务查询接口。
6.2 完整代码:共享 fixture
创建tests/conftest.py:
import os import pytest import requests BASE_URL = os.getenv("BASE_URL", "http://127.0.0.1:5000") @pytest.fixture(scope="session") def base_url(): return BASE_URL @pytest.fixture(scope="session") def api_session(base_url): session = requests.Session() resp = session.post( f"{base_url}/api/login", json={"username": "admin", "password": "123456"}, timeout=5 ) assert resp.status_code == 200, "登录前置失败,无法获取Token" token = resp.json()["data"]["token"] session.headers.update({"Authorization": f"Bearer {token}"}) return session关键点在于assert resp.status_code == 200。不要把登录失败留到后续用例中才暴露,前置失败应该立即中止测试会话。
6.3 完整代码:登录测试用例
创建tests/test_login.py:
import requests def test_login_success(base_url): resp = requests.post( f"{base_url}/api/login", json={"username": "admin", "password": "123456"}, timeout=5 ) assert resp.status_code == 200 assert resp.json()["code"] == 0 assert "token" in resp.json()["data"] def test_login_wrong_password(base_url): resp = requests.post( f"{base_url}/api/login", json={"username": "admin", "password": "wrong"}, timeout=5 ) assert resp.status_code == 401 assert resp.json()["message"] == "用户名或密码错误"这里没有把base_url写死在代码中,而是通过 fixture 读取,目的是后续接 CI 时可以直接用环境变量切换被测环境,不用改代码。
6.4 完整代码:用户查询测试用例
创建tests/test_user.py:
def test_get_user_success(api_session, base_url): resp = api_session.get(f"{base_url}/api/users/1001", timeout=5) assert resp.status_code == 200 assert resp.json()["code"] == 0 assert resp.json()["data"]["username"] == "admin" assert resp.json()["data"]["role"] == "admin" def test_get_user_not_found(api_session, base_url): resp = api_session.get(f"{base_url}/api/users/9999", timeout=5) assert resp.status_code == 404 assert resp.json()["code"] == 2001 def test_get_user_without_token(base_url): resp = requests.get(f"{base_url}/api/users/1001", timeout=5) assert resp.status_code == 401这里体现了两种场景的差异:
- 前两条用例需要登录态,所以参数里声明
api_session; - 第三条用例专门验证“没有 Token 时访问受保护接口”,所以用原始
requests.get,而不是api_session。如果把这条用例也写成使用api_session,就测不出未认证的效果。
6.5 运行方法
先启动 Mock 服务,在两个终端窗口中分别执行:
终端 1:
python mock_server.py终端 2:
pytest -v预期结果大约是这样:
tests/conftest.py ... # 如果有 setup 日志会显示 tests/test_login.py::test_login_success PASSED tests/test_login.py::test_login_wrong_password PASSED tests/test_user.py::test_get_user_success PASSED tests/test_user.py::test_get_user_not_found PASSED tests/test_user.py::test_get_user_without_token PASSED如果出现失败,优先看失败断言两侧打印的值,大多是因为接口返回与预期不一致,而不是代码运行错误。接口自动化最容易出现的问题是用例断言写得太严格:接口返回顺序稍微调整、多余了一层嵌套,用例就红了。所以用例中的数据断言要结合接口文档确认。
7. 生成测试报告并接入持续集成
7.1 先解决“测试给谁看”的问题
本地运行pytest -v,绿色 PASSED 和红色 FAILED 已经能让人看懂,但团队协作时不能要求每个人都去命令行跑一遍。实践中需要把测试结果生成 HTML 报告,并让持续集成工具保存报告产物。这是接口自动化从“个人脚本”走向“团队资产”的关键一步。
安装 pytest-html 后运行以下命令:
pytest tests/ -v --html=reports/report.html --self-contained-html--self-contained-html会把 CSS 和 JS 内嵌到 HTML 中,否则报告文件单独打开时样式会丢失。运行结束后,打开reports/report.html就能看到用例总数、通过数、失败数和每个用例的执行时间。
如果想要更美观、更丰富的报告,可以后续引入 Allure。Allure 需要安装allure-pytest插件和 Allure 命令行工具,生成结果文件后再输出 HTML。相对 pytest-html 更重一些,适合团队统一报告平台时使用。本文先以 pytest-html 跑通闭环。
7.2 持续集成:自动化测试的归宿
本地跑通过的用例,如果不接入持续集成,价值会大打折扣。原因很简单:人不会每天记得跑一遍回归。接口自动化更适合在代码提交、定时任务或版本发布前自动执行,并把结果通知到团队。
从工程实践看,常见的持续集成触发方式是:
- 开发提交代码后自动触发;
- 每天早上定时执行一次全量回归;
- 测试环境发版后自动触发冒烟用例。
如果使用 GitLab CI,在项目根目录创建.gitlab-ci.yml:
stages: - test api_test: stage: test image: python:3.12-slim script: - pip install -r requirements.txt - pytest tests/ -v --html=report.html --self-contained-html artifacts: when: always paths: - report.html这段配置的含义是:当代码推送到仓库时,CI 会拉一个新的 Python 镜像,在干净环境里安装依赖、执行测试,最后把测试报告作为产物保存。when: always表示即使测试失败也会保留报告,方便排查。
如果你的团队使用 Jenkins,核心思路也一样。最简单的方式是在 Jenkins Pipeline 中执行以下命令:
stage('运行接口自动化测试') { steps { sh ''' pip install -r requirements.txt pytest tests/ -v --html=report.html --self-contained-html ''' } }Jenkins 还需要在系统里配置好 Python 环境和项目工作空间。具体插件版本会随着 Jenkins 版本变化,这里不展开,重点记住这个套路:先装依赖,再跑 Pytest,再收集报告。
7.3 环境切换的正确姿势
在 CI 里运行测试,用例调用的不一定是本地127.0.0.1,可能是公司测试环境的地址。前文conftest.py中的os.getenv("BASE_URL", "...")就是为环境切换预留的入口。
在 GitLab CI 中,可以配置变量:
api_test: variables: BASE_URL: "http://test-api.example.com"在 Jenkins 中可以在 Pipeline 里通过环境变量传入:
environment { BASE_URL = "http://test-api.example.com" }核心原则是:测试代码不应该硬编码被测环境地址,而应该从配置或环境变量中读取。这不仅是为了方便切换环境,更是为了避免有人误把本地地址提交到正式流水线中。
7.4 在 CI 中如何处理依赖的 Mock 服务
如果你像本文一样使用本地 Mock 服务,在 CI 里也需要先启动它再执行测试。可以在 CI 脚本中加一行后台启动,等待服务就绪后再跑 Pytest。例如在.gitlab-ci.yml中:
script: - pip install -r requirements.txt - nohup python mock_server.py > mock.log 2>&1 & - sleep 2 - pytest tests/ -v --html=report.html --self-contained-html不过更推荐的做法是让接口自动化用例直接使用团队的测试环境,而不是依赖 Mock 服务。Mock 适合开发和调试,真实业务的覆盖还是应该打真实测试环境。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
Pytest 提示no tests ran | 测试文件名不以test_开头,或函数名不以test_开头 | 检查文件命名和函数命名是否符合 Pytest 收集规则 | 重命名文件或函数;确认pytest.ini中testpaths指向正确目录 |
Requests 报MissingSchema | URL 没有写http://或https:// | 打印实际请求的 URL 字符串 | 确保拼接 URL 后是完整路径;使用f"{base_url}/api/login"时确认 base_url 带协议 |
| 登录成功但后续接口仍 401 | 没有使用同一个Session,或没有把 Token 放入请求头 | 打印session.headers和服务端返回信息 | 使用api_session发送后续请求;调用session.headers.update()更新请求头 |
接口返回429 Too Many Requests | 请求频率超过被测系统或公共服务的限流阈值 | 查看服务端返回的限流说明,检查脚本是否循环发送大量请求 | 不绕过限流,合理降低调用频率;必要时在请求之间增加延时;使用服务方提供的认证信息获取更高的合法限额 |
| 请求长时间无响应 | 没有设置timeout,服务异常导致连接挂起 | 在代码中补上timeout参数 | 为每个请求设置合理的超时参数,并在异常中打印请求信息方便定位 |
| 某个用例在本地通过,CI 失败 | CI 环境变量 `BASE_URL |