Pytest+Requests接口自动化框架实战:解决接口关联与工程化难题

1. 项目概述与核心价值

最近在带团队做接口自动化项目,发现很多同学虽然会用Pytest写几个测试用例,但一遇到复杂的业务场景,比如需要登录后才能操作的接口、或者一个接口的返回值要作为另一个接口的入参时,代码就变得一团糟。要么是全局变量满天飞,要么是重复的请求代码写了一遍又一遍,维护起来简直是一场灾难。这正是“接口关联”这个核心痛点没解决好的表现。今天我就结合一个基于requestsPytest的实战项目,来拆解一下如何构建一个清晰、可维护且能优雅处理接口关联的自动化测试框架。这个方案不仅能帮你跑通测试,更重要的是能建立起一套工程化的思维,让你在面对成百上千个接口时也能从容不迫。

简单来说,我们要做的是:用requests库作为HTTP请求的基石,用Pytest来组织和驱动测试用例,并通过设计良好的框架结构来解决接口之间的依赖和数据传递问题。最终目标是产出一份漂亮的Allure报告,让测试结果一目了然。无论你是刚接触接口自动化的新手,还是想优化现有框架的老手,相信这套实战经验都能给你带来直接的启发。

2. 框架整体设计与核心思路拆解

2.1 为什么是 Pytest + Requests 这个组合?

在Python的测试生态里,unittestpytest是两大主流。我选择pytest,是因为它在灵活性、插件生态和可读性上优势明显。比如它的fixture机制,是管理测试前置和后置条件(如登录态、数据库连接)的神器,这正是处理接口关联(如获取token)的绝佳场所。而parametrize装饰器能轻松实现数据驱动,把测试数据和测试逻辑分离。requests库就更不用说了,它是Python领域进行HTTP通信的事实标准,API设计优雅,功能强大且文档齐全,远比urllib好用。

这个组合的核心思路是“分层”和“解耦”。不要把发送请求、处理响应、断言检查、数据管理这些逻辑全部堆在一个测试函数里。那样的话,一旦接口地址变更或者认证方式调整,你就得改无数个地方。正确的做法是分层处理,让每一层只专注一件事。

2.2 项目结构规划:清晰的分层是成功的一半

一个混乱的目录结构是项目后期难以维护的根源。我推荐下面这种结构,它经过了多个项目的检验:

project_root/ ├── api/ # 接口封装层 │ ├── __init__.py │ ├── auth_api.py # 认证相关接口,如登录 │ └── user_api.py # 用户相关接口 ├── common/ # 公共工具层 │ ├── __init__.py │ ├── logger.py # 日志模块 │ └── utils.py # 通用工具函数,如加密、时间处理 ├── core/ # 核心请求层 │ ├── __init__.py │ └── request_client.py # 对requests的二次封装,统一请求行为 ├── data/ # 测试数据层 │ ├── __init__.py │ └── test_data.yaml # 或使用JSON、Excel管理数据 ├── fixtures/ # Pytest夹具层(可选,也可放在conftest.py) │ └── __init__.py ├── testcases/ # 测试用例层 │ ├── __init__.py │ ├── conftest.py # 项目级的Pytest配置和夹具 │ ├── test_auth.py # 认证模块测试用例 │ └── test_user.py # 用户模块测试用例 ├── reports/ # 测试报告输出目录 ├── configs/ # 配置文件层 │ ├── __init__.py │ └── settings.ini # 或settings.py,管理环境配置 ├── pytest.ini # Pytest配置文件 ├── requirements.txt # 项目依赖 └── README.md

各层职责解析:

  • core/request_client.py:这是框架的“心脏”。它封装了requests.Session(),目的是统一添加请求头(如Content-Type)、处理认证(如自动添加Token)、记录日志、重试机制以及全局的响应处理(如状态码非200的异常抛出)。所有具体的接口调用都通过这个客户端发出,保证行为一致。
  • api/:这一层面向业务。每个文件对应一个业务模块,里面的函数代表一个具体的HTTP接口。它调用core层的客户端来发送请求,并负责将原始的响应字典转换成一个更友好、易于断言的对象(比如一个ResponseWrapper类,包含status_codejson_dataheaders等属性)。
  • testcases/:这一层才是真正的测试逻辑。它导入api层封装好的接口函数,使用pytest的装饰器编写测试函数。这里应该只包含测试步骤、断言和必要的业务逻辑组合,不出现具体的URL、请求参数拼接等底层细节。
  • data/:测试数据独立存放,通常使用YAML或JSON。YAML的可读性更好,支持注释,非常适合描述结构化的测试数据。数据驱动测试时,pytest.mark.parametrize可以从这些文件中读取数据。
  • fixtures/conftest.py:这是实现接口关联的关键所在。我们把需要跨用例、跨模块共享的状态(最典型的就是登录后的tokensession)定义成pytest.fixture。通过设置scope参数(如session,module,class,function),可以精确控制这个状态的生命周期和共享范围。

