1. 项目概述
在接口自动化测试中,登录模块往往是整个测试流程的起点和基础。很多接口都需要依赖登录后获取的token或其他认证信息才能正常执行。然而,当我们将这些测试用例放在一起运行时,经常会遇到一个棘手的问题:由于pytest默认的测试执行顺序是不确定的,可能导致依赖登录的接口在登录接口之前执行,从而引发大量测试失败。
1.1 核心需求解析
我们需要解决两个关键问题:
- 确保登录测试用例在所有依赖它的用例之前执行
- 将登录成功后的状态(如token、用户信息)安全地共享给后续测试用例
传统的解决方案往往是通过测试用例命名来控制执行顺序,或者将所有依赖登录的断言放在一个大的测试函数中。这些方法要么不够可靠,要么破坏了测试的原子性和可维护性。
2. 技术方案设计
2.1 整体架构
我们采用pytest-order插件来控制执行顺序,结合session级别的fixture来实现状态共享。这套方案的优点在于:
- 执行顺序明确可控
- 状态共享安全可靠
- 各测试用例保持独立性和原子性
- 易于维护和扩展
2.2 关键技术选型
2.2.1 pytest-order插件
pytest-order是一个专门用于控制pytest测试执行顺序的插件。它提供了多种标记方式:
- 绝对顺序:@pytest.mark.order(1)
- 相对顺序:@pytest.mark.order(before="test_other")
- 依赖关系:@pytest.mark.dependency()
我们选择使用绝对顺序标记,因为登录接口必须第一个执行的需求是明确的。
2.2.2 session级别fixture
pytest的fixture系统提供了不同作用域的生命周期管理。我们选择scope="session"的fixture是因为:
- 登录状态在整个测试会话期间保持不变
- 避免重复登录带来的性能开销
- 确保所有测试用例看到的是同一份状态数据
3. 详细实现步骤
3.1 环境准备
首先需要安装必要的依赖:
bash复制pip install pytest pytest-order requests
建议使用虚拟环境来管理这些依赖:
bash复制python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
3.2 项目结构设计
推荐的项目结构如下:
code复制project/
├── conftest.py
├── tests/
│ ├── __init__.py
│ ├── test_login.py
│ ├── test_user.py
│ └── test_order.py
└── requirements.txt
3.3 核心代码实现
3.3.1 conftest.py配置
python复制import pytest
@pytest.fixture(scope="session")
def api_logined_data():
"""全局共享的登录状态存储
使用字典结构方便扩展,可以存储token、用户信息、
权限列表等各种登录后获取的数据
"""
return {
# 预置一些默认值,避免KeyError
"token": None,
"username": None,
"logged_in": False
}
@pytest.fixture(scope="session")
def api_client():
"""模拟API客户端
实际项目中可以替换为真实的HTTP客户端,
如requests.Session或自定义的API Client类
"""
from unittest.mock import MagicMock
client = MagicMock()
# 配置mock的post方法模拟登录
def mock_post(url, json=None, **kwargs):
mock_response = MagicMock()
if url == "/login":
if json.get("username") == "test" and json.get("password") == "123456":
mock_response.status_code = 200
mock_response.json.return_value = {
"token": "fake_token_123",
"username": "test_user"
}
else:
mock_response.status_code = 401
else:
mock_response.status_code = 404
return mock_response
client.post = mock_post
# 配置mock的get方法模拟其他接口
def mock_get(url, headers=None, **kwargs):
mock_response = MagicMock()
if url == "/user/profile":
if headers and headers.get("token") == "fake_token_123":
mock_response.status_code = 200
mock_response.json.return_value = {
"username": "test_user",
"email": "test@example.com"
}
else:
mock_response.status_code = 403
else:
mock_response.status_code = 404
return mock_response
client.get = mock_get
return client
3.3.2 登录测试用例
python复制# test_login.py
import pytest
@pytest.mark.order(1)
def test_api_login(api_client, api_logined_data):
"""登录接口测试用例
第一个执行,成功后将token等信息存入api_logined_data
"""
# 模拟登录请求
response = api_client.post(
"/login",
json={"username": "test", "password": "123456"}
)
# 验证响应状态码
assert response.status_code == 200
# 解析响应数据
data = response.json()
token = data.get("token")
username = data.get("username")
# 验证必要字段存在
assert token is not None
assert username is not None
# 保存到共享存储
api_logined_data.update({
"token": token,
"username": username,
"logged_in": True
})
print(f"\n登录成功,获取到token: {token}")
3.3.3 依赖登录的测试用例
python复制# test_user.py
import pytest
def test_get_user_profile(api_client, api_logined_data):
"""获取用户信息接口测试
依赖登录状态,如果登录失败则跳过
"""
if not api_logined_data.get("logged_in"):
pytest.skip("前置登录失败,跳过当前测试")
token = api_logined_data["token"]
username = api_logined_data["username"]
# 使用token发起请求
headers = {
"Authorization": f"Bearer {token}",
"X-Username": username
}
response = api_client.get("/user/profile", headers=headers)
# 验证响应
assert response.status_code == 200
profile = response.json()
assert profile["username"] == username
assert "@" in profile.get("email", "")
3.4 边界情况处理
3.4.1 登录失败处理
python复制# test_login_fail.py
import pytest
@pytest.mark.order(1)
def test_api_login_fail(api_client, api_logined_data):
"""测试登录失败场景"""
# 使用错误密码
response = api_client.post(
"/login",
json={"username": "test", "password": "wrong"}
)
assert response.status_code == 401
api_logined_data["logged_in"] = False
def test_dependent_after_fail(api_logined_data):
"""登录失败后依赖的测试"""
if not api_logined_data.get("logged_in"):
pytest.skip("登录失败,跳过测试")
assert False, "这行不应该执行"
3.4.2 Token过期处理
在实际项目中,token可能有有效期。我们可以扩展fixture来处理token刷新:
python复制# conftest.py
@pytest.fixture(scope="session")
def auth_headers(api_logined_data):
"""自动处理token过期的请求头
每次调用都会检查token是否即将过期,
必要时自动刷新
"""
def get_headers():
if not api_logined_data.get("logged_in"):
pytest.skip("未登录")
# 这里可以添加token刷新逻辑
return {
"Authorization": f"Bearer {api_logined_data['token']}"
}
return get_headers
4. 高级应用场景
4.1 多用户登录管理
当需要测试不同权限用户时,可以扩展共享存储:
python复制# conftest.py
@pytest.fixture(scope="session")
def multi_user_data():
"""多用户登录状态管理"""
return {
"admin": {"token": None, "logged_in": False},
"user": {"token": None, "logged_in": False}
}
# test_multi_login.py
@pytest.mark.order(1)
def test_admin_login(api_client, multi_user_data):
"""管理员登录"""
response = api_client.post(
"/login",
json={"username": "admin", "password": "admin123"}
)
assert response.status_code == 200
multi_user_data["admin"].update({
"token": response.json()["token"],
"logged_in": True
})
4.2 并行测试支持
虽然session fixture在并行测试中需要特殊处理,但可以通过以下方式支持:
python复制# conftest.py
@pytest.fixture(scope="session")
def shared_login(pytestconfig):
"""支持并行测试的共享登录"""
worker_id = getattr(pytestconfig, "worker_id", "master")
return {
"token": f"token_{worker_id}",
"worker": worker_id
}
5. 最佳实践与常见问题
5.1 最佳实践
- 明确的命名规范:对共享fixture使用清晰的前缀,如
shared_或global_ - 最小化共享状态:只共享必要的数据,避免测试间耦合
- 完善的清理机制:即使测试失败也要确保清理资源
- 详细的日志记录:记录关键状态变化,方便调试
5.2 常见问题排查
问题1:测试顺序不符合预期
- 检查是否正确定义了order标记
- 确保所有测试文件被正确发现
- 验证pytest-order插件版本
问题2:共享状态被意外修改
- 使用不可变数据结构
- 在fixture中返回深拷贝
- 添加状态验证断言
问题3:并行测试时状态混乱
- 使用pytest-xdist的worker_id隔离状态
- 考虑使用测试数据库隔离
- 为每个worker创建独立会话
6. 性能优化建议
- 减少重复登录:合理使用session作用域
- 并行化独立测试:使用pytest-xdist插件
- 模拟外部服务:使用pytest-mock减少网络延迟
- 选择性执行:使用-k选项过滤测试用例
提示:在大型测试套件中,可以将登录测试单独放在一个文件中,并使用pytest的--first选项确保它最先运行。
7. 扩展思考
这套方案不仅适用于登录模块,还可以应用于:
- 初始化测试数据
- 获取一次性配置
- 建立数据库连接
- 准备测试文件
关键是将"前置条件"与"测试逻辑"分离,通过明确的执行顺序控制和安全的状态共享机制,构建可靠、可维护的自动化测试体系。