Python接口自动化测试实战:pytest与requests框架设计与工程化实践
1. 项目概述:为什么选择 pytest + requests 做自动化?
如果你正在为如何高效、稳定地编写接口自动化测试代码而头疼,那么 pytest 搭配 requests 的组合,很可能是你当前阶段最务实、最高效的选择。这不是一个花哨的框架,而是一套经过无数项目验证的“黄金搭档”。我见过太多团队一开始追求大而全的自动化平台,结果陷入维护泥潭,最终回归到这种轻量、灵活、开发体验极佳的模式。
简单来说, requests 库负责处理 HTTP 请求,它用起来就像用 Python 的字典和列表一样自然,让你能专注于业务逻辑,而不是底层网络细节。而 pytest 则是一个功能强大的测试框架,它不仅仅能“运行测试”,更重要的是,它提供了一套优雅的代码组织方式、丰富的断言机制、灵活的夹具(fixture)系统以及强大的插件生态。当这两者结合,你得到的不是一个僵化的“框架”,而是一个可以根据项目需求自由裁剪和扩展的“工具箱”。
这个组合特别适合以下场景:项目迭代快,接口变动频繁,需要快速响应;团队规模不大,测试人员需要兼顾功能测试和自动化脚本编写;或者你希望自动化代码本身易于阅读、维护和调试。它不强制你遵循某种复杂的模式(比如一开始就上 PO 模型),而是允许你从最简单的脚本开始,随着项目复杂度的提升,逐步引入更结构化的设计,平滑演进。接下来,我会详细拆解如何从零开始,构建一个既健壮又灵活的自动化代码编写思路。
2. 核心思路与框架设计:从脚本到工程
很多新手会直接把 requests 调用和断言塞进一个 .py 文件里,然后手动运行。这作为探索是可以的,但绝不是可持续的工程实践。我们的目标是建立一套可维护、可复用、可报告的代码结构。核心思路可以概括为: “数据与逻辑分离,用例与配置解耦,通过夹具管理资源” 。
2.1 分层架构设计
一个典型的、易于维护的 pytest + requests 项目会采用分层设计,这能让你的代码条理清晰,各司其职。
第一层:测试数据层。 这是最容易出问题的地方。切忌把测试数据(如 URL、请求参数、预期结果)硬编码在测试用例函数里。我们应该将它们剥离出来。对于简单的项目,可以使用 YAML 或 JSON 文件;对于需要关联、动态生成的数据,可以结合 Python 字典或 CSV 。我个人的习惯是,将接口的基本信息(如路径、方法)放在一个 config 模块或 YAML 中,而将具体的测试用例数据(尤其是多种参数组合)放在 data 目录下的独立文件里。这样做的好处是,当接口变更时,你通常只需要修改一两个数据文件,而不是在成百上千行测试代码里搜索替换。
第二层:核心请求层。 这一层是对 requests 库的封装。我们不应该在每个测试用例里都写 requests.get(url, params=params, headers=headers) 。而是应该创建一个通用的“请求客户端”。这个客户端会统一处理一些公共逻辑,比如:
- 基础 URL 的拼接。
- 通用请求头(如 Content-Type, Authorization Token)的管理。
- 统一的超时、重试策略。
- 全局的日志记录和请求/响应信息的打印(便于调试)。
- 对响应结果进行初步处理,比如自动将 JSON 字符串转为 Python 字典。
封装后,你的测试用例调用起来会非常简洁: client.send_request(api_name, data) 。这极大地减少了代码重复,也使得后续更换底层 HTTP 库(虽然 requests 很难被替代)或增加统一功能(如加密签名)变得容易。
第三层:测试用例层。 这是 pytest 的主场。在这一层,你编写以 test_ 开头的函数或方法。每个函数应该专注于一个具体的测试场景。用例函数内部逻辑应该尽量简单:准备测试数据 -> 调用封装好的请求客户端 -> 对响应进行断言。复杂的准备和清理工作,应该交给 pytest 的 fixture 。
第四层:夹具与钩子层。 这是 pytest 的精髓,也是区分“会用”和“精通”的关键。 Fixture 可以理解为测试的“脚手架”或“资源管理器”。你可以用它来:
- 初始化测试所需的数据库连接、测试用户。
- 为每个用例提供一个干净的、带特定 Token 的请求客户端。
- 在用例执行前后进行特定的设置和清理,比如创建测试订单并在测试后删除。 通过
@pytest.fixture装饰器定义,然后在测试用例函数参数中声明使用,pytest会自动帮你调用和管理它们的生命周期。
2.2 目录结构示例
一个清晰的目录结构是良好设计的开始。我推荐如下结构:
project_root/
├── conftest.py # 全局 pytest 配置和 fixture 定义
├── pytest.ini # pytest 配置文件
├── requirements.txt # 项目依赖
├── common/ # 公共模块
│ ├── __init__.py
│ ├── client.py # 封装的 requests 客户端
│ └── logger.py # 日志配置
├── config/ # 配置层
│ ├── __init__.py
│ └── api_config.yaml # 接口基础配置
├── test_data/ # 数据层
│ ├── __init__.py
│ ├── case_data_login.yaml
│ └── case_data_order.yaml
├── test_cases/ # 用例层
│ ├── __init__.py
│ ├── test_login.py
│ └── test_order.py
└── reports/ # 测试报告(自动生成)
└── html/
conftest.py 文件特别重要,它可以被放置在任何目录,其内部定义的 fixture 对该目录及其子目录下的所有测试文件生效。通常我们把最通用的 fixture (如请求客户端、日志对象)放在项目根目录的 conftest.py 中。
3. 核心工具深度解析:requests 封装与 pytest 夹具
3.1 如何封装一个健壮的 requests 客户端
直接使用 requests 虽然简单,但在自动化测试中远远不够。下面是一个我经过多次迭代后形成的客户端封装示例,它包含了几个关键特性:
# common/client.py
import requests
import json
import logging
from typing import Any, Dict, Optional, Union
class APIClient:
def __init__(self, base_url: str):
self.base_url = base_url.rstrip('/') # 去除末尾斜杠
self.session = requests.Session() # 使用 Session 保持连接,提升性能
self.logger = logging.getLogger(__name__)
# 设置默认请求头
self.session.headers.update({
'Content-Type': 'application/json; charset=utf-8',
'User-Agent': 'Pytest-Requests-Auto/1.0'
})
def _send_request(self,
method: str,
endpoint: str,
params: Optional[Dict] = None,
json_data: Optional[Dict] = None,
data: Optional[Dict] = None,
headers: Optional[Dict] = None,
**kwargs) -> requests.Response:
"""发送请求的核心方法"""
url = f"{self.base_url}/{endpoint.lstrip('/')}"
req_headers = {**self.session.headers, **(headers or {})}
# 记录请求日志(注意脱敏,不要打印密码等敏感信息)
self.logger.info(f"Request: {method.upper()} {url}")
if params:
self.logger.debug(f"Request Params: {params}")
if json_data:
self.logger.debug(f"Request JSON Body: {self._mask_sensitive_data(json_data)}")
if data:
self.logger.debug(f"Request Form Data: {self._mask_sensitive_data(data)}")
try:
# 统一增加超时设置,避免请求卡死
kwargs.setdefault('timeout', (10, 30)) # (连接超时, 读取超时)
resp = self.session.request(
method=method,
url=url,
params=params,
json=json_data,
data=data,
headers=req_headers,
**kwargs
)
# 记录响应日志
self.logger.info(f"Response Status: {resp.status_code}")
# 尝试记录响应体,对于非文本内容进行截断
try:
resp_body = resp.json()
self.logger.debug(f"Response Body (JSON): {json.dumps(resp_body, ensure_ascii=False)[:500]}...") # 截断防止日志过长
except json.JSONDecodeError:
self.logger.debug(f"Response Body (Text): {resp.text[:500]}...")
return resp
except requests.exceptions.Timeout:
self.logger.error(f"Request timeout: {method} {url}")
raise
except requests.exceptions.ConnectionError:
self.logger.error(f"Connection error: {method} {url}")
raise
except Exception as e:
self.logger.exception(f"Unexpected error during request: {e}")
raise
def _mask_sensitive_data(self, data: Dict) -> Dict:
"""简单的敏感信息脱敏,防止密码等写入日志"""
masked_data = data.copy()
sensitive_keys = ['password', 'token', 'authorization', 'secret']
for key in sensitive_keys:
if key in masked_data:
masked_data[key] = '***MASKED***'
return masked_data
# 提供便捷方法
def get(self, endpoint: str, **kwargs) -> requests.Response:
return self._send_request('GET', endpoint, **kwargs)
def post(self, endpoint: str, **kwargs) -> requests.Response:
return self._send_request('POST', endpoint, **kwargs)
def put(self, endpoint: str, **kwargs) -> requests.Response:
return self._send_request('PUT', endpoint, **kwargs)
def delete(self, endpoint: str, **kwargs) -> requests.Response:
return self._send_request('DELETE', endpoint, **kwargs)
# 一个常用的扩展:获取响应并自动解析为 JSON,失败时抛出清晰异常
def request_and_parse(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
resp = self._send_request(method, endpoint, **kwargs)
resp.raise_for_status() # 如果状态码不是 2xx,抛出 HTTPError
try:
return resp.json()
except json.JSONDecodeError as e:
self.logger.error(f"Failed to parse response as JSON: {resp.text[:200]}")
raise ValueError(f"Response is not valid JSON: {e}") from e
封装要点解析:
- 使用 Session :
requests.Session()可以复用底层的 TCP 连接,对于连续调用同一 host 的接口,能显著提升性能。 - 统一超时 :网络请求必须设置超时。
timeout=(10, 30)表示连接阶段10秒超时,接收数据阶段30秒超时。这是避免自动化任务因某个接口挂起而“卡死”的关键。 - 结构化日志 :详细的日志是调试的救命稻草。务必记录请求和响应的关键信息。注意使用
debug级别记录可能较大的请求体/响应体,并用_mask_sensitive_data方法对密码等字段进行脱敏,这是安全红线。 - 异常处理 :区分不同类型的网络异常(超时、连接错误),并记录完整的异常信息(
logger.exception),便于快速定位是网络问题、服务问题还是脚本问题。 - 便捷方法与增强 :提供
get,post等快捷方法,并可以像request_and_parse一样,封装“发送请求+状态码检查+JSON解析”这个最常见组合,让用例代码更简洁。
3.2 pytest fixture 的实战应用
Fixture 是 pytest 的灵魂。理解它的作用域(scope)和生命周期是高效使用的关键。
作用域(Scope):
function(默认):每个测试函数运行一次。class:每个测试类运行一次。module:每个.py文件运行一次。package:每个包运行一次。session:整个 pytest 执行过程运行一次。
一个综合性的 conftest.py 示例:
# conftest.py
import pytest
import logging
from common.client import APIClient
# 读取配置文件,这里用简单示例,实际可用 yaml、ini 或环境变量
BASE_URL = "https://api.example.com/v1"
@pytest.fixture(scope="session")
def logger():
"""会话级别的日志器 fixture"""
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
logging.basicConfig(level=logging.INFO, format=log_format)
# 可以添加文件处理器,将日志写入文件
# file_handler = logging.FileHandler('test_run.log')
# file_handler.setFormatter(logging.Formatter(log_format))
# logging.getLogger().addHandler(file_handler)
return logging.getLogger(__name__)
@pytest.fixture(scope="session")
def api_client():
"""创建一个全局的 API 客户端,整个测试会话只初始化一次"""
client = APIClient(BASE_URL)
yield client # yield 之前是 setup,之后是 teardown
# 如果需要,可以在这里执行会话结束后的清理,比如关闭连接
client.session.close()
print("\n>>> 所有测试执行完毕,API 客户端连接已关闭。")
@pytest.fixture(scope="function")
def authenticated_client(api_client):
"""
基于全局客户端,为每个测试函数生成一个已认证的客户端。
这是一个典型的 fixture 依赖链:authenticated_client -> api_client
"""
# 假设我们需要先调用登录接口获取 token
login_payload = {"username": "test_user", "password": "test_pass123"}
# 注意:实际项目中,密码应从安全的环境变量或配置中心读取,绝不能硬编码!
try:
resp = api_client.post("/auth/login", json=login_payload)
token = resp.json()["data"]["token"]
# 将 token 设置到 session 的 headers 中
api_client.session.headers.update({'Authorization': f'Bearer {token}'})
except Exception as e:
pytest.fail(f"用户登录失败,无法获取 token: {e}")
yield api_client # 将携带了 token 的客户端提供给测试用例使用
# 每个用例执行完后,清理 token,避免影响下一个用例(如果需要隔离的话)
api_client.session.headers.pop('Authorization', None)
@pytest.fixture
def create_test_data():
"""一个用于准备测试数据的 fixture"""
data = {"name": f"test_item_{pytest.current_test_name}"}
yield data
# 测试后清理数据(这里只是示例,实际需要调用删除接口)
print(f"清理测试数据: {data}")
# 钩子函数示例:在每个测试开始和结束时打印信息
def pytest_runtest_logstart(nodeid, location):
print(f"\n=== 开始测试: {nodeid} ===")
def pytest_runtest_logfinish(nodeid, location):
print(f"=== 结束测试: {nodeid} ===\n")
Fixture 使用心得:
-
yield魔法 :yield语句将 fixture 分为两部分。yield之前的代码是“设置”,yield返回的值是提供给测试用例使用的对象,yield之后的代码是“清理”。这比旧的request.addfinalizer方式更清晰。 - 作用域选择 :
api_client用了session作用域,因为创建 HTTP 会话成本较高,且测试间通常无需隔离。authenticated_client用了function作用域,因为每个测试用例可能需要独立的认证状态(比如用不同权限的用户测试)。错误的作用域选择会导致测试相互污染或效率低下。 - Fixture 依赖 :
authenticated_client依赖api_client,只需在参数列表中声明。pytest 会自动按依赖关系顺序调用。 - 失败处理 :在
authenticated_client中,如果登录失败,我们使用pytest.fail直接让依赖它的测试用例标记为失败,这比让用例自己去处理“客户端未初始化”的错误更清晰。
4. 测试用例编写与断言艺术
有了强大的客户端和灵活的 fixture,编写测试用例就变成了一件愉快的事情。核心是让用例函数保持简洁、可读。
4.1 数据驱动测试
这是提高用例覆盖率和维护性的关键。pytest 可以通过 @pytest.mark.parametrize 装饰器轻松实现。
# test_cases/test_login.py
import pytest
import allure # 可选,用于生成更美观的 Allure 报告
class TestLogin:
"""登录模块测试"""
# 用例数据可以定义在类内部,或者从外部文件加载
@pytest.mark.parametrize("username, password, expected_code, expected_msg", [
("correct_user", "correct_pwd", 200, "success"),
("wrong_user", "correct_pwd", 401, "用户名或密码错误"),
("correct_user", "", 400, "密码不能为空"),
("", "correct_pwd", 400, "用户名不能为空"),
("a" * 101, "correct_pwd", 400, "用户名长度超限"), # 边界值测试
])
def test_login_with_different_input(self, api_client, username, password, expected_code, expected_msg):
"""测试不同输入组合下的登录行为"""
payload = {"username": username, "password": password}
resp = api_client.post("/auth/login", json=payload)
# 断言1:状态码
assert resp.status_code == expected_code, f"预期状态码{expected_code},实际为{resp.status_code},响应:{resp.text}"
# 断言2:响应消息(如果状态码非200,响应结构可能不同,需要安全访问)
resp_json = resp.json()
if resp.status_code == 200:
assert resp_json["message"] == expected_msg
assert "token" in resp_json["data"] # 成功时应返回 token
assert isinstance(resp_json["data"]["token"], str) and len(resp_json["data"]["token"]) > 10
else:
# 失败时,断言错误信息包含预期内容(有时是模糊匹配)
assert expected_msg in resp_json.get("message", "")
# 使用从 YAML 文件加载的数据
@pytest.mark.parametrize("case_data", pytest.data_loader.load_yaml("test_data/case_data_login.yaml"))
def test_login_with_yaml_data(self, api_client, case_data):
"""使用外部 YAML 文件驱动测试"""
resp = api_client.request_and_parse(
method=case_data["method"],
endpoint=case_data["endpoint"],
json=case_data["request"]
)
# 使用深层断言,验证响应中的特定字段
assert resp["code"] == case_data["expected"]["code"]
assert resp["data"]["userId"] == case_data["expected"]["userId"]
# 可以验证更多字段...
数据驱动要点:
- 参数化装饰器 :
@pytest.mark.parametrize的第一个参数是字符串,定义了注入到测试函数中的参数名,第二个参数是一个可迭代对象(列表、元组),每个元素是一组测试数据。 - 清晰的断言信息 :在
assert语句后添加自定义的错误信息(如f”预期…实际…”),这在断言失败时能提供极其有价值的上下文,让你一眼就知道是哪个数据组出了问题,而不需要再去翻看请求日志。 - 灵活的数据源 :数据可以直接写在代码里(适合简单、少变的场景),也可以从
YAML、JSON、Excel甚至数据库中加载。我通常会写一个简单的pytest插件或fixture(如上面的pytest.data_loader假设)来统一加载数据。
4.2 断言:不仅仅是 assert
Python 自带的 assert 很简单,但在复杂的响应断言中可能力不从心。 pytest 自身提供了一些增强,但更推荐使用专门的断言库,如 assertpy 或 pytest-assume (用于软断言)。
import pytest
from assertpy import assert_that
def test_complex_response_assertion(authenticated_client):
"""测试获取用户信息的复杂断言"""
resp_data = authenticated_client.request_and_parse("GET", "/user/profile")
# 使用 assertpy 进行流式、可读性更强的断言
assert_that(resp_data).is_not_none()
assert_that(resp_data).contains_key('data')
user_data = resp_data['data']
assert_that(user_data).contains_key('id', 'username', 'email')
assert_that(user_data['id']).is_type_of(int).is_greater_than(0)
assert_that(user_data['username']).is_length(5, 20) # 用户名长度在5-20之间
assert_that(user_data['email']).matches(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$') # 邮箱格式正则匹配
# 使用 pytest-assume 进行软断言(所有断言都会执行,最后汇总失败)
# 需要先安装 pytest-assume
# import pytest_assume
# pytest.assume(user_data['status'] == 'active')
# pytest.assume(user_data['phone'] is not None)
# 如果上面两个断言一个失败一个成功,测试不会在第一个失败处停止,而是继续执行第二个。
断言策略:
- 基础断言 :对于简单、关键的断言(如状态码必须为200),使用原生
assert并附加清晰错误信息。 - 复杂对象断言 :对于需要验证多个字段、类型、范围的复杂响应,使用
assertpy能让代码更清晰、更接近自然语言。 - 软断言 :在需要验证一个接口返回的多个相互独立的字段时,使用
pytest-assume。这样即使第一个字段验证失败,后面的验证仍会执行,你可以在一次测试执行中看到所有不符合预期的点,而不是修好一个错误跑一次测试。
5. 高级技巧与实战问题排查
5.1 处理动态依赖与测试数据隔离
自动化测试中最棘手的问题之一就是测试数据。比如测试“删除订单”接口,你需要先有一个订单。这个订单不能是线上环境的真实数据,也不能是写死的固定ID(可能被其他人删除)。
解决方案:夹具组合与清理。
import pytest
import random
import string
@pytest.fixture
def random_order_id():
"""生成一个随机的订单ID,用于测试"""
return f"TEST_ORDER_{''.join(random.choices(string.ascii_uppercase + string.digits, k=8))}"
@pytest.fixture
def created_order(authenticated_client, random_order_id):
"""创建一个测试订单 fixture,测试后自动清理"""
# 1. 准备创建订单的数据
order_data = {
"orderId": random_order_id,
"productId": "prod_001",
"quantity": 2
}
# 2. 调用创建订单接口
create_resp = authenticated_client.post("/orders", json=order_data)
assert create_resp.status_code == 201, f"创建订单失败: {create_resp.text}"
created_order_info = create_resp.json()["data"]
# 3. 将创建好的订单信息 yield 给测试用例使用
yield created_order_info
# 4. 测试用例执行完毕后,清理(删除)这个订单
print(f"\n>>> 开始清理测试订单: {created_order_info['id']}")
try:
delete_resp = authenticated_client.delete(f"/orders/{created_order_info['id']}")
# 这里可以断言删除成功,但即使失败也不应影响测试结果主体,记录警告即可
if delete_resp.status_code not in [200, 204]:
print(f"警告:清理订单 {created_order_info['id']} 失败,状态码: {delete_resp.status_code}")
except Exception as e:
print(f"警告:清理订单 {created_order_info['id']} 时发生异常: {e}")
def test_delete_order(created_order):
"""测试删除订单,依赖于 created_order fixture"""
order_id = created_order["id"]
# 这里可以直接测试删除,或者测试其他关于这个订单的操作
# 因为 created_order fixture 已经确保了订单存在
# 测试完成后,fixture 的 teardown 部分会自动删除订单
print(f"测试正在使用订单: {order_id}")
# ... 具体的删除断言逻辑
关键点:
-
random_order_id:生成唯一标识,避免冲突。 -
created_order:这是一个“创建-清理”模式的经典 fixture。它在yield前创建资源,提供给测试用例;在yield后执行清理。即使测试用例执行失败,pytest也会保证清理代码被执行(除非整个进程崩溃)。 - 清理的健壮性 :清理操作(删除订单)本身也可能失败。在实际项目中,我们通常不会因为清理失败而让测试用例失败(
assert),而是记录警告日志。更复杂的场景下,可以设置一个“待清理队列”,在session级别的 fixture 的 teardown 中统一进行最终清理。
5.2 常见问题与排查技巧实录
在实际编写和运行 pytest + requests 自动化脚本时,你会遇到各种各样的问题。下面是我踩过的一些坑和总结的技巧。
问题1:测试用例相互污染。
- 现象 :用例A修改了全局配置(如请求头的 Token),导致用例B运行异常。
- 根因 :
fixture作用域设置不当,或者直接在测试用例中修改了fixture返回的可变对象(如api_client.session.headers)。 - 解决 :
- 为需要隔离状态的测试使用
function作用域的 fixture。例如,每个用例使用独立的authenticated_client。 - 在
fixture的yield之后(teardown 部分)重置状态,如上文authenticated_client中清理 Token 的操作。 - 避免直接修改
fixture返回的对象内部状态。如果必须,考虑使用copy.deepcopy返回一个副本。
- 为需要隔离状态的测试使用
问题2:遇到 429 Too Many Requests 错误。
- 现象 :在快速连续运行测试时,服务端返回 HTTP 429 状态码。
- 根因 :触发了服务端的限流策略。
- 解决 :
- 降低请求频率 :在客户端封装层(
APIClient._send_request)中,对于非幂等的POST/PUT/DELETE请求,可以简单增加time.sleep(0.5)。对于GET请求,如果服务端允许,可以稍快一些。但这会拖慢测试速度。 - 实现重试机制 :使用
tenacity或urllib3的Retry库,对 429 状态码进行指数退避重试。这是更优雅的方案。
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception import requests def is_rate_limit_error(exception): return isinstance(exception, requests.exceptions.HTTPError) and exception.response.status_code == 429 class APIClient: # ... 其他代码 ... @retry( stop=stop_after_attempt(5), # 最多重试5次 wait=wait_exponential(multiplier=1, min=2, max=10), # 指数退避,2s, 4s, 8s... retry=retry_if_exception(is_rate_limit_error) ) def _send_request(self, method, endpoint, **kwargs): # ... 原有的请求发送逻辑 ... resp.raise_for_status() # 触发 HTTPError 供 tenacity 判断 return resp- 与开发沟通 :确认测试环境的限流策略,看是否可以临时调高阈值或为测试账号设置白名单。
- 降低请求频率 :在客户端封装层(
问题3:测试报告不够直观。
- 现象 :控制台输出杂乱,无法快速看清哪些用例通过/失败,失败原因是什么。
- 解决 :
- 使用
pytest-html插件生成 HTML 报告 :安装后,运行pytest --html=report.html。报告会包含用例列表、状态、耗时和失败时的 traceback。 - 使用
pytest-allure生成 Allure 报告 :Allure 报告非常强大美观,可以展示用例层级、步骤、附件(如请求/响应日志、截图)。需要先安装allure-pytest,运行测试时添加--alluredir=./allure-results,然后用allure serve ./allure-results查看。 - 善用
-v和-s参数 :-v显示详细输出,-s允许打印print语句和日志(默认被捕获)。调试时非常有用。
- 使用
问题4:如何高效地只运行一部分测试?
- 技巧 :
- 标记(Mark) :使用
@pytest.mark.smoke标记冒烟测试用例,然后运行pytest -m smoke。 - 按名称运行 :
pytest -k “login”会运行所有名称中包含 “login” 的测试类、函数。 - 按目录/文件运行 :直接指定路径,如
pytest test_cases/或pytest test_cases/test_login.py。 - 上次运行失败 :
pytest --lf只重新运行上次失败的用例。
- 标记(Mark) :使用
问题5:环境配置管理混乱。
- 现象 :测试脚本中硬编码了测试环境的 URL、账号密码,无法适配多环境(开发、测试、预生产)。
- 解决 :使用
pytest的插件如pytest-base-url,或者更通用的python-dotenv+pytest自定义选项。
运行命令:# conftest.py import os import pytest from dotenv import load_dotenv load_dotenv() # 从 .env 文件加载环境变量 def pytest_addoption(parser): parser.addoption("--env", action="store", default="test", help="选择测试环境: dev, test, staging") @pytest.fixture(scope="session") def base_url(pytestconfig): env = pytestconfig.getoption("--env") env_urls = { "dev": "https://dev-api.example.com", "test": "https://test-api.example.com", "staging": "https://staging-api.example.com", } url = env_urls.get(env) if not url: raise ValueError(f"未知的环境: {env}") return url @pytest.fixture(scope="session") def test_username(): # 从环境变量读取,安全且可配置 user = os.getenv("TEST_USERNAME") if not user: pytest.fail("请设置环境变量 TEST_USERNAME") return userpytest --env=staging。这样就能轻松切换测试环境。
更多推荐
所有评论(0)