注意:很多新手会把conftest.py放在项目根目录。实际上,pytest会从测试文件所在目录向上递归查找conftest.py。将项目级的夹具(如获取全局token)放在testcases/conftest.py中,将模块级的夹具放在具体测试文件同目录或父目录的conftest.py里,是更清晰的做法。

3. 核心模块实现与接口关联关键技术

3.1 打造健壮的请求客户端 (Core层)

一个健壮的请求客户端能省去后续无数麻烦。以下是一个加强版的request_client.py示例:

# core/request_client.py import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry import logging from typing import Any, Dict, Optional, Union class RequestClient: """封装requests,提供统一会话、重试、日志和错误处理""" def __init__(self, base_url: str = ""): self.session = requests.Session() self.base_url = base_url.rstrip('/') # 设置默认请求头 self.session.headers.update({ 'Content-Type': 'application/json; charset=utf-8', 'User-Agent': 'Pytest-API-Automation/1.0' }) # 配置重试机制,应对网络抖动或服务端429/5xx错误 retry_strategy = Retry( total=3, # 总重试次数 backoff_factor=1, # 重试等待时间因子 status_forcelist=[429, 500, 502, 503, 504], # 遇到这些状态码才重试 allowed_methods=["GET", "POST", "PUT", "DELETE"] # 只对这些方法重试 ) adapter = HTTPAdapter(max_retries=retry_strategy) self.session.mount('http://', adapter) self.session.mount('https://', adapter) self.logger = logging.getLogger(__name__) def set_common_headers(self, headers: Dict[str, str]): """更新公共请求头,如设置Token""" self.session.headers.update(headers) def _request(self, method: str, endpoint: str, **kwargs) -> requests.Response: """统一的请求发送方法,内置日志记录""" url = f"{self.base_url}{endpoint}" if self.base_url else endpoint self.logger.info(f"Request: {method.upper()} {url}") if kwargs.get('json'): self.logger.debug(f"Request Body: {kwargs['json']}") if kwargs.get('params'): self.logger.debug(f"Request Params: {kwargs['params']}") try: resp = self.session.request(method, url, **kwargs) self.logger.info(f"Response Status: {resp.status_code}") self.logger.debug(f"Response Body: {resp.text[:500]}...") # 日志截断,防止过长 # 可以在这里添加对特定状态码的全局处理,比如401自动刷新token if resp.status_code == 401: self.logger.warning("Received 401 Unauthorized. Token might be expired.") # 这里可以触发一个刷新token的流程,然后重试原请求(需谨慎实现) # 对于非2xx响应,可以选择直接抛出异常,或在更高层处理 # resp.raise_for_status() # 强烈建议使用,让错误尽早暴露 return resp except requests.exceptions.RequestException as e: self.logger.error(f"Request failed: {e}") raise # 提供便捷的HTTP方法封装 def get(self, endpoint: str, **kwargs): return self._request('GET', endpoint, **kwargs) def post(self, endpoint: str, **kwargs): return self._request('POST', endpoint, **kwargs) def put(self, endpoint: str, **kwargs): return self._request('PUT', endpoint, **kwargs) def delete(self, endpoint: str, **kwargs): return self._request('DELETE', endpoint, **kwargs) # 其他方法如 patch, head 等可按需添加

关键点解析:

  1. 使用Sessionrequests.Session()可以自动保持cookies,在需要cookie鉴权的场景下非常有用,避免了手动管理。
  2. 重试机制:通过RetryHTTPAdapter配置重试,能有效应对偶发的网络问题或服务端过载(返回429状态码)。这是提升测试稳定性的重要一环。
  3. 集中式日志:在每个请求前后记录关键信息,当测试失败时,能快速定位是请求没发出去,还是服务器返回了错误。
  4. 统一错误处理:在_request方法中集中处理异常和特定的状态码(如401),可以使业务层的代码更干净。

3.2 业务接口封装 (API层)

API层的作用是“翻译”,将HTTP API翻译成直观的Python函数。我们以用户登录和获取用户信息两个关联接口为例:

