1. 项目概述:为什么我们需要封装接口请求?
做接口自动化测试,尤其是用Pytest框架,你肯定写过不少这样的代码:在每个测试用例里,直接调用requests.get()或者requests.post(),然后断言响应结果。刚开始项目小、接口少,这么干似乎也没什么问题。但一旦项目规模上来,接口数量破百,维护成本就会指数级上升。今天改个域名,你得全局搜索替换;明天加个统一的鉴权头,你得在每个用例里手动添加;后天接口响应格式变了,你发现断言逻辑散落在几百个用例里,改起来想哭。
这就是我们今天要聊的核心:在Pytest框架下,如何系统性地封装接口请求。这不是一个简单的“写个函数把requests包一下”,而是一套从设计到落地的工程化实践。它关乎测试脚本的可维护性、可读性、可扩展性。一个好的封装,能让你的自动化项目在半年、一年后依然清晰、健壮,新同事接手也能快速理解;一个差的封装,或者干脆不封装,项目很快就会变成一座无人敢动的“屎山”。
简单说,封装接口请求,就是为了把那些变化的东西(如基础URL、公共头信息、通用处理逻辑)和不变的东西(具体的测试断言逻辑)分离开。让测试用例编写者只需要关心“测什么”,而不是“怎么发起请求、怎么处理通用问题”。接下来,我会结合我踩过的无数坑,带你从零构建一个既灵活又稳固的请求封装层。
2. 整体设计与核心思路拆解
在动手写代码之前,我们先得想清楚要封装什么,以及怎么组织这些代码。一个粗糙的封装可能比不封装更糟糕。
2.1 核心需求与设计目标
我们的封装需要满足以下几个核心目标:
- 统一入口与管理:所有接口请求都通过一个或几个核心类/函数发起,便于集中管理请求行为(如超时设置、重试机制)。
- 环境隔离与灵活配置:能轻松切换测试、预发布、生产等不同环境,基础URL、密钥等配置与代码分离。
- 简化用例编写:让测试用例尽可能简洁,用例脚本里只应包含测试数据、接口路径和断言逻辑。
- 增强健壮性与可维护性:
- 会话保持:自动处理
Cookie或Token,无需在每个请求中手动传递。 - 公共参数:自动为所有请求添加统一的
Headers(如Content-Type,User-Agent,Authorization)。 - 日志与报告:自动记录详细的请求和响应信息,方便调试和生成测试报告。
- 异常处理:对网络异常、状态码异常等进行统一捕获和处理,避免用例因非业务逻辑原因失败。
- 会话保持:自动处理
- 易于扩展:当需要支持新的协议(如
gRPC、WebSocket)或添加新的全局功能(如请求签名、流量录制)时,能方便地扩展,而不需要大规模修改现有用例。
2.2 技术选型与架构分层
基于以上目标,一个典型的分层架构如下:
- 配置层:使用
pytest.ini、conftest.py、config.yaml或.env文件管理环境变量和全局配置。 - 工具层:封装核心的HTTP客户端。这里我们依然选择
requests库,因为它足够强大和普及。我们会基于requests.Session()来构建,以天然支持会话保持。 - 业务层:根据被测系统的业务模块,对工具层进行二次封装,提供更语义化的方法。例如,
UserAPI、OrderAPI、ProductAPI等。 - 用例层:使用Pytest编写测试用例,调用业务层提供的方法,并专注于数据驱动和断言。
本次我们聚焦在工具层的封装,这是整个架构的基石。业务层的封装是建立在一个健壮的工具层之上的。
2.3 设计模式考量
我们将采用类似“装饰器”或“中间件”的思想来构建我们的请求客户端。核心是一个BaseApiClient类,它内部持有一个requests.Session实例,并通过一系列方法(如_add_common_headers,_handle_response)来修饰每一次请求的前后过程。这种设计模式通常被称为“客户端包装模式”或“门面模式”,它为复杂的requests库提供了一个更简单、更统一的接口。
3. 核心细节解析与实操要点
理解了整体设计,我们来深入每个核心模块的细节。这里有很多“坑”,处理不好会让封装变得很难用。
3.1 会话(Session)管理的艺术
requests.Session()是封装的灵魂。它自动保存cookies,并在同一会话的所有请求间保持,这对于需要登录态的测试至关重要。但直接使用裸的Session是不够的。
关键点1:会话的生命周期我们应该为每个测试用例或每个测试类创建一个新的Session吗?还是全局共享一个?这需要权衡。
- 每个用例独立Session:隔离性好,一个用例的Cookie不会污染另一个。但会创建大量TCP连接,效率稍低。适合用例间完全独立的场景。
- 全局共享一个Session:效率高,复用TCP连接。但需要小心处理Cookie的清理,避免状态泄漏。更常见的做法是,为每个测试类(
pytest中每个class)创建一个独立的Session实例。这可以通过Pytest的fixture作用域(scope=”class”)来实现,在类级别上做到隔离与效率的平衡。
关键点2:Session的配置在初始化Session时,就应该设置一些全局参数,这比在每次请求时设置要优雅得多。
import requests class BaseApiClient: def __init__(self, base_url): self.base_url = base_url.rstrip('/') # 确保没有多余的斜杠 self.session = requests.Session() # 设置会话级参数 self.session.headers.update({ 'User-Agent': 'My-Automation-Suite/1.0', 'Accept': 'application/json', }) self.session.timeout = (5, 30) # 连接超时5秒,读取超时30秒 # 可以挂载自定义的请求/响应钩子 self.session.hooks['response'] = [self._log_response]注意:
timeout参数务必设置!这是线上测试脚本卡死、资源耗尽的常见元凶。(connect_timeout, read_timeout)的元组形式能更精细地控制。
3.2 环境配置的优雅处理
硬编码的base_url是自动化脚本的“毒药”。我们必须将配置外置。
方案一:使用Pytest Fixture + 命令行参数这是非常Pytest风格的做法。在conftest.py中定义一个fixture来提供客户端实例。
# conftest.py import pytest from my_project.api_client import BaseApiClient def pytest_addoption(parser): parser.addoption("--env", action="store", default="test", help="Environment: test, staging, prod") @pytest.fixture(scope="session") def env_config(request): env = request.config.getoption("--env") # 可以从文件(如config.yaml)或字典中读取配置 config_map = { "test": {"base_url": "https://api-test.example.com"}, "staging": {"base_url": "https://api-staging.example.com"}, "prod": {"base_url": "https://api.example.com"}, } return config_map.get(env, config_map["test"]) @pytest.fixture(scope="class") # 每个测试类一个client def api_client(env_config): client = BaseApiClient(base_url=env_config["base_url"]) # 可以在这里进行一些全局初始化,比如登录获取token # client.login() yield client # 测试类结束后,可以做一些清理工作 client.logout()运行测试时,通过pytest --env=staging来指定环境。
方案二:使用环境变量或配置文件对于更复杂的配置(如多个微服务地址、数据库连接串),可以使用python-dotenv加载.env文件,或者直接解析yaml/json配置文件。然后在BaseApiClient的初始化方法中读取这些配置。
实操心得:我强烈推荐方案一(Pytest Fixture)。它与测试框架深度集成,作用域管理清晰,并且能利用Pytest强大的参数化机制。将api_client作为一个fixture注入到测试用例中,是符合Pytest哲学的最佳实践。
3.3 请求与响应的统一处理
这是封装的核心逻辑,目标是让发送请求和解析响应标准化。
请求封装: 我们需要一个通用的_request方法,作为所有HTTP方法(GET, POST, PUT, DELETE等)的底层实现。
def _request(self, method, endpoint, **kwargs): """ 统一请求方法 :param method: HTTP方法,'GET', 'POST'等 :param endpoint: 接口路径,如 '/user/login' :param kwargs: 传递给requests.request的其他参数,如 params, json, data, headers :return: 处理后的响应对象或数据 """ url = f"{self.base_url}{endpoint}" # 1. 预处理:合并公共头,处理特殊参数 headers = kwargs.pop('headers', {}) # 如果传入了json数据,自动设置Content-Type为application/json(如果未指定) if 'json' in kwargs and 'Content-Type' not in headers: headers['Content-Type'] = 'application/json' # 将自定义头与会话级头合并,自定义头优先级更高 merged_headers = {**self.session.headers, **headers} # 2. 发送请求(包含统一的超时和异常捕获) try: response = self.session.request( method=method, url=url, headers=merged_headers, timeout=self.session.timeout, **kwargs ) except requests.exceptions.Timeout: raise Exception(f"请求超时: {method} {url}") except requests.exceptions.ConnectionError: raise Exception(f"网络连接错误: {method} {url}") except requests.exceptions.RequestException as e: raise Exception(f"请求发生异常: {e}") # 3. 后处理:日志、状态码检查、响应体解析 self._log_request_and_response(response, method, url, kwargs) return self._handle_response(response)然后,基于_request提供便捷方法:
def get(self, endpoint, params=None, **kwargs): return self._request('GET', endpoint, params=params, **kwargs) def post(self, endpoint, json=None, data=None, **kwargs): return self._request('POST', endpoint, json=json, data=data, **kwargs) # ... 同理实现 put, delete, patch 等响应处理:_handle_response方法决定了用例里拿到的是什么。是原始的Response对象,还是直接解析好的JSON字典?我推荐后者,因为大多数现代API都返回JSON。
def _handle_response(self, response): """统一处理响应""" # 可以在这里根据项目约定,检查状态码 if not 200 <= response.status_code < 300: # 非2xx状态码,可以记录更详细的错误信息并抛出异常 error_info = f"状态码异常: {response.status_code}. URL: {response.url}. Response: {response.text}" self._logger.error(error_info) # 假设有logger raise AssertionError(error_info) # 抛出AssertionError,Pytest会将其识别为测试失败 # 尝试解析JSON try: return response.json() except ValueError: # 如果不是JSON,返回文本 return response.text注意:是否在工具层做状态码断言存在争议。我的建议是:做检查,抛异常。这能让用例在接口根本不可达或返回严重错误时快速失败,避免后续无意义的断言。但业务逻辑的状态码检查(如
404表示资源不存在是符合预期的)应留在用例层。
3.4 日志与可观测性
没有日志的自动化框架就像在黑暗中调试。好的日志能帮你快速定位是请求没发出去,还是服务器没响应,或者是响应解析错了。
在_log_request_and_response方法中,你需要记录:
- 请求方法、完整URL、请求头、请求体(敏感信息如密码需脱敏)。
- 响应状态码、响应头、响应体(可能很长,可以截断或记录到文件)。
- 耗时。
你可以使用Python标准的logging模块,并配置不同的Handler(如输出到控制台和文件)。在_request方法前后记录时间,就能计算出请求耗时,这对于性能监控很有帮助。
实操心得:不要在日志里打印完整的、未脱敏的认证信息(如Authorization: Bearer <token>)。可以将其替换为<REDACTED>或只打印前几位。这是一个基本的安全和隐私意识。
4. 实操过程:构建完整的BaseApiClient
现在,我们把上面的模块组合起来,写一个功能相对完整的BaseApiClient。我会加上详细的注释。
# api_client/base_client.py import json import logging from typing import Any, Dict, Optional, Union import requests class BaseApiClient: """ 基础API请求客户端封装 """ def __init__(self, base_url: str, timeout: tuple = (5, 30)): """ 初始化客户端 :param base_url: API基础地址,如 https://api.example.com :param timeout: (连接超时, 读取超时) 单位秒 """ self.base_url = base_url.rstrip('/') self.session = requests.Session() self.timeout = timeout self._setup_session() # 设置日志 self.logger = logging.getLogger(self.__class__.__name__) def _setup_session(self): """配置会话默认参数""" # 默认请求头 self.session.headers.update({ 'User-Agent': 'AutoTest-Client/1.0', 'Accept': 'application/json', 'Accept-Encoding': 'gzip, deflate', }) # 可以配置默认的认证,比如通过Basic Auth # self.session.auth = ('username', 'password') # 挂载响应钩子,用于统一日志记录(可选,另一种日志方式) # self.session.hooks['response'] = [self._response_hook] def _log_request_and_response(self, response: requests.Response, method: str, url: str, request_kwargs: dict): """记录详细的请求和响应信息(脱敏后)""" # 构建请求信息字符串 req_headers = self._sanitize_headers(dict(response.request.headers)) req_body = self._get_request_body(response.request, request_kwargs) # 构建响应信息字符串 resp_headers = dict(response.headers) try: resp_body = response.json() except ValueError: resp_body = response.text[:500] # 非JSON,截断显示 log_message = f""" ====== Request & Response ====== [Request] {method} {url} Headers: {json.dumps(req_headers, indent=2, ensure_ascii=False)} Body: {req_body} [Response] Status: {response.status_code} Headers: {json.dumps(resp_headers, indent=2, ensure_ascii=False)} Body: {json.dumps(resp_body, indent=2, ensure_ascii=False) if isinstance(resp_body, dict) else resp_body} ====== End ====== """ self.logger.info(log_message) def _sanitize_headers(self, headers: Dict) -> Dict: """脱敏请求头中的敏感信息""" sensitive_keys = ['authorization', 'token', 'api-key', 'password'] sanitized = headers.copy() for key in headers: if any(s in key.lower() for s in sensitive_keys): sanitized[key] = '<REDACTED>' return sanitized def _get_request_body(self, prepared_request: requests.PreparedRequest, request_kwargs: dict) -> Optional[str]: """获取并格式化请求体""" body = None if prepared_request.body: if isinstance(prepared_request.body, bytes): try: body = prepared_request.body.decode('utf-8') except UnicodeDecodeError: body = '<BINARY_DATA>' else: body = prepared_request.body # 如果通过json参数传递,request_kwargs里也有 elif 'json' in request_kwargs: body = json.dumps(request_kwargs['json'], ensure_ascii=False) return body def _handle_response(self, response: requests.Response) -> Union[Dict, str]: """统一处理响应,检查状态码并解析内容""" # 这里可以定义哪些状态码是“可接受”的异常(如404对于查找不存在的资源是预期的) # 对于非预期的状态码,直接抛出异常,让测试失败 if response.status_code >= 400: error_summary = f"HTTP {response.status_code} for {response.request.method} {response.url}" try: error_detail = response.json() except ValueError: error_detail = response.text[:200] self.logger.error(f"{error_summary}. Detail: {error_detail}") # 抛出一个自定义异常或AssertionError raise AssertionError(f"{error_summary}. Response: {error_detail}") # 尝试解析为JSON content_type = response.headers.get('Content-Type', '') if 'application/json' in content_type: try: return response.json() except json.JSONDecodeError as e: self.logger.warning(f"响应声明为JSON但解析失败: {e}. 返回文本。") return response.text # 其他类型返回文本 return response.text def _request(self, method: str, endpoint: str, **kwargs) -> Union[Dict, str]: """统一请求入口""" url = f"{self.base_url}{endpoint}" # 预处理headers headers = kwargs.pop('headers', {}) # 自动添加Content-Type for json if 'json' in kwargs and 'Content-Type' not in headers: headers['Content-Type'] = 'application/json' # 合并headers,传入的headers优先级更高 all_headers = {**self.session.headers, **headers} self.logger.debug(f"准备请求: {method} {url}") try: response = self.session.request( method=method, url=url, headers=all_headers, timeout=self.timeout, **kwargs ) except requests.exceptions.Timeout: self.logger.error(f"请求超时: {method} {url}") raise Exception(f"Request timeout after {self.timeout} seconds: {method} {url}") except requests.exceptions.ConnectionError: self.logger.error(f"连接错误: {method} {url}") raise Exception(f"Connection error: {method} {url}") except requests.exceptions.RequestException as e: self.logger.error(f"请求异常: {e}") raise # 记录日志 self._log_request_and_response(response, method, url, kwargs) # 处理响应 return self._handle_response(response) # 便捷方法 def get(self, endpoint: str, params: Optional[Dict] = None, **kwargs): return self._request('GET', endpoint, params=params, **kwargs) def post(self, endpoint: str, json: Optional[Dict] = None, data: Optional[Any] = None, **kwargs): return self._request('POST', endpoint, json=json, data=data, **kwargs) def put(self, endpoint: str, json: Optional[Dict] = None, data: Optional[Any] = None, **kwargs): return self._request('PUT', endpoint, json=json, data=data, **kwargs) def delete(self, endpoint: str, **kwargs): return self._request('DELETE', endpoint, **kwargs) def patch(self, endpoint: str, json: Optional[Dict] = None, data: Optional[Any] = None, **kwargs): return self._request('PATCH', endpoint, json=json, data=data, **kwargs)这个BaseApiClient已经具备了生产可用的雏形。它处理了会话、超时、日志、异常、响应解析和便捷方法。
5. 在Pytest中集成与使用
有了强大的客户端,下一步就是把它优雅地集成到Pytest中。
5.1 创建Conftest与Fixture
在项目根目录或测试目录下创建conftest.py。
# tests/conftest.py import pytest from my_project.api_client.base_client import BaseApiClient def pytest_addoption(parser): parser.addoption( "--env", action="store", default="test", choices=["test", "staging", "prod"], help="选择测试环境: test, staging, prod" ) @pytest.fixture(scope="session") def env_config(request): """读取环境配置""" env = request.config.getoption("--env") # 这里可以从yaml文件读取更复杂的配置 config = { "test": { "base_url": "https://api-test.myapp.com/v1", "app_key": "test_key_123", }, "staging": { "base_url": "https://api-staging.myapp.com/v1", "app_key": "staging_key_456", }, "prod": { "base_url": "https://api.myapp.com/v1", "app_key": "prod_key_789", # 谨慎使用! } } return config.get(env) @pytest.fixture(scope="class") def api_client(env_config): """提供配置好的API客户端实例,每个测试类一个""" client = BaseApiClient(base_url=env_config["base_url"]) # 示例:如果需要全局的签名头,可以在这里设置 # client.session.headers['X-App-Key'] = env_config['app_key'] # 示例:如果需要登录,可以在这里调用登录接口,并将token存入session # login_resp = client.post("/auth/login", json={"username": "test", "password": "123"}) # client.session.headers['Authorization'] = f"Bearer {login_resp['token']}" yield client # 测试类结束后,如果需要登出,可以在这里调用 # client.post("/auth/logout") # 或者清理session client.session.close()5.2 编写业务层封装(可选但推荐)
对于大型项目,直接使用BaseApiClient可能还不够直观。我们可以为每个业务模块创建专门的客户端。
# api_client/user_client.py from .base_client import BaseApiClient class UserApiClient(BaseApiClient): """用户模块API客户端""" def login(self, username: str, password: str): """登录并自动设置token到会话""" endpoint = "/auth/login" payload = {"username": username, "password": password} resp = self.post(endpoint, json=payload) # 假设返回格式为 {"code": 0, "data": {"token": "xxx"}} if resp.get("code") == 0: token = resp["data"]["token"] self.session.headers['Authorization'] = f"Bearer {token}" return resp else: raise Exception(f"登录失败: {resp}") def get_user_profile(self, user_id: int): """获取用户资料""" endpoint = f"/users/{user_id}" return self.get(endpoint) def create_user(self, user_info: dict): """创建用户""" endpoint = "/users" return self.post(endpoint, json=user_info) # ... 其他用户相关接口然后在conftest.py中,将api_clientfixture返回的对象换成UserApiClient实例。
5.3 编写清晰的测试用例
现在,编写测试用例变得非常简洁和专注。
# tests/test_user_api.py import pytest class TestUserApi: """用户接口测试类""" def test_login_success(self, api_client): """测试登录成功""" # 假设api_client已经是UserApiClient实例,并且登录逻辑在fixture里完成了 # 或者在这里调用登录 # resp = api_client.login("valid_user", "valid_pass") # assert resp["code"] == 0 # assert "token" in resp["data"] # 更常见的场景是,登录在fixture中完成,这里直接测试需要认证的接口 profile = api_client.get_user_profile(1) # 断言业务逻辑 assert profile["code"] == 0 assert profile["data"]["username"] is not None # 可以断言更多的字段... @pytest.mark.parametrize("username, password, expected_code", [ ("wrong_user", "123456", 1001), # 错误用户名 ("valid_user", "wrong_pass", 1002), # 错误密码 ("", "", 1003), # 空参数 ]) def test_login_failure(self, api_client, username, password, expected_code): """参数化测试登录失败场景""" # 注意:这里需要一个新的client,避免污染其他测试的状态 # 一种做法是创建一个不自动登录的fixture,或者在这里临时处理 resp = api_client.post("/auth/login", json={"username": username, "password": password}) assert resp["code"] == expected_code def test_create_user_with_invalid_email(self, api_client): """测试创建用户时邮箱格式错误""" invalid_user_info = { "username": "newuser", "email": "not-an-email", "password": "password123" } resp = api_client.create_user(invalid_user_info) # 断言接口返回了相应的验证错误 assert resp["code"] == 400 assert "email" in resp["message"].lower()可以看到,测试用例里几乎没有HTTP请求的细节了,全是业务数据和断言逻辑。这才是封装想要达到的效果。
6. 高级特性与扩展思路
基础封装完成后,可以考虑加入更多提升效率和健壮性的特性。
6.1 请求重试机制
对于网络抖动或服务瞬时不可用,重试机制能提高测试的稳定性。可以使用tenacity库或自己实现一个简单的装饰器。
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type class BaseApiClient: # ... 其他代码 ... @retry( stop=stop_after_attempt(3), # 最多重试3次 wait=wait_exponential(multiplier=1, min=2, max=10), # 指数退避等待 retry=retry_if_exception_type((requests.exceptions.ConnectionError, requests.exceptions.Timeout)), reraise=True # 重试耗尽后抛出原异常 ) def _request_with_retry(self, method, endpoint, **kwargs): """带重试的请求方法""" # 注意:重试只应用于幂等的GET、PUT、DELETE等,对于POST要小心。 # 可以在装饰器里根据method判断,或者单独为get方法实现重试。 return self._request(method, endpoint, **kwargs) def get_with_retry(self, endpoint, **kwargs): return self._request_with_retry('GET', endpoint, **kwargs)重要提示:对于非幂等操作(如POST创建资源),重试可能导致重复创建,必须谨慎使用或完全避免。通常只为
GET请求或明确幂等的PUT/DELETE添加重试。
6.2 响应数据验证与模式(Schema)校验
除了状态码,接口返回的数据结构是否符合约定也很重要。可以使用jsonschema或pydantic来定义和校验响应模式。
from jsonschema import validate, ValidationError class BaseApiClient: # ... 其他代码 ... def get_and_validate(self, endpoint, schema: dict, **kwargs): """发送GET请求并校验响应JSON是否符合给定的schema""" resp_data = self.get(endpoint, **kwargs) try: validate(instance=resp_data, schema=schema) self.logger.info(f"响应数据符合schema: {endpoint}") except ValidationError as e: self.logger.error(f"响应数据schema校验失败: {endpoint}. Error: {e.message}") raise AssertionError(f"Schema validation failed for {endpoint}: {e.message}") return resp_data在测试用例中,你可以预先定义好每个接口的schema,确保返回字段的类型、是否必需等符合预期。
6.3 文件上传与下载
文件上传是常见的需求。requests库处理multipart/form-data也很方便。
class BaseApiClient: # ... 其他代码 ... def upload_file(self, endpoint: str, file_field_name: str, file_path: str, extra_data: dict = None, **kwargs): """上传文件""" url = f"{self.base_url}{endpoint}" with open(file_path, 'rb') as f: files = {file_field_name: (os.path.basename(file_path), f)} data = extra_data or {} # 注意:上传文件时,requests会自动处理Content-Type,不要手动设置 if 'headers' in kwargs and 'Content-Type' in kwargs['headers']: kwargs['headers'].pop('Content-Type') response = self.session.post(url, files=files, data=data, **kwargs) return self._handle_response(response)对于下载,可以直接获取response.content并保存到本地。
6.4 性能监控与统计
在_request方法中记录每个请求的耗时,并聚合统计。可以集成到测试报告中,帮助发现性能退化接口。
import time class BaseApiClient: def _request(self, method, endpoint, **kwargs): url = f"{self.base_url}{endpoint}" start_time = time.time() try: response = self.session.request(...) finally: elapsed = time.time() - start_time self.logger.info(f"请求耗时: {elapsed:.2f}s - {method} {endpoint}") # 可以将耗时数据存入一个列表或发送到监控系统 if elapsed > 3.0: # 慢请求警告 self.logger.warning(f"慢请求警告!耗时 {elapsed:.2f}s - {method} {url}") # ... 后续处理7. 常见问题与排查技巧实录
在实际使用中,你肯定会遇到各种问题。这里记录一些典型场景和解决方法。
7.1 SSL证书验证问题
在内网测试或使用自签名证书时,可能会遇到SSLError。
- 临时禁用(不推荐用于生产):在
Session初始化时设置self.session.verify = False,并忽略警告urllib3.disable_warnings()。这有安全风险,仅用于开发/测试环境。 - 指定证书路径:
self.session.verify = ‘/path/to/cert.pem’。
7.2 连接池与资源管理
大量用例运行时,可能会遇到TCP端口耗尽的问题。
- 使用连接池:
requests.Session默认使用urllib3的连接池,这是好事。确保在测试结束时(如在fixture的teardown中)调用session.close()来释放连接。 - 调整池大小:可以通过
requests.adapters.HTTPAdapter来调整连接池参数。from requests.adapters import HTTPAdapter adapter = HTTPAdapter(pool_connections=10, pool_maxsize=100, max_retries=3) self.session.mount('http://', adapter) self.session.mount('https://', adapter)
7.3 处理Cookie与Token的过期
如果测试执行时间很长,Token可能会过期。
- 方案一(简单):在
_request方法中捕获特定的状态码(如401 Unauthorized),然后自动调用刷新Token的接口,并重试原请求。这需要实现一个刷新令牌的逻辑。 - 方案二(推荐):设计测试用例时,让每个用例或每个测试类独立登录获取
Token,避免长时间依赖同一个Token。这可以通过在@pytest.fixture(scope=”function”)级别的fixture中实现登录来完成。
7.4 异步接口测试
如果被测接口是异步的(请求立即返回,通过轮询或回调获取结果),封装需要更复杂。
- 轮询模式:封装一个
polling_get方法,在_request成功后,根据响应中的状态字段(如”status”: “processing”),循环调用另一个查询进度的接口,直到状态变为完成或失败。def poll_for_result(self, initial_response, poll_endpoint, poll_interval=2, timeout=30): task_id = initial_response['task_id'] start_time = time.time() while time.time() - start_time < timeout: time.sleep(poll_interval) status_resp = self.get(f"{poll_endpoint}/{task_id}") if status_resp['status'] == 'SUCCESS': return status_resp['result'] elif status_resp['status'] == 'FAILED': raise Exception(f"异步任务失败: {status_resp['error']}") raise TimeoutError(f"等待异步任务超时: {timeout}s")
7.5 测试数据管理与清理
自动化测试经常需要创建测试数据,并在测试后清理。
- Fixture的
yield与清理:在创建资源的fixture中使用yield,并在yield后编写清理代码(调用删除接口)。 - 使用独立的测试账号和数据标识:通过
uuid或时间戳生成唯一的测试数据(如用户名test_user_<timestamp>),避免冲突。即使清理失败,也不会影响后续测试。 - 最终兜底清理:可以写一个独立的清理脚本,定期清理标记为测试数据的陈旧资源。
封装好接口请求层,是构建一个可维护、可扩展的Pytest接口自动化测试项目的基石。它把繁琐、重复且易变的底层通信逻辑隐藏起来,让测试开发人员能更专注于业务逻辑验证本身。从简单的requests包装开始,逐步加入会话管理、环境配置、统一日志、异常处理、重试机制等,你的测试框架会变得越来越健壮。记住,好的封装不是一蹴而就的,而是在项目演进中不断迭代和完善出来的。