04.pytest中的mark

Pytest 中的 Mark 详解

1. 什么是 Mark

1.1 基本概念

Mark(标记) 是 pytest 提供的一个强大功能,允许你给测试用例添加”标签”或”标记”,用于对测试进行分类、筛选和管理。

1.2 形象比喻

想象一下,你有一个装满各种书籍的图书馆:

  • 有些书是”小说”
  • 有些书是”技术类”
  • 有些书是”适合儿童阅读”
  • 有些书是”需要特殊权限才能借阅”

Mark 就像是给测试用例贴上的”标签”,让你可以:

  • 快速找到特定类型的测试(比如只运行接口测试)
  • 跳过某些测试(比如跳过慢速测试)
  • 对测试进行分类管理(比如标记为”登录相关”、”支付相关”)

1.3 Mark 的作用

Mark 主要有以下几个作用:

  1. 分类管理:将测试用例按照功能、类型、优先级等进行分类
  2. 选择性运行:只运行特定标记的测试用例
  3. 条件执行:根据条件跳过或执行某些测试
  4. 参数化:为测试用例提供不同的参数组合
  5. 标记预期失败:标记已知的 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.iniconftest.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 组合使用逻辑运算符

可以组合使用 andornot 来构建复杂的筛选条件:

# 运行 (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.iniconftest.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 可以包含 p0fast 的含义)
  • 建立标记使用规范

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.iniconftest.py 中定义
  • Mark 可以组合:一个测试可以有多个标记

12.2 Mark 的主要用途

  1. 分类管理:按类型、模块、优先级等分类
  2. 选择性运行:只运行需要的测试
  3. 条件执行:根据条件跳过或执行测试
  4. 提高效率:快速运行特定类型的测试

12.3 Mark 的使用流程

  1. 定义标记:在 pytest.ini 中定义标记
  2. 添加标记:使用 @pytest.mark.标记名 装饰器
  3. 运行测试:使用 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: 集成测试(多个模块组合测试)

发表评论