# api/auth_api.py from core.request_client import RequestClient from typing import Dict, Any class AuthAPI: def __init__(self, client: RequestClient): self.client = client def login(self, username: str, password: str) -> Dict[str, Any]: """ 登录接口 :return: 响应数据的字典,通常包含token """ endpoint = "/api/v1/auth/login" payload = {"username": username, "password": password} resp = self.client.post(endpoint, json=payload) # 假设成功响应为 {"code": 0, "message": "success", "data": {"token": "xxx"}} resp.raise_for_status() # 检查HTTP状态码 result = resp.json() # 可以在这里添加对业务状态码的检查,例如 if result['code'] != 0: raise ... return result # api/user_api.py from core.request_client import RequestClient from typing import Dict, Any class UserAPI: def __init__(self, client: RequestClient): self.client = client def get_user_profile(self, user_id: int) -> Dict[str, Any]: """ 获取用户资料,此接口需要认证(Token) """ endpoint = f"/api/v1/users/{user_id}" resp = self.client.get(endpoint) # Token已由client的session headers自动携带 resp.raise_for_status() return resp.json() def update_user_profile(self, user_id: int, update_data: Dict) -> Dict[str, Any]: """更新用户资料""" endpoint = f"/api/v1/users/{user_id}" resp = self.client.put(endpoint, json=update_data) resp.raise_for_status() return resp.json()

3.3 接口关联的核心:Pytest Fixture 实战

这是本文的精华所在。接口关联的本质是测试上下文(Context)的共享pytest.fixture完美地解决了这个问题。

我们通常在testcases/conftest.py中定义项目级的夹具:

# testcases/conftest.py import pytest from core.request_client import RequestClient from api.auth_api import AuthAPI from api.user_api import UserAPI import logging # 读取配置,这里简单示例,实际可以从configs/settings.ini读取 BASE_URL = "http://your-test-server.com" @pytest.fixture(scope="session") def api_client(): """创建一个贯穿整个测试会话的请求客户端""" client = RequestClient(base_url=BASE_URL) yield client # 测试会话结束后,可以在这里做一些清理工作,比如关闭session client.session.close() logging.info("API client session closed.") @pytest.fixture(scope="session") def auth_api(api_client): """依赖api_client,创建AuthAPI实例""" return AuthAPI(api_client) @pytest.fixture(scope="session") def user_api(api_client): """依赖api_client,创建UserAPI实例""" return UserAPI(api_client) @pytest.fixture(scope="session") def get_auth_token(auth_api): """ 关键Fixture:获取认证Token。 scope="session"意味着在整个pytest执行过程中,这个fixture只运行一次,返回的token被缓存并复用。 这避免了每个测试用例都去登录一次,极大提升效率。 """ # 从环境变量或配置文件中读取测试账号,切勿硬编码在代码中! test_username = "test_user" test_password = "test_pass123" login_resp = auth_api.login(test_username, test_password) token = login_resp.get("data", {}).get("token") if not token: pytest.fail("Login failed, cannot obtain token.") return token @pytest.fixture(scope="function") # 或 scope="class",根据需求调整 def authenticated_client(api_client, get_auth_token): """ 为需要认证的测试用例提供一个“已认证”的客户端。 scope="function"表示每个测试函数都会得到一个新的client副本(但共享底层的session和token)。 这样做的优点是测试之间完全隔离,不会因为一个测试修改了client headers而影响另一个。 """ # 复制一份client,避免直接修改全局的api_client fixture # 实际上,由于我们使用同一个session,设置headers会影响所有使用该session的请求。 # 更安全的做法是为每个需要独立上下文的测试创建一个新的RequestClient实例。 # 这里演示的是简单共享场景。 api_client.set_common_headers({"Authorization": f"Bearer {get_auth_token}"}) return api_client

Fixture 设计经验谈:

  • scope的选择get_auth_token用了scope="session",因为登录通常代价较高,且token在短时间内有效,在整个测试会话中共享是最佳实践。authenticated_client用了scope="function",确保每个测试函数开始时,认证头是确定设置好的,避免了测试间的状态污染。
  • Fixture 依赖authenticated_client依赖api_clientget_auth_tokenpytest会自动按依赖关系解析和执行它们,顺序是api_client->get_auth_token->authenticated_client
  • yield与清理:在api_client中,我们使用yield而不是returnyield之前的代码是setup(初始化),yield返回fixture对象,测试函数执行完毕后,会执行yield之后的代码进行清理(teardown),比如关闭网络连接。

