Pytest 中的 Mark 详解
1. 什么是 Mark
1.1 基本概念
Mark(标记) 是 pytest 提供的一个强大功能,允许你给测试用例添加”标签”或”标记”,用于对测试进行分类、筛选和管理。
1.2 形象比喻
想象一下,你有一个装满各种书籍的图书馆:
- 有些书是”小说”
- 有些书是”技术类”
- 有些书是”适合儿童阅读”
- 有些书是”需要特殊权限才能借阅”
Mark 就像是给测试用例贴上的”标签”,让你可以:
- 快速找到特定类型的测试(比如只运行接口测试)
- 跳过某些测试(比如跳过慢速测试)
- 对测试进行分类管理(比如标记为”登录相关”、”支付相关”)
1.3 Mark 的作用
Mark 主要有以下几个作用:
- 分类管理:将测试用例按照功能、类型、优先级等进行分类
- 选择性运行:只运行特定标记的测试用例
- 条件执行:根据条件跳过或执行某些测试
- 参数化:为测试用例提供不同的参数组合
- 标记预期失败:标记已知的 bug,避免影响测试结果
1.4 为什么需要 Mark
在实际项目中,你可能会遇到以下场景:
- 场景 1:项目有 1000 个测试用例,但只想运行接口测试相关的 200 个
- 场景 2:有些测试运行很慢,平时开发时不想运行,但 CI/CD 时需要运行
- 场景 3:有些测试需要特定的环境或权限,在普通环境下需要跳过
- 场景 4:有些测试是针对特定功能的,需要分组管理
没有 Mark 的情况:
# 只能运行所有测试,无法筛选
pytest # 运行所有 1000 个测试,耗时很长
有 Mark 的情况:
# 只运行接口测试
pytest -m api # 只运行 200 个接口测试,快速高效
2. Mark 的基本语法
2.1 使用装饰器添加 Mark
Mark 通过 @pytest.mark.标记名 装饰器来使用,放在测试函数或测试类的前面。
2.1.1 基本语法格式
import pytest
@pytest.mark.标记名
def test_function():
pass
2.1.2 最简单的示例
import pytest
@pytest.mark.api
def test_login():
"""这是一个接口测试"""
assert 1 + 1 == 2
@pytest.mark.web
def test_open_browser():
"""这是一个UI测试"""
assert True
2.1.3 给测试类添加 Mark
import pytest
@pytest.mark.api
class TestUserAPI:
"""整个测试类都被标记为 api"""
def test_create_user(self):
assert True
def test_delete_user(self):
assert True
2.2 Mark 的位置
Mark 装饰器必须放在测试函数或测试类的正上方,不能有其他代码(除了注释)。
✅ 正确的位置
import pytest
# 这是注释,可以放在装饰器上方
@pytest.mark.api
def test_login():
assert True
@pytest.mark.web
class TestWebPage:
def test_open(self):
assert True
❌ 错误的位置
import pytest
def test_login():
@pytest.mark.api # ❌ 错误:装饰器不能在函数内部
assert True
@pytest.mark.api
# ❌ 错误:装饰器和函数之间不能有空行
def test_login():
assert True
3. 如何定义 Mark
在使用 Mark 之前,需要先定义它。定义 Mark 有几种方式:
3.1 方式一:在 pytest.ini 文件中定义(推荐)
这是最常用的方式,适合团队协作和统一管理。
3.1.1 创建 pytest.ini 文件
在项目根目录下创建 pytest.ini 文件:
项目根目录/
├── pytest.ini # 配置文件
├── test_login.py
└── test_user.py
3.1.2 基本格式
[pytest]
markers =
标记名1: 标记说明
标记名2: 标记说明
标记名3: 标记说明
3.1.3 完整示例
pytest.ini 文件内容:
[pytest]
markers =
api: 接口测试
web: UI测试
ut: 单元测试
login: 登录相关
pay: 支付相关
slow: 慢速测试(运行时间较长)
smoke: 冒烟测试(核心功能测试)
regression: 回归测试
3.1.4 详细说明
[pytest]:这是 pytest 配置文件的固定格式,必须要有markers =:这是定义标记的关键字,后面可以定义多个标记标记名: 标记说明:每个标记的格式,冒号前面是标记名,后面是说明(可选)
3.1.5 多行定义
如果标记很多,可以分行写:
[pytest]
markers =
api: 接口测试
web: UI测试
ut: 单元测试
login: 登录相关
pay: 支付相关
slow: 慢速测试
smoke: 冒烟测试
regression: 回归测试
positive: 正向测试用例
negative: 负向测试用例
critical: 关键功能测试
normal: 普通功能测试
3.2 方式二:在 conftest.py 文件中定义
如果你不想创建 pytest.ini 文件,也可以在 conftest.py 中使用 pytest_configure 钩子函数来注册标记。
3.2.1 基本格式
def pytest_configure(config):
config.addinivalue_line("markers", "标记名: 标记说明")
3.2.2 完整示例
conftest.py 文件内容:
def pytest_configure(config):
"""注册自定义标记"""
config.addinivalue_line("markers", "api: 接口测试")
config.addinivalue_line("markers", "web: UI测试")
config.addinivalue_line("markers", "ut: 单元测试")
config.addinivalue_line("markers", "login: 登录相关")
config.addinivalue_line("markers", "pay: 支付相关")
3.2.3 批量注册示例
def pytest_configure(config):
"""批量注册自定义标记"""
markers = [
"api: 接口测试",
"web: UI测试",
"ut: 单元测试",
"login: 登录相关",
"pay: 支付相关",
"slow: 慢速测试",
"smoke: 冒烟测试",
]
for marker in markers:
config.addinivalue_line("markers", marker)
3.3 方式三:不定义直接使用(不推荐)
理论上,你可以不定义标记就直接使用,但 pytest 会发出警告:
import pytest
@pytest.mark.undefined_marker # ⚠️ 会发出警告
def test_something():
assert True
运行时的警告信息:
PytestUnknownMarkWarning: Unknown pytest.mark.undefined_marker - is this a typo?
为什么会有警告?
- 防止拼写错误:如果你写错了标记名,pytest 会提醒你
- 保持规范:明确定义标记可以让团队统一管理
建议:始终在 pytest.ini 或 conftest.py 中定义标记。
3.4 查看已定义的 Mark
运行以下命令可以查看所有已定义的标记:
pytest --markers
输出示例:
@pytest.mark.api: 接口测试
@pytest.mark.web: UI测试
@pytest.mark.ut: 单元测试
@pytest.mark.login: 登录相关
@pytest.mark.pay: 支付相关
@pytest.mark.slow: 慢速测试
@pytest.mark.smoke: 冒烟测试
4. 如何使用 Mark
4.1 给单个测试函数添加 Mark
4.1.1 基本用法
import pytest
@pytest.mark.api
def test_user_login():
"""测试用户登录接口"""
assert True
4.1.2 多个标记
一个测试函数可以有多个标记:
import pytest
@pytest.mark.api
@pytest.mark.login
def test_user_login():
"""这是一个接口测试,也是登录相关的测试"""
assert True
注意:多个装饰器要分别写,不能合并成一个。
✅ 正确写法
@pytest.mark.api
@pytest.mark.login
def test_user_login():
assert True
❌ 错误写法
@pytest.mark.api.login # ❌ 错误:不能这样合并
def test_user_login():
assert True
4.2 给测试类添加 Mark
4.2.1 基本用法
import pytest
@pytest.mark.api
class TestUserAPI:
"""整个类都被标记为 api"""
def test_create_user(self):
assert True
def test_delete_user(self):
assert True
def test_update_user(self):
assert True
说明:给类添加标记后,类中的所有测试方法都会继承这个标记。
4.2.2 类和方法都有标记
import pytest
@pytest.mark.api
class TestUserAPI:
"""类标记为 api"""
@pytest.mark.login
def test_user_login(self):
"""方法标记为 login,同时继承类的 api 标记"""
assert True
@pytest.mark.pay
def test_user_pay(self):
"""方法标记为 pay,同时继承类的 api 标记"""
assert True
说明:测试方法会同时拥有类的标记和自身的标记。
4.3 Mark 的继承
4.3.1 类标记的继承
import pytest
@pytest.mark.api
class TestUserAPI:
"""类标记为 api"""
def test_create_user(self):
"""这个方法会自动继承 api 标记"""
assert True
@pytest.mark.web
class TestWebPage:
"""类标记为 web"""
def test_open_page(self):
"""这个方法会自动继承 web 标记"""
assert True
4.3.2 继承链示例
import pytest
@pytest.mark.api
class TestAPI:
"""父类标记为 api"""
def test_base(self):
"""继承 api 标记"""
assert True
@pytest.mark.login
class TestLogin(TestAPI):
"""子类标记为 login,同时继承父类的 api 标记"""
def test_login(self):
"""同时拥有 login 和 api 标记"""
assert True
5. 运行带 Mark 的测试
5.1 基本运行命令
使用 -m 参数来运行特定标记的测试:
pytest -m 标记名
5.2 运行单个标记的测试
5.2.1 示例代码
test_example.py:
import pytest
@pytest.mark.api
def test_api_login():
assert True
@pytest.mark.web
def test_web_open():
assert True
@pytest.mark.api
def test_api_logout():
assert True
5.2.2 运行命令
# 只运行标记为 api 的测试
pytest -m api
# 只运行标记为 web 的测试
pytest -m web
5.2.3 运行结果
运行 pytest -m api:
======================== test session starts ========================
platform win32 -- Python 3.9.0, pytest-7.0.0
collected 3 items / 1 deselected / 2 selected
test_example.py::test_api_login PASSED [ 50%]
test_example.py::test_api_logout PASSED [100%]
======================== 2 passed, 1 deselected in 0.05s ========================
说明:
collected 3 items:收集到 3 个测试用例1 deselected:1 个测试被取消选择(test_web_open)2 selected:2 个测试被选中运行(test_api_login 和 test_api_logout)
5.3 运行多个标记的测试(OR 逻辑)
使用 or 关键字来运行多个标记中的任意一个:
# 运行标记为 api 或 web 的测试
pytest -m "api or web"
注意:标记名要用引号括起来,整个表达式也要用引号。
5.3.1 示例代码
test_example.py:
import pytest
@pytest.mark.api
def test_api_login():
assert True
@pytest.mark.web
def test_web_open():
assert True
@pytest.mark.ut
def test_unit_test():
assert True
5.3.2 运行命令
# 运行 api 或 web 标记的测试
pytest -m "api or web"
5.3.3 运行结果
======================== test session starts ========================
collected 3 items / 1 deselected / 2 selected
test_example.py::test_api_login PASSED [ 50%]
test_example.py::test_web_open PASSED [100%]
======================== 2 passed, 1 deselected in 0.05s ========================
5.4 运行同时包含多个标记的测试(AND 逻辑)
使用 and 关键字来运行同时包含多个标记的测试:
# 运行同时标记为 api 和 login 的测试
pytest -m "api and login"
5.4.1 示例代码
test_example.py:
import pytest
@pytest.mark.api
@pytest.mark.login
def test_api_login():
"""同时有 api 和 login 标记"""
assert True
@pytest.mark.api
def test_api_logout():
"""只有 api 标记"""
assert True
@pytest.mark.login
def test_web_login():
"""只有 login 标记"""
assert True
5.4.2 运行命令
# 运行同时有 api 和 login 标记的测试
pytest -m "api and login"
5.4.3 运行结果
======================== test session starts ========================
collected 3 items / 2 deselected / 1 selected
test_example.py::test_api_login PASSED [100%]
======================== 1 passed, 2 deselected in 0.05s ========================
5.5 运行不包含某个标记的测试(NOT 逻辑)
使用 not 关键字来运行不包含某个标记的测试:
# 运行不包含 slow 标记的测试
pytest -m "not slow"
5.5.1 示例代码
test_example.py:
import pytest
@pytest.mark.slow
def test_slow_test():
"""慢速测试"""
import time
time.sleep(5)
assert True
@pytest.mark.api
def test_api_login():
"""快速测试"""
assert True
@pytest.mark.web
def test_web_open():
"""快速测试"""
assert True
5.5.2 运行命令
# 运行所有不包含 slow 标记的测试
pytest -m "not slow"
5.5.3 运行结果
======================== test session starts ========================
collected 3 items / 1 deselected / 2 selected
test_example.py::test_api_login PASSED [ 50%]
test_example.py::test_web_open PASSED [100%]
======================== 2 passed, 1 deselected in 0.05s ========================
5.6 组合使用逻辑运算符
可以组合使用 and、or、not 来构建复杂的筛选条件:
# 运行 (api 或 web) 且 不是 slow 的测试
pytest -m "(api or web) and not slow"
# 运行 (api 且 login) 或 (web 且 login) 的测试
pytest -m "(api and login) or (web and login)"
5.6.1 复杂示例
test_example.py:
import pytest
@pytest.mark.api
@pytest.mark.login
def test_api_login():
assert True
@pytest.mark.api
@pytest.mark.slow
def test_api_slow():
assert True
@pytest.mark.web
@pytest.mark.login
def test_web_login():
assert True
@pytest.mark.web
@pytest.mark.slow
def test_web_slow():
assert True
5.6.2 运行命令
# 运行 (api 或 web) 且 不是 slow 的测试
pytest -m "(api or web) and not slow"
5.6.3 运行结果
======================== test session starts ========================
collected 4 items / 2 deselected / 2 selected
test_example.py::test_api_login PASSED [ 50%]
test_example.py::test_web_login PASSED [100%]
======================== 2 passed, 2 deselected in 0.05s ========================
5.7 在 Windows 系统上的注意事项
在 Windows 的 PowerShell 中,需要使用双引号:
# PowerShell 中使用双引号
pytest -m "api or web"
在 Windows 的 CMD 中,需要使用单引号或双引号:
# CMD 中使用单引号或双引号都可以
pytest -m "api or web"
pytest -m 'api or web'
6. Pytest 内置的 Mark
Pytest 提供了一些内置的标记,可以直接使用,无需定义。
6.1 @pytest.mark.skip:跳过测试
6.1.1 基本用法
无条件跳过测试用例:
import pytest
@pytest.mark.skip
def test_old_feature():
"""这个功能已经废弃,跳过测试"""
assert False # 即使这里会失败,也不会运行
6.1.2 带原因的跳过
import pytest
@pytest.mark.skip(reason="功能已废弃,等待重构")
def test_old_feature():
assert False
6.1.3 运行结果
======================== test session starts ========================
collected 1 item
test_example.py::test_old_feature SKIPPED [100%]
======================== 1 skipped in 0.02s ========================
6.2 @pytest.mark.skipif:条件跳过
6.2.1 基本用法
根据条件决定是否跳过:
import pytest
import sys
# 如果 Python 版本小于 3.8,则跳过
@pytest.mark.skipif(sys.version_info < (3, 8), reason="需要 Python 3.8 或更高版本")
def test_new_feature():
assert True
6.2.2 多个条件
import pytest
import sys
import platform
# 如果 Python 版本小于 3.8 或 操作系统是 Windows,则跳过
@pytest.mark.skipif(
sys.version_info < (3, 8) or platform.system() == "Windows",
reason="需要 Python 3.8+ 且非 Windows 系统"
)
def test_linux_feature():
assert True
6.2.3 使用标记表达式
import pytest
# 定义一个标记来表示是否启用某个功能
ENABLE_NEW_FEATURE = False
@pytest.mark.skipif(not ENABLE_NEW_FEATURE, reason="新功能未启用")
def test_new_feature():
assert True
6.3 @pytest.mark.xfail:预期失败
6.3.1 基本用法
标记测试为预期失败(已知的 bug):
import pytest
@pytest.mark.xfail
def test_buggy_feature():
"""这个功能有已知的 bug,预期会失败"""
assert False # 这个测试会失败,但不会影响整体测试结果
6.3.2 带原因的预期失败
import pytest
@pytest.mark.xfail(reason="已知 bug:issue #123")
def test_buggy_feature():
assert False
6.3.3 运行结果
======================== test session starts ========================
collected 1 item
test_example.py::test_buggy_feature XFAIL [100%]
======================== 1 xfailed in 0.02s ========================
说明:
XFAIL:预期失败,测试确实失败了(符合预期)- 如果测试通过了,会显示
XPASS(意外通过)
6.3.4 条件预期失败
import pytest
import sys
@pytest.mark.xfail(sys.version_info < (3, 8), reason="Python 3.8 以下版本不支持")
def test_new_feature():
assert True
6.4 @pytest.mark.parametrize:参数化测试
6.4.1 基本用法
为测试提供多组参数:
import pytest
@pytest.mark.parametrize("username,password", [
("admin", "123456"),
("user1", "password1"),
("user2", "password2"),
])
def test_login(username, password):
"""这个测试会运行 3 次,每次使用不同的参数"""
print(f"测试登录:用户名={username}, 密码={password}")
assert username is not None
assert password is not None
6.4.2 运行结果
======================== test session starts ========================
collected 3 items
test_example.py::test_login[admin-123456] PASSED [ 33%]
test_example.py::test_login[user1-password1] PASSED [ 66%]
test_example.py::test_login[user2-password2] PASSED [100%]
======================== 3 passed in 0.05s ========================
6.4.3 多个参数
import pytest
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(2, 3, 5),
(3, 4, 7),
])
def test_add(a, b, expected):
"""测试加法"""
assert a + b == expected
6.5 @pytest.mark.usefixtures:使用 Fixture
6.5.1 基本用法
标记测试使用特定的 fixture:
import pytest
@pytest.fixture
def setup_database():
"""数据库初始化 fixture"""
print("初始化数据库")
yield
print("清理数据库")
@pytest.mark.usefixtures("setup_database")
def test_query_data():
"""这个测试会自动使用 setup_database fixture"""
assert True
6.5.2 多个 fixture
import pytest
@pytest.fixture
def setup_database():
print("初始化数据库")
yield
print("清理数据库")
@pytest.fixture
def setup_cache():
print("初始化缓存")
yield
print("清理缓存")
@pytest.mark.usefixtures("setup_database", "setup_cache")
def test_complex_test():
"""使用多个 fixture"""
assert True
7. 自定义 Mark 的常见场景
7.1 按测试类型分类
7.1.1 场景说明
将测试按照类型分类:接口测试、UI测试、单元测试等。
7.1.2 配置文件
pytest.ini:
[pytest]
markers =
api: 接口测试
web: UI测试
ut: 单元测试
7.1.3 测试代码
test_example.py:
import pytest
@pytest.mark.api
def test_user_login_api():
"""接口测试:用户登录"""
# 模拟接口调用
response = {"status": 200, "message": "登录成功"}
assert response["status"] == 200
@pytest.mark.web
def test_user_login_web():
"""UI测试:用户登录页面"""
# 模拟浏览器操作
assert True
@pytest.mark.ut
def test_calculate_sum():
"""单元测试:计算函数"""
def add(a, b):
return a + b
assert add(1, 2) == 3
7.1.4 运行命令
# 只运行接口测试
pytest -m api
# 只运行UI测试
pytest -m web
# 只运行单元测试
pytest -m ut
7.2 按功能模块分类
7.2.1 场景说明
将测试按照功能模块分类:登录、支付、订单等。
7.2.2 配置文件
pytest.ini:
[pytest]
markers =
login: 登录相关
pay: 支付相关
order: 订单相关
user: 用户相关
7.2.3 测试代码
test_login.py:
import pytest
@pytest.mark.login
def test_user_login():
"""测试用户登录"""
assert True
@pytest.mark.login
def test_user_logout():
"""测试用户登出"""
assert True
@pytest.mark.login
def test_forgot_password():
"""测试忘记密码"""
assert True
test_pay.py:
import pytest
@pytest.mark.pay
def test_create_payment():
"""测试创建支付"""
assert True
@pytest.mark.pay
def test_payment_callback():
"""测试支付回调"""
assert True
7.2.4 运行命令
# 只运行登录相关的测试
pytest -m login
# 只运行支付相关的测试
pytest -m pay
7.3 按测试速度分类
7.3.1 场景说明
将测试按照运行速度分类:快速测试、慢速测试。
7.3.2 配置文件
pytest.ini:
[pytest]
markers =
fast: 快速测试(运行时间 < 1秒)
slow: 慢速测试(运行时间 > 1秒)
7.3.3 测试代码
test_example.py:
import pytest
import time
@pytest.mark.fast
def test_quick_calculation():
"""快速测试:简单计算"""
assert 1 + 1 == 2
@pytest.mark.slow
def test_database_query():
"""慢速测试:数据库查询"""
time.sleep(2) # 模拟慢速操作
assert True
@pytest.mark.slow
def test_api_integration():
"""慢速测试:API集成测试"""
time.sleep(3) # 模拟API调用
assert True
7.3.4 运行命令
# 开发时只运行快速测试
pytest -m fast
# CI/CD 时运行所有测试
pytest
# 或者排除慢速测试
pytest -m "not slow"
7.4 按测试优先级分类
7.4.1 场景说明
将测试按照优先级分类:P0(关键)、P1(重要)、P2(一般)。
7.4.2 配置文件
pytest.ini:
[pytest]
markers =
p0: P0级别测试(关键功能,必须通过)
p1: P1级别测试(重要功能)
p2: P2级别测试(一般功能)
7.4.3 测试代码
test_example.py:
import pytest
@pytest.mark.p0
def test_user_login():
"""P0:用户登录是核心功能"""
assert True
@pytest.mark.p0
def test_create_order():
"""P0:创建订单是核心功能"""
assert True
@pytest.mark.p1
def test_user_profile():
"""P1:用户资料是重要功能"""
assert True
@pytest.mark.p2
def test_user_avatar():
"""P2:用户头像是一般功能"""
assert True
7.4.4 运行命令
# 只运行P0级别的测试(冒烟测试)
pytest -m p0
# 运行P0和P1级别的测试
pytest -m "p0 or p1"
# 运行所有非P2级别的测试
pytest -m "not p2"
7.5 按测试方向分类
7.5.1 场景说明
将测试按照测试方向分类:正向测试、负向测试。
7.5.2 配置文件
pytest.ini:
[pytest]
markers =
positive: 正向测试用例(正常流程)
negative: 负向测试用例(异常流程)
7.5.3 测试代码
test_login.py:
import pytest
@pytest.mark.positive
def test_login_with_correct_password():
"""正向测试:使用正确密码登录"""
assert True
@pytest.mark.positive
def test_login_with_remember_me():
"""正向测试:记住我功能"""
assert True
@pytest.mark.negative
def test_login_with_wrong_password():
"""负向测试:使用错误密码登录"""
assert False # 预期失败
@pytest.mark.negative
def test_login_with_empty_username():
"""负向测试:用户名为空"""
assert False # 预期失败
7.5.4 运行命令
# 只运行正向测试
pytest -m positive
# 只运行负向测试
pytest -m negative
7.6 组合使用多个标记
7.6.1 场景说明
一个测试用例可以同时有多个标记,实现更精细的分类。
7.6.2 配置文件
pytest.ini:
[pytest]
markers =
api: 接口测试
login: 登录相关
p0: P0级别测试
fast: 快速测试
7.6.3 测试代码
test_example.py:
import pytest
@pytest.mark.api
@pytest.mark.login
@pytest.mark.p0
@pytest.mark.fast
def test_user_login_api():
"""接口测试 + 登录相关 + P0级别 + 快速测试"""
assert True
@pytest.mark.api
@pytest.mark.pay
@pytest.mark.p0
@pytest.mark.slow
def test_payment_api():
"""接口测试 + 支付相关 + P0级别 + 慢速测试"""
assert True
7.6.4 运行命令
# 运行接口测试中的登录相关测试
pytest -m "api and login"
# 运行P0级别的快速测试
pytest -m "p0 and fast"
# 运行接口测试中的P0级别测试,且不是慢速测试
pytest -m "api and p0 and not slow"
8. 实际项目应用示例
8.1 完整的项目结构示例
假设你有一个完整的测试项目,结构如下:
项目根目录/
├── pytest.ini # pytest 配置文件
├── conftest.py # 公共 fixture
├── tests/ # 测试目录
│ ├── api/ # 接口测试
│ │ ├── test_login.py
│ │ ├── test_user.py
│ │ └── test_order.py
│ ├── web/ # UI测试
│ │ ├── test_homepage.py
│ │ └── test_checkout.py
│ └── unit/ # 单元测试
│ ├── test_utils.py
│ └── test_models.py
8.2 pytest.ini 配置
pytest.ini:
[pytest]
# 测试文件发现规则
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# 标记定义
markers =
# 测试类型
api: 接口测试
web: UI测试
ut: 单元测试
# 功能模块
login: 登录相关
user: 用户相关
order: 订单相关
pay: 支付相关
# 测试速度
fast: 快速测试(< 1秒)
slow: 慢速测试(> 1秒)
# 测试优先级
p0: P0级别(关键功能)
p1: P1级别(重要功能)
p2: P2级别(一般功能)
# 测试方向
positive: 正向测试
negative: 负向测试
# 其他
smoke: 冒烟测试
regression: 回归测试
8.3 测试文件示例
8.3.1 接口测试文件
tests/api/test_login.py:
import pytest
@pytest.mark.api
@pytest.mark.login
@pytest.mark.p0
@pytest.mark.fast
@pytest.mark.positive
def test_login_with_valid_credentials():
"""接口测试:使用有效凭证登录(P0,快速,正向)"""
# 模拟接口调用
response = {"status": 200, "token": "abc123"}
assert response["status"] == 200
assert "token" in response
@pytest.mark.api
@pytest.mark.login
@pytest.mark.p1
@pytest.mark.fast
@pytest.mark.negative
def test_login_with_invalid_password():
"""接口测试:使用无效密码登录(P1,快速,负向)"""
# 模拟接口调用
response = {"status": 401, "message": "密码错误"}
assert response["status"] == 401
@pytest.mark.api
@pytest.mark.login
@pytest.mark.p2
@pytest.mark.slow
@pytest.mark.positive
def test_login_with_remember_me():
"""接口测试:记住我功能(P2,慢速,正向)"""
import time
time.sleep(2) # 模拟慢速操作
assert True
8.3.2 UI测试文件
tests/web/test_homepage.py:
import pytest
@pytest.mark.web
@pytest.mark.p0
@pytest.mark.fast
@pytest.mark.positive
def test_homepage_loads():
"""UI测试:首页加载(P0,快速,正向)"""
# 模拟浏览器操作
assert True
@pytest.mark.web
@pytest.mark.p1
@pytest.mark.slow
@pytest.mark.positive
def test_homepage_slider():
"""UI测试:首页轮播图(P1,慢速,正向)"""
import time
time.sleep(3) # 模拟等待动画
assert True
8.3.3 单元测试文件
tests/unit/test_utils.py:
import pytest
@pytest.mark.ut
@pytest.mark.fast
def test_string_utils():
"""单元测试:字符串工具函数"""
def capitalize_first(s):
return s.capitalize() if s else ""
assert capitalize_first("hello") == "Hello"
assert capitalize_first("") == ""
@pytest.mark.ut
@pytest.mark.fast
def test_number_utils():
"""单元测试:数字工具函数"""
def add(a, b):
return a + b
assert add(1, 2) == 3
assert add(-1, 1) == 0
8.4 常用运行命令
8.4.1 开发阶段
# 只运行快速测试(开发时快速反馈)
pytest -m fast
# 只运行P0级别的快速测试(核心功能快速验证)
pytest -m "p0 and fast"
# 只运行接口测试中的快速测试
pytest -m "api and fast"
8.4.2 提交代码前
# 运行所有P0和P1级别的测试(重要功能验证)
pytest -m "p0 or p1"
# 运行所有接口测试
pytest -m api
# 运行所有正向测试
pytest -m positive
8.4.3 CI/CD 阶段
# 运行所有测试(完整测试)
pytest
# 运行冒烟测试(P0级别)
pytest -m p0
# 运行回归测试
pytest -m regression
8.4.4 特定功能测试
# 只测试登录相关功能
pytest -m login
# 只测试支付相关功能
pytest -m pay
# 测试登录相关的接口测试
pytest -m "api and login"
9. Mark 的高级用法
9.1 动态添加 Mark
9.1.1 使用 pytest_collection_modifyitems 钩子
可以在收集测试用例时动态添加标记:
conftest.py:
def pytest_collection_modifyitems(config, items):
"""动态添加标记"""
for item in items:
# 如果测试函数名包含 "slow",自动添加 slow 标记
if "slow" in item.name:
item.add_marker(pytest.mark.slow)
# 如果测试函数名包含 "api",自动添加 api 标记
if "api" in item.name:
item.add_marker(pytest.mark.api)
9.1.2 根据文件路径添加标记
def pytest_collection_modifyitems(config, items):
"""根据文件路径自动添加标记"""
for item in items:
# 如果测试文件在 api 目录下,添加 api 标记
if "api" in str(item.fspath):
item.add_marker(pytest.mark.api)
# 如果测试文件在 web 目录下,添加 web 标记
if "web" in str(item.fspath):
item.add_marker(pytest.mark.web)
9.2 Mark 与 Fixture 结合
9.2.1 根据 Mark 选择不同的 Fixture
import pytest
@pytest.fixture
def fast_database():
"""快速数据库(内存数据库)"""
return {"type": "memory"}
@pytest.fixture
def slow_database():
"""慢速数据库(真实数据库)"""
return {"type": "real"}
def pytest_collection_modifyitems(config, items):
"""根据标记选择不同的 fixture"""
for item in items:
if item.get_closest_marker("fast"):
# 快速测试使用内存数据库
item.fixturenames.append("fast_database")
else:
# 其他测试使用真实数据库
item.fixturenames.append("slow_database")
9.3 Mark 与参数化结合
9.3.1 为不同的参数组合添加不同的标记
import pytest
@pytest.mark.parametrize("username,password", [
pytest.param("admin", "123456", marks=pytest.mark.p0),
pytest.param("user1", "pass1", marks=pytest.mark.p1),
pytest.param("user2", "pass2", marks=pytest.mark.p2),
])
def test_login(username, password):
"""不同用户有不同的优先级标记"""
assert username is not None
assert password is not None
9.4 自定义 Mark 验证
9.4.1 验证标记的正确使用
# conftest.py
def pytest_collection_modifyitems(config, items):
"""验证标记使用"""
for item in items:
# 检查是否所有接口测试都有 api 标记
if "api" in str(item.fspath) and not item.get_closest_marker("api"):
pytest.fail(f"测试 {item.name} 在 api 目录下但没有 api 标记")
10. 常见问题和注意事项
10.1 标记未定义的警告
10.1.1 问题描述
如果使用未定义的标记,pytest 会发出警告:
PytestUnknownMarkWarning: Unknown pytest.mark.undefined_marker
10.1.2 解决方法
在 pytest.ini 或 conftest.py 中定义标记。
10.2 标记拼写错误
10.2.1 问题描述
标记名拼写错误会导致测试无法被正确筛选。
10.2.2 示例
# 错误:标记名拼写错误
@pytest.mark.apii # 应该是 api
# 正确
@pytest.mark.api
10.2.3 解决方法
- 使用 IDE 的自动补全功能
- 运行
pytest --markers查看所有已定义的标记 - 使用
pytest -m 标记名 --collect-only检查是否能正确筛选
10.3 标记表达式语法错误
10.3.1 问题描述
在使用逻辑运算符时,语法错误会导致筛选失败。
10.3.2 常见错误
# ❌ 错误:缺少引号
pytest -m api or web
# ✅ 正确:使用引号
pytest -m "api or web"
10.3.3 解决方法
- 在 Windows PowerShell 中使用双引号
- 在 Linux/Mac 中使用单引号或双引号
- 复杂表达式使用括号:
pytest -m "(api or web) and not slow"
10.4 标记继承问题
10.4.1 问题描述
类的标记会继承给所有方法,但有时可能不符合预期。
10.4.2 示例
@pytest.mark.api
class TestAPI:
def test_method1(self):
"""这个方法有 api 标记"""
pass
@pytest.mark.web
def test_method2(self):
"""这个方法同时有 api 和 web 标记"""
pass
10.4.3 解决方法
- 明确了解标记的继承规则
- 如果不需要继承,不要给类添加标记,只给方法添加
10.5 标记过多导致混乱
10.5.1 问题描述
给一个测试添加太多标记会导致管理困难。
10.5.2 示例
# ❌ 不推荐:标记太多
@pytest.mark.api
@pytest.mark.login
@pytest.mark.p0
@pytest.mark.fast
@pytest.mark.positive
@pytest.mark.smoke
def test_login():
pass
10.5.3 解决方法
- 只添加必要的标记
- 使用组合标记(如
smoke可以包含p0和fast的含义) - 建立标记使用规范
10.6 标记与测试发现
10.6.1 问题描述
标记不会影响测试发现,只会影响测试筛选。
10.6.2 说明
# 这两个命令都会发现所有测试
pytest --collect-only
pytest -m "api" --collect-only
# 区别在于第二个命令只会显示标记为 api 的测试
10.7 标记的性能影响
10.7.1 问题描述
标记本身对性能影响很小,但使用 -m 参数筛选测试会影响收集阶段。
10.7.2 说明
- 标记只是元数据,不会影响测试执行速度
- 使用
-m筛选可以减少执行的测试数量,从而提高整体速度
11. 最佳实践
11.1 标记命名规范
11.1.1 使用小写字母
# ✅ 推荐
@pytest.mark.api
@pytest.mark.login
# ❌ 不推荐
@pytest.mark.API
@pytest.mark.Login
11.1.2 使用有意义的名称
# ✅ 推荐:名称清晰
@pytest.mark.api
@pytest.mark.user_login
# ❌ 不推荐:名称不清晰
@pytest.mark.a
@pytest.mark.test1
11.1.3 使用下划线分隔单词
# ✅ 推荐
@pytest.mark.user_login
@pytest.mark.payment_callback
# ❌ 不推荐
@pytest.mark.userlogin
@pytest.mark.payment-callback
11.2 标记分类原则
11.2.1 按维度分类
建议按照不同的维度来分类标记:
- 测试类型:api、web、ut
- 功能模块:login、pay、order
- 测试速度:fast、slow
- 测试优先级:p0、p1、p2
- 测试方向:positive、negative
11.2.2 避免重复
# ❌ 不推荐:标记含义重复
@pytest.mark.api
@pytest.mark.api_test # 与 api 重复
# ✅ 推荐:使用一个标记
@pytest.mark.api
11.3 标记使用原则
11.3.1 只添加必要的标记
# ❌ 不推荐:标记过多
@pytest.mark.api
@pytest.mark.login
@pytest.mark.p0
@pytest.mark.fast
@pytest.mark.positive
@pytest.mark.smoke
@pytest.mark.regression
def test_login():
pass
# ✅ 推荐:只添加必要的标记
@pytest.mark.api
@pytest.mark.login
@pytest.mark.p0
def test_login():
pass
11.3.2 保持一致性
团队内部应该统一标记的使用方式:
# 所有登录相关的接口测试都使用这些标记
@pytest.mark.api
@pytest.mark.login
def test_login():
pass
@pytest.mark.api
@pytest.mark.login
def test_logout():
pass
11.4 配置文件管理
11.4.1 集中管理标记定义
所有标记定义应该集中在 pytest.ini 文件中:
[pytest]
markers =
# 测试类型
api: 接口测试
web: UI测试
ut: 单元测试
# 功能模块
login: 登录相关
pay: 支付相关
11.4.2 添加详细说明
为每个标记添加清晰的说明:
# ✅ 推荐:说明清晰
markers =
api: 接口测试(测试HTTP API接口)
slow: 慢速测试(运行时间超过1秒的测试)
# ❌ 不推荐:说明不清晰
markers =
api: api
slow: slow
11.5 文档和培训
11.5.1 建立标记使用文档
为团队建立标记使用文档,说明:
- 每个标记的含义
- 何时使用哪个标记
- 标记的组合规则
11.5.2 定期审查标记使用
定期检查标记的使用情况,确保:
- 标记使用一致
- 没有冗余标记
- 标记定义合理
12. 总结
12.1 Mark 的核心概念
- Mark 是标签:给测试用例添加分类标签
- Mark 用于筛选:可以只运行特定标记的测试
- Mark 需要定义:在
pytest.ini或conftest.py中定义 - Mark 可以组合:一个测试可以有多个标记
12.2 Mark 的主要用途
- 分类管理:按类型、模块、优先级等分类
- 选择性运行:只运行需要的测试
- 条件执行:根据条件跳过或执行测试
- 提高效率:快速运行特定类型的测试
12.3 Mark 的使用流程
- 定义标记:在
pytest.ini中定义标记 - 添加标记:使用
@pytest.mark.标记名装饰器 - 运行测试:使用
pytest -m 标记名运行
12.4 常用命令
# 查看所有标记
pytest --markers
# 运行单个标记
pytest -m api
# 运行多个标记(OR)
pytest -m "api or web"
# 运行同时包含多个标记(AND)
pytest -m "api and login"
# 运行不包含某个标记(NOT)
pytest -m "not slow"
# 组合使用
pytest -m "(api or web) and not slow"
附录:完整示例
完整的 pytest.ini 配置示例
[pytest]
# 测试文件发现规则
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# 测试路径
testpaths = tests
# 标记定义
markers =
# ========== 测试类型 ==========
api: 接口测试(测试HTTP API接口)
web: UI测试(测试Web页面功能)
ut: 单元测试(测试单个函数或方法)
# ========== 功能模块 ==========
login: 登录相关功能测试
user: 用户相关功能测试
order: 订单相关功能测试
pay: 支付相关功能测试
product: 商品相关功能测试
# ========== 测试速度 ==========
fast: 快速测试(运行时间 < 1秒)
slow: 慢速测试(运行时间 > 1秒)
# ========== 测试优先级 ==========
p0: P0级别测试(关键功能,必须通过)
p1: P1级别测试(重要功能)
p2: P2级别测试(一般功能)
# ========== 测试方向 ==========
positive: 正向测试用例(正常流程)
negative: 负向测试用例(异常流程)
# ========== 其他 ==========
smoke: 冒烟测试(核心功能快速验证)
regression: 回归测试(完整功能验证)
integration: 集成测试(多个模块组合测试)