4. 测试用例编写与数据驱动实践

有了强大的夹具,编写测试用例就变得非常清晰和简单。

4.1 基础测试用例示例

# testcases/test_auth.py import pytest import allure @allure.feature("认证模块") class TestAuth: """测试登录相关接口""" @allure.story("用户登录") @allure.title("使用正确的用户名和密码登录成功") def test_login_success(self, auth_api): """测试正常登录流程""" # 测试数据可以放在data/目录下,这里为了演示直接写 username = "correct_user" password = "correct_password" with allure.step("步骤1: 调用登录接口"): resp = auth_api.login(username, password) with allure.step("步骤2: 验证响应状态和数据结构"): # 断言HTTP状态码(通常由raise_for_status处理,这里演示业务断言) # 断言业务状态码 assert resp["code"] == 0, f"登录失败,返回码: {resp['code']}, 信息: {resp.get('message')}" # 断言返回数据中包含token字段 assert "token" in resp.get("data", {}), "响应中未找到token字段" # 断言token非空 assert resp["data"]["token"], "获取到的token为空" # 你可以把获取到的token存起来,供其他测试使用,但更推荐通过fixture共享 # self.auth_token = resp["data"]["token"] @allure.story("用户登录") @allure.title("使用错误的密码登录失败") @pytest.mark.parametrize("username, password, expected_code", [ ("correct_user", "wrong_pass", 1001), # 假设1001是密码错误码 ("non_exist_user", "any_pass", 1002), # 假设1002是用户不存在 ]) def test_login_failure(self, auth_api, username, password, expected_code): """测试登录失败的各种情况 - 数据驱动测试""" resp = auth_api.login(username, password) assert resp["code"] == expected_code, f"预期错误码{expected_code},实际返回{resp['code']}"

4.2 接口关联测试用例示例

这才是体现框架价值的地方。测试一个“先登录,然后修改个人资料”的业务流。

# testcases/test_user_profile.py import pytest import allure @allure.feature("用户管理模块") class TestUserProfile: """测试用户资料相关接口,依赖登录状态""" @allure.story("用户资料CRUD") @allure.title("登录后成功获取并更新自己的用户资料") def test_get_and_update_profile(self, authenticated_client, get_auth_token): """ 这个测试用例依赖两个fixture: 1. authenticated_client: 一个已经设置了认证头的请求客户端。 2. get_auth_token: 虽然authenticated_client内部已经用了,但这里显式依赖是为了在Allure报告中更清晰,或者需要用到token值本身。 注意:由于authenticated_client的scope是function,每个测试都会得到一个带有认证header的新上下文。 """ # 通过authenticated_client创建已认证的API实例 from api.user_api import UserAPI user_api = UserAPI(authenticated_client) # 假设我们知道当前登录用户的ID是1(实际项目中可能从登录响应或配置中获取) test_user_id = 1 new_nickname = "Pytest自动化达人_" + str(int(time.time())) # 加时间戳确保唯一 with allure.step("步骤1: 获取用户当前资料"): profile_before = user_api.get_user_profile(test_user_id) assert profile_before["code"] == 0 original_nickname = profile_before["data"]["nickname"] allure.attach(f"原始昵称: {original_nickname}", name="原始资料") with allure.step("步骤2: 更新用户昵称"): update_payload = {"nickname": new_nickname} update_resp = user_api.update_user_profile(test_user_id, update_payload) assert update_resp["code"] == 0, f"更新资料失败: {update_resp}" allure.attach(f"新昵称: {new_nickname}", name="更新请求") with allure.step("步骤3: 再次获取资料,验证更新生效"): profile_after = user_api.get_user_profile(test_user_id) assert profile_after["code"] == 0 updated_nickname = profile_after["data"]["nickname"] assert updated_nickname == new_nickname, f"昵称更新未生效,期望'{new_nickname}',实际'{updated_nickname}'" allure.attach(f"验证后的昵称: {updated_nickname}", name="更新验证") # 可选步骤4:清理数据,将昵称改回去(如果是可逆操作) # user_api.update_user_profile(test_user_id, {"nickname": original_nickname})

这个测试用例的精妙之处:

  1. 完全解耦:测试函数里看不到任何requests.get/post的代码,也看不到拼接URL、处理headers的细节。它只关心业务逻辑:获取 -> 更新 -> 验证。
  2. 依赖注入authenticated_client这个fixture像“魔法”一样,在测试开始前就为我们准备好了已认证的客户端。测试作者无需关心token如何获取、如何设置。
  3. 可读性高:配合Allure的@allure.step,测试步骤一目了然,无论是开发、测试还是产品经理,都能看懂这个测试在验证什么业务场景。

4.3 使用YAML管理测试数据

将测试数据从代码中分离是良好实践。我们创建一个YAML文件:

# data/test_user_data.yaml login_cases: success: username: "test_user" password: "secure_password_123" expected_code: 0 expected_token_present: true failure_wrong_pass: username: "test_user" password: "wrong" expected_code: 1001 expected_token_present: false failure_wrong_user: username: "not_exist" password: "any" expected_code: 1002 expected_token_present: false user_profile_cases: update_nickname: user_id: 1 update_field: "nickname" # 使用时间戳确保数据唯一,避免重复数据导致更新冲突 new_value: "AutoTest_{{timestamp}}"

然后在测试中读取并使用:

# testcases/test_auth_with_yaml.py import pytest import yaml import time import os def load_test_data(file_name): data_file_path = os.path.join(os.path.dirname(__file__), '..', 'data', file_name) with open(data_file_path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) class TestAuthWithYAML: @pytest.mark.parametrize("case_name, case_data", [ ("成功登录", "success"), ("错误密码", "failure_wrong_pass"), ("用户不存在", "failure_wrong_user"), ]) def test_login_with_data(self, auth_api, case_name, case_data): data = load_test_data("test_user_data.yaml") case = data["login_cases"][case_data] # 动态替换值,例如时间戳 # 这里示例简单,实际可能需要更复杂的模板渲染 resp = auth_api.login(case["username"], case["password"]) assert resp["code"] == case["expected_code"] if case["expected_token_present"]: assert "token" in resp.get("data", {}) else: assert "token" not in resp.get("data", {})

5. 测试执行、报告生成与常见问题排查

5.1 配置与执行

首先,安装依赖。requirements.txt文件内容如下:

pytest>=7.0.0 requests>=2.28.0 PyYAML>=6.0 allure-pytest>=2.9.0 pytest-html>=3.2.0 # 可选,生成HTML报告

安装命令:pip install -r requirements.txt

配置pytest.ini文件,让pytest运行更符合我们的习惯:

# pytest.ini [pytest] # 指定测试文件的位置和模式 testpaths = testcases python_files = test_*.py python_classes = Test* python_functions = test_* # 添加命令行默认选项 addopts = -v # 详细输出 --tb=short # 发生错误时,打印简短的traceback --strict-markers # 严格检查marker,避免拼写错误 --alluredir=./reports/allure_raw # 指定Allure原始数据输出目录 # 定义自定义标记,用于分类运行测试 markers = smoke: 冒烟测试用例 regression: 回归测试用例 slow: 运行缓慢的测试

运行测试:

  • 运行全部测试:pytest
  • 运行带有smoke标记的测试:pytest -m smoke
  • 运行特定文件:pytest testcases/test_auth.py
  • 运行特定类:pytest testcases/test_auth.py::TestAuth
  • 运行特定方法:pytest testcases/test_auth.py::TestAuth::test_login_success

5.2 生成Allure测试报告

Allure报告能直观展示测试层级、步骤、附件和错误信息。

  1. 运行测试并收集结果:上面的pytest.ini已经配置了--alluredir,运行pytest后会在./reports/allure_raw目录下生成一堆.json文件。
  2. 生成HTML报告
    • 方法一(在线查看):allure serve ./reports/allure_raw。这会启动一个本地服务并打开浏览器。
    • 方法二(生成静态文件):allure generate ./reports/allure_raw -o ./reports/allure_html --clean。然后在浏览器中打开./reports/allure_html/index.html

注意:需要先安装Allure命令行工具。具体安装方法请参考Allure官方文档。对于Mac用户,常用brew install allure;Windows用户可以通过Scoop或下载zip包配置环境变量。

5.3 常见问题与排查技巧实录

在实际操作中,你肯定会遇到各种问题。下面是我踩过的一些坑和解决方案:

问题1:测试用例间状态污染,一个用例修改了全局Client的headers,影响了其他用例。

  • 现象test_A通过了,test_B莫名其妙失败,报错401未授权。
  • 根因:多个测试用例共用了同一个RequestClient实例(或它的session),并且某个用例修改了它的session.headers
  • 解决方案
    1. 为每个测试函数创建独立的Client实例:将api_clientfixture的scope设为"function",并在fixture内部yield RequestClient(base_url)。这是最彻底的方法,但会牺牲一些性能(重复创建连接)。
    2. 使用copydeepcopy:在修改headers前,复制一份headers字典,修改副本。但这对session级别的cookies无效。
    3. 最佳实践(推荐)不要修改fixture返回的共享对象的状态。如果测试需要不同的配置(如不同的header),应该通过fixture的参数化或者创建新的fixture来获取不同配置的对象。例如,我们之前定义的authenticated_clientfixture,它依赖于api_client,但通过返回一个新配置的client(或直接修改共享client的headers,但scope设为function确保每个测试重新设置),实现了隔离。

问题2:遇到429 Too Many Requests错误。

  • 现象:测试运行时,突然大量接口返回429状态码。
  • 根因:测试脚本发送请求的频率超过了服务端的速率限制。
  • 解决方案
    1. 在请求客户端中增加重试机制:正如我们在RequestClient中配置的Retry,对于429状态码进行退避重试(backoff_factor)。这是第一道防线。
    2. 在测试用例中增加延迟:对于连续调用同一接口的测试,使用time.sleep(interval)在请求间加入间隔。
    3. 使用pytest--tb=short:当发生429时,缩短错误堆栈,便于快速定位是哪个用例触发的。
    4. Mock或Stub:对于非核心的、外部依赖的、容易限流的接口,在单元测试或集成测试中可以考虑使用pytest-mockresponses库进行模拟,避免触发真实限流。

问题3:Allure报告中的用例标题被长参数挤得换行,很难看。

  • 现象:使用了@pytest.mark.parametrize,并且参数值很长,导致Allure报告中的用例标题显示不全或排版混乱。
  • 解决方案
    1. 自定义idsparametrize装饰器接受一个ids参数,它是一个字符串列表,用于为每组参数提供一个简短的、可读的标识符,这个标识符会显示在报告标题中。
      @pytest.mark.parametrize( "username, password, expected_code", [ ("very_long_username_email@example.com", "pass", 0), ("short", "pass", 1002), ], ids=["login_with_long_email", "login_with_short_name"] # 指定显示名称 ) def test_login(self, username, password, expected_code): ...
    2. 使用@allure.title动态生成标题:在测试函数内部,使用allure.dynamic.title()来设置一个更清晰的标题。
      def test_login(self, username, password, expected_code): allure.dynamic.title(f"登录测试 - 用户: {username[:10]}...") # 截断长用户名 ...
    3. 优化参数值:尽量使用有业务含义的短字符串作为测试参数,避免使用超长的随机字符串或完整邮箱。

问题4:依赖服务不稳定,导致测试用例间歇性失败。

  • 现象:测试时好时坏,错误多是网络超时、连接拒绝等。
  • 解决方案
    1. 强化客户端的错误处理和重试:我们已经做了。
    2. 对测试用例进行标记:使用@pytest.mark.flaky(reruns=3, reruns_delay=2)标记那些依赖不稳定外部服务的用例,允许它们失败后自动重跑几次。需要安装pytest-rerunfailures插件。
    3. 建立测试环境健康检查:在conftest.py中定义一个session范围的fixture,在所有测试开始前,先调用一个简单的健康检查接口(如GET /health),如果失败则跳过所有测试或直接pytest.exit()
    4. 明确测试边界:分清单元测试、集成测试和端到端测试。接口自动化大部分属于集成测试,允许一定程度的不稳定,但需要通过CI/CD的稳定策略(如重试、失败通知)来管理。

问题5:测试数据管理混乱,尤其是需要清理的测试数据。

  • 现象:测试创建的数据没有清理,污染了后续测试或数据库。
  • 解决方案
    1. 使用Fixture的Teardown:在创建测试数据的fixture中使用yield,并在yield后编写清理逻辑。
      @pytest.fixture def temporary_user(api_client): """创建一个临时用户,测试后删除""" user_api = UserAPI(api_client) # Setup: 创建用户 user_data = {"name": "temp_user", ...} create_resp = user_api.create_user(user_data) user_id = create_resp["data"]["id"] yield user_id # 将user_id提供给测试用例使用 # Teardown: 删除用户 user_api.delete_user(user_id)
    2. 最终断言(Finalizers):使用request.addfinalizer注册清理函数,即使setup过程中出现异常,也会执行清理。
    3. 测试数据标识化:所有测试创建的数据,都带上一个唯一标识,比如f"test_{int(time.time())}_{random.randint(1000,9999)}"。在测试套件开始或结束时,可以运行一个清理脚本,删除所有带有该标识(或创建时间早于某个点)的测试数据。