Pytest 从入门到精通,一篇就够(超详细实战教程)

一、Pytest 核心介绍

1. 什么是 Pytest

Pytest 是 Python 生态中最主流的自动化测试框架 ,专为编写和运行单元测试、接口测试、UI 测试等各类测试用例设计,相比 Python 内置的 unittest

  • ✅ 语法更简洁:无需继承类 / 写模板代码,一行断言即可完成测试;
  • ✅ 功能更丰富:支持参数化、灵活的前后置操作、分类执行用例;
  • ✅ 扩展性更强:上千个第三方插件覆盖报告、重试、并行执行等场景;
  • ✅ 调试更友好:断言失败时输出详细对比信息,快速定位问题。

2. 适用场景

  • 单元测试:验证单个函数 / 类的逻辑(如计算函数、数据处理类);
  • 接口自动化:结合 requests 测试 HTTP/HTTPS 接口(如登录、下单接口);
  • UI 自动化:结合 selenium/appium 测试 Web/APP 页面;
  • 回归测试:批量执行核心用例,保障代码迭代后功能稳定。

3. 核心特点

极简的语法

无需继承类、写繁琐的模板代码,一行断言就能完成基础测试,新手上手成本极低:

python 复制代码
# 最简单的 Pytest 用例
def test_add():
    assert 1 + 2 == 3  # 断言成立则用例通过,不成立则失败

强大的自动发现机制

遵循简单的命名约定,Pytest 会自动扫描并识别测试用例,无需手动配置:

  • 测试文件:以 test_ 开头(如 test_demo.py)或 _test 结尾(如 demo_test.py
  • 测试类:以 Test 开头,且无 __init__ 方法(如 TestLogin
  • 测试函数 / 方法:以 test_ 开头(如 test_login_success()

丰富的内置功能

  • 灵活的断言 :支持自定义失败提示,调试更友好(如 assert a == b, "a和b不相等"
  • 参数化:批量生成测试用例,避免重复代码
  • Fixture 固件 :替代传统 setup/teardown,实现灵活的前置 / 后置操作(如初始化数据库、启动浏览器)
  • 标记功能:给用例分类(如冒烟用例、回归用例),按需执行
  • 失败重试 :结合插件(pytest-rerunfailures)实现失败用例自动重试
  • 测试报告 :结合插件(pytest-html)生成可视化 HTML 报告

高度可扩展

支持上千个第三方插件,覆盖各类测试场景:

  • 接口测试:pytest-requests
  • UI 测试:pytest-selenium
  • 并行执行:pytest-xdist(提升测试效率)
  • 用例排序:pytest-ordering

兼容原有测试代码

可直接运行 unittest 编写的测试用例,无需修改代码,迁移成本低。

4. 核心概念

断言(Assert)

断言是判断用例是否通过的核心,Pytest 对 Python 原生 assert 做了增强,失败时会输出详细的对比信息:

python 复制代码
def test_user_info():
    user = {"name": "张三", "age": 20}
    # 带自定义提示的断言
    assert user["age"] == 18, f"预期年龄18,实际{user['age']}"

运行失败时会显示:AssertionError: 预期年龄18,实际20

Fixture(固件)

Fixture 是 Pytest 最核心的特性,用于实现测试用例的前置 / 后置操作,支持不同作用域(函数、类、模块、会话):

python 复制代码
import pytest

# 定义 Fixture(函数级,每个用例执行前/后运行)
@pytest.fixture
def init_data():
    print("\n前置操作:初始化测试数据")
    data = [1, 2, 3]
    yield data  # 返回数据给用例,yield 后是后置操作
    print("\n后置操作:清理测试数据")

# 使用 Fixture(直接传函数名)
def test_use_data(init_data):
    assert len(init_data) == 3

标记(Mark)

通过 @pytest.mark.xxx 给用例打标签,实现分类执行:

python 复制代码
import pytest

# 标记为冒烟用例
@pytest.mark.smoke
def test_login():
    assert True

# 标记为用户模块用例
@pytest.mark.user
def test_user_list():
    assert True

执行指定标记的用例:

bash 复制代码
pytest -m smoke  # 只执行冒烟用例

参数化(Parametrize)

批量生成测试用例,避免重复编写代码:

python 复制代码
import pytest

# 参数化:(参数名, 参数列表)
@pytest.mark.parametrize("a, b, expected", [(1,2,3), (4,5,9), (10,20,30)])
def test_add(a, b, expected):
    assert a + b == expected

上述代码会自动生成 3 个测试用例,分别验证不同的加法场景。

5. 相关单词

英文单词 / 短语 音标 中文含义 常用场景
pytest /ˈpaɪtest/ Python 测试框架 单元测试、接口自动化
test /test/ 测试 测试用例、测试函数
case /keɪs/ 用例 test case 测试用例
function /ˈfʌŋkʃn/ 函数 测试函数、函数级用例
class /klɑːs/ 测试类 TestClass
module /ˈmɒdjuːl/ 模块 测试文件、模块级 fixture
session /ˈseʃn/ 会话 一次完整测试运行周期
fixture /ˈfɪkstʃə®/ 夹具 前置 / 后置、数据准备
scope /skəʊp/ 作用域 fixture 作用域
setup /ˈsetʌp/ 初始化、前置 测试前准备环境
teardown /ˈtɪədaʊn/ 清理、后置 测试后释放资源
conftest /ˈkɒnftest/ 全局配置 conftest.py 共享 fixture
mark /mɑːk/ 标记 @pytest.mark 自定义标记
skip /skɪp/ 跳过 跳过当前用例
skipif /ˈskɪpɪf/ 条件跳过 满足条件则跳过
xfail /ˈeksfeɪl/ 预期失败 预期会失败的用例
parametrize /pəˈræmətrˌaɪz/ 参数化 多组数据驱动测试
assert /əˈsɜːt/ 断言 校验结果是否符合预期
equal /ˈiːkwəl/ 相等 assert a == b
raise /reɪz/ 抛出 断言抛出异常
exception /ɪkˈsepʃn/ 异常 pytest.raises 捕获异常
plugin /ˈplʌɡɪn/ 插件 pytest 扩展功能
hook /hʊk/ 钩子 自定义测试流程
discover /dɪˈskʌvə®/ 发现 自动识别测试用例
run /rʌn/ 运行 执行测试
pass /pɑːs/ 通过 用例执行成功
fail /feɪl/ 失败 用例执行失败
warning /ˈwɔːnɪŋ/ 警告 测试警告信息
report /rɪˈpɔːt/ 报告 生成测试报告
coverage /ˈkʌvərɪdʒ/ 覆盖率 代码测试覆盖率
allure /əˈlʊə®/ 测试报告框架 生成美观可视化报告
allure-pytest /əˈlʊə ˈpaɪtest/ allure 插件 pytest 集成 allure
feature /ˈfiːtʃə®/ 功能 @allure.feature 标记模块
story /ˈstɔːri/ 子功能 @allure.story 标记子模块
step /step/ 步骤 @allure.step 标记测试步骤
title /ˈtaɪtl/ 标题 @allure.title 自定义用例标题
severity /sɪˈverəti/ 优先级 标记用例严重等级
attachment /əˈtætʃmənt/ 附件 添加截图、日志、文件
link /lɪŋk/ 链接 关联需求 / 缺陷地址

二、Pytest 基础用法

1. 环境准备

1.1. 安装 Pytest

bash 复制代码
# 推荐安装稳定版本
pip install pytest==9.0.2

# 验证安装结果
pytest --version

执行结果

终端输出 pytest 9.0.2 即安装成功;若提示 pytest: 未找到命令,需检查 Python 环境变量(Windows 需勾选 "Add Python to PATH",Mac/Linux 可执行 python -m pytest --version)。

1.2. 核心命名约定(必遵守)

Pytest 会自动扫描符合以下规则的文件 / 类 / 函数,不遵守则收集不到用例

类型 规则 正确示例 错误示例
测试文件 test_ 开头 或 _test 结尾 test_calculate.py calculate_test.py(✅)、calculate.py(❌)
测试类 Test 开头 + 无 __init__ 方法 TestCalculate test_calculate(❌)、TestCalc()(含__init__,❌)
测试函数 / 方法 test_ 开头 test_add()test_sub() add_test()(❌)、TestAdd()(❌)

2. 快速入门

2.1. 编写完整用例文件

新建项目目录,创建 test_calculate.py,内容如下:

python 复制代码
"""测试计算器功能的基础用例"""

# 定义待测试的普通函数(模拟实际项目中的业务逻辑)
def add(a, b):
    """加法函数"""
    return a + b

def sub(a, b):
    """减法函数"""
    return a - b

# 测试函数:验证加法功能
def test_add():
    # 步骤1:准备测试数据
    num1 = 10
    num2 = 20
    expected = 30
    
    # 步骤2:执行待测试函数
    actual = add(num1, num2)
    
    # 步骤3:断言结果是否符合预期(核心)
    assert actual == expected, f"加法计算错误!预期{expected},实际{actual}"

# 测试类:组织多个相关测试方法
class TestCalculate:
    # 测试减法功能(正常场景)
    def test_sub_normal(self):
        actual = sub(50, 20)
        assert actual == 30, f"减法计算错误!预期30,实际{actual}"
    
    # 测试减法功能(边界场景:负数)
    def test_sub_negative(self):
        actual = sub(10, 30)
        assert actual == -20, f"负数减法错误!预期-20,实际{actual}"

2.2. 运行用例

bash 复制代码
# 方式1:运行当前目录所有用例(显示详细结果)
pytest -v

# 方式2:指定文件运行(精准执行,推荐)
pytest test_calculate.py -v -s

参数说明:-v 显示用例名称和结果,-s 显示用例中的 print 输出(若有)。

2.3. 执行结果解读

cmd 复制代码
collected 3 items
test_calculate.py::test_add PASSED
test_calculate.py::TestCalculate::test_sub_normal PASSED
test_calculate.py::TestCalculate::test_sub_negative PASSED

========================== 3 passed in 0.01s ==========================
  • collected 3 items:Pytest 识别到 3 个可用用例;
  • PASSED:用例执行通过;若断言失败,会显示 FAILED + 详细错误信息;
  • 0.01s:用例执行总耗时。

2.4. 模拟断言失败场景

修改 test_add 中的预期值:

python 复制代码
def test_add():
    num1 = 10
    num2 = 20
    expected = 31  # 故意写错,模拟失败
    actual = add(num1, num2)
    assert actual == expected, f"加法计算错误!预期{expected},实际{actual}"

重新运行 pytest test_calculate.py -v,结果如下:

cmd 复制代码
test_calculate.py::test_add FAILED
test_calculate.py::TestCalculate::test_sub_normal PASSED
test_calculate.py::TestCalculate::test_sub_negative PASSED

========================== FAILURES ==========================
___________________________ test_add ___________________________
    def test_add():
        num1 = 10
        num2 = 20
        expected = 31
        actual = add(num1, num2)
>       assert actual == expected, f"加法计算错误!预期{expected},实际{actual}"
E       AssertionError: 加法计算错误!预期31,实际30
E       assert 30 == 31

test_calculate.py:19: AssertionError
========================== 1 failed, 2 passed in 0.02s ==========================

关键信息:

  • AssertionError:断言失败类型;
  • assert 30 == 31:明确显示预期值和实际值的对比;
  • test_calculate.py:19:定位到失败代码的行号。

3. 核心语法:断言(Assert)

Pytest 增强了 Python 原生断言,支持自定义提示复杂场景判断,以下是实战中最常用的断言类型:

(1)基础断言(附详细示例)

python 复制代码
"""断言实战示例"""
import pytest

def test_assert_basic():
    # 1. 相等/不相等判断(最常用)
    a = "hello"
    b = "world"
    assert a != b, f"a和b不应相等!a={a}, b={b}"  # 成功
    
    # 2. 包含/不包含判断(字符串/列表/字典)
    str1 = "pytest教程"
    assert "pytest" in str1, f"str1不包含'pytest'!str1={str1}"  # 成功
    list1 = [1,2,3]
    assert 4 not in list1, f"list1不应包含4!list1={list1}"  # 成功
    
    # 3. 布尔判断(True/False)
    flag = True
    assert flag, "flag应为True!"  # 成功
    
    # 4. 数值大小判断
    num = 99
    assert num > 90, f"num应大于90!num={num}"  # 成功

def test_assert_dict():
    # 复杂断言:字典内容验证(接口测试常用)
    actual_response = {
        "code": 200,
        "msg": "登录成功",
        "data": {"user_id": 1001, "username": "zhangsan"}
    }
    # 验证状态码
    assert actual_response["code"] == 200, "接口返回码错误"
    # 验证data中的user_id
    assert actual_response["data"]["user_id"] == 1001, "用户ID错误"

运行 pytest test_assert.py -v,结果全部 PASSED;若修改 actual_response["code"] = 500,则会提示 AssertionError: 接口返回码错误

4. 命令行参数

4.1. 常用参数

参数 作用 实战示例 示例说明
-v 显示详细用例名称和结果 pytest -v 能看到每个用例的完整路径(如 test_assert.py::test_assert_basic
-s 显示用例中的 print / 日志输出 pytest -s 调试时查看变量值、执行步骤
-x 第一个用例失败则停止执行 pytest -x 快速定位首个失败用例,节省调试时间
-k 按关键字筛选用例 pytest -k "add or sub" 只执行名称含 addsub 的用例
--tb=short 简化失败时的报错信息(只显示关键行) pytest --tb=short 避免冗长报错日志,快速看失败原因
-m 按标记筛选用例(进阶) pytest -m smoke 只执行标记为 smoke 的冒烟用例

实战示例

运行 pytest -vs -k "add",结果如下:

cmd 复制代码
collected 3 items / 2 deselected / 1 selected
test_calculate.py::test_add PASSED

========================== 1 passed, 2 deselected in 0.01s ==========================
  • 1 selected:选中 1 个含 add 的用例;
  • 2 deselected:排除 2 个不含 add 的用例。

4.2. 全部参数

分类 参数名 中文说明
位置参数 file_or_dir 指定要运行的测试文件或目录(可传多个)
通用参数 -k EXPRESSION 仅运行匹配给定子字符串表达式的测试。表达式是可求值的 Python 表达式,所有名称会按子字符串匹配测试名及其父类(大小写不敏感)。示例:-k 'test_method or test_other' 匹配含该关键词的测试,-k 'not test_method' 排除匹配项
-m MARKEXPR 仅运行匹配给定标记表达式的测试。示例:-m 'mark1 and not mark2'
--markers 显示所有标记(内置、插件、项目自定义)
-x, --exitfirst 遇到第一个错误 / 失败用例时立即退出
--maxfail=num 累计失败 / 错误数达到 num 时退出
--strict-config 启用 strict_config 选项(解析配置文件时遇到警告直接报错)
--strict-markers 启用 strict_markers 选项(未注册的标记直接报错)
--strict 启用所有严格模式(包含 strict_config、strict_markers 等)
--fixtures, --funcargs 显示可用的夹具(按插件加载顺序排序,以_开头的夹具仅在 - v 时显示)
--fixtures-per-test 显示每个测试用例关联的夹具
--pdb 在测试错误 / 键盘中断时启动交互式 Python 调试器
--pdbcls=modulename:classname 指定自定义调试器(示例:--pdbcls=IPython.terminal.debugger:TerminalPdb)
--trace 运行每个测试用例时立即触发断点
--capture=method 每个测试的输出捕获方式:fd|sys|no|tee-sys
-s --capture=no 的简写(禁用输出捕获,显示 print 等输出)
--runxfail 将 xfail 标记的测试按普通测试执行(不标记为 xfail)
--lf, --last-failed 仅重跑上次运行失败的测试(无失败则运行全部)
--ff, --failed-first 运行所有测试,但先运行上次失败的测试(可能打乱夹具的 setup/teardown 顺序)
--nf, --new-first 先运行新文件中的测试,其余按文件修改时间排序
--cache-show=CACHESHOW 显示缓存内容,不执行收集 / 测试(默认 glob:*)
--cache-clear 测试开始时清空所有缓存内容
--lfnf, --last-failed-no-failures={all,none} 配合 --lf 使用:无历史失败时,all(运行全部,默认)/none(仅提示并退出)
--sw, --stepwise 测试失败时退出,下次运行从失败的用例继续
--sw-skip, --stepwise-skip 忽略第一个失败用例,在第二个失败时停止(自动启用 --stepwise)
--sw-reset, --stepwise-reset 重置 stepwise 状态,重新开始流程(自动启用 --stepwise)
报告参数 --durations=N 显示 N 个最慢的夹具 / 测试执行时长(N=0 显示全部)
--durations-min=N 纳入 "最慢列表" 的最小时长(秒),默认 0.005(-vv 时为 0.0)
-v, --verbose 提高输出详细程度(显示每个用例的执行结果)
--no-header 禁用输出头部信息
--no-summary 禁用输出总结信息
--no-fold-skipped 简短总结中不折叠跳过的测试
--force-short-summary 强制输出精简版总结(忽略详细程度级别)
-q, --quiet 降低输出详细程度(仅显示关键结果)
--verbosity=VERBOSE 设置详细程度,默认 0
-r chars 显示额外的测试总结信息,字符含义:f (失败)、E (错误)、s (跳过)、x (预期失败)、X (预期失败但通过)、p (通过)、P (通过且有输出)、a (除 p/P 外全部)、A (全部);w (警告,默认启用);N (重置列表);默认:fE
--disable-warnings, --disable-pytest-warnings 禁用警告总结输出
-l, --showlocals 在回溯信息中显示局部变量(默认禁用)
--no-showlocals 隐藏回溯中的局部变量(抵消 addopts 中的 --showlocals)
--tb=style 回溯信息打印模式:auto|long|short|line|native|no
--xfail-tb 为 xfail 用例显示回溯(只要 --tb≠no)
--show-capture={no,stdout,stderr,log,all} 控制失败测试中捕获的输出显示方式,默认 all
--full-trace 不截断任何回溯信息(默认会截断)
--color=color 终端输出颜色:yes|no|auto
--code-highlight={yes,no} 是否高亮代码(仅在 --color 启用时生效),默认 yes
--pastebin=mode 将失败 / 全部测试信息发送到bpaste.net粘贴板(mode:failed|all)
--junitxml, --junit-xml=path 在指定路径生成 JUnit XML 格式的报告
--junitprefix, --junit-prefix=str 为 JUnit XML 输出中的类名添加前缀
--html=path 在指定路径生成 HTML 测试报告
--self-contained-html 生成包含所有样式 / 脚本 / 图片的独立 HTML 报告(CSP 限制环境可能无法渲染)
--css=path 将指定 CSS 文件内容追加到报告样式中
Python 警告参数 -W, --pythonwarnings PYTHONWARNINGS 设置要报告的警告类型(同 Python 的 - W 参数)
用例收集参数 --collect-only, --co 仅收集测试用例,不执行
--pyargs 尝试将所有参数解析为 Python 包
--ignore=path 收集时忽略指定路径(可多次使用)
--ignore-glob=path 收集时忽略匹配通配符的路径(可多次使用)
--deselect=nodeid_prefix 收集时取消选中指定节点 ID 前缀的用例(可多次使用)
--confcutdir=dir 仅加载指定目录下的 conftest.py
--noconftest 不加载任何 conftest.py 文件
--keep-duplicates 保留重复的测试用例
--collect-in-virtualenv 不忽略本地虚拟环境目录中的测试
--continue-on-collection-errors 即使收集阶段出错,仍强制执行测试
--import-mode={prepend,append,importlib} 导入测试模块 /conftest 时修改 sys.path 的方式:prepend(前置,默认)|append(后置)|importlib
--doctest-modules 运行所有.py 模块中的 doctest
--doctest-report={none,cdiff,ndiff,udiff,only_first_failure} doctest 失败时的 diff 输出格式
--doctest-glob=pat doctest 文件匹配通配符,默认 test*.txt
--doctest-ignore-import-errors 忽略 doctest 收集阶段的导入错误
--doctest-continue-on-failure 单个 doctest 中第一个失败后继续执行
调试 & 配置参数 -c, --config-file FILE 从指定文件加载配置(不使用默认配置文件)
--rootdir=ROOTDIR 定义测试根目录(支持相对路径、绝对路径、环境变量)
--basetemp=dir 测试运行的基础临时目录(警告:目录存在会被删除)
-V, --version 显示 pytest 版本和插件信息(两次 - V 显示更多插件详情)
-h, --help 显示帮助信息和配置说明
-p name 提前加载指定插件(可多次使用);前缀 no: 表示禁用插件(示例:-p no:doctest)
--disable-plugin-autoload 禁用插件自动加载(仅加载 - p 或 PYTEST_PLUGINS 指定的插件)
--trace-config 追踪 conftest.py 文件的加载过程
--debug=DEBUG_FILE_NAME 将内部调试信息存储到日志文件(默认 pytestdebug.log,覆盖写入)
-o, --override-ini OVERRIDE_INI 覆盖配置选项(格式:option=value,示例:-o strict_xfail=True -o cache_dir=cache)
--assert=MODE 控制断言调试工具:plain(无调试)、rewrite(默认,重写 assert 语句)
--setup-only 仅初始化夹具,不执行测试用例
--setup-show 执行测试时显示夹具的初始化过程
--setup-plan 显示要执行的夹具和测试,但不实际执行
日志参数 --log-level=LEVEL 捕获 / 显示的日志级别(默认继承根处理器的 WARNING)
--log-format=LOG_FORMAT 日志模块使用的格式
--log-date-format=LOG_DATE_FORMAT 日志模块使用的日期格式
--log-cli-level=LOG_CLI_LEVEL 终端日志显示级别
--log-cli-format=LOG_CLI_FORMAT 终端日志使用的格式
--log-cli-date-format=LOG_CLI_DATE_FORMAT 终端日志使用的日期格式
--log-file=LOG_FILE 日志写入的文件路径
--log-file-mode={w,a} 日志文件打开模式(w:覆盖,a:追加)
--log-file-level=LOG_FILE_LEVEL 日志文件的日志级别
--log-file-format=LOG_FILE_FORMAT 日志文件使用的格式
--log-file-date-format=LOG_FILE_DATE_FORMAT 日志文件使用的日期格式
--log-auto-indent=LOG_AUTO_INDENT 自动缩进多行日志消息(值:true|on/false|off / 整数)
--log-disable=LOGGER_DISABLE 禁用指定名称的日志器(可多次使用)
异步 IO 参数 --asyncio-mode=MODE asyncio 模式:auto(自动处理所有异步函数,默认)、strict(禁用自动处理)
--asyncio-debug 为默认事件循环启用 asyncio 调试模式
覆盖率参数 --cov=SOURCE 指定要测量覆盖率的路径 / 包名(可多次使用);--cov = 表示不过滤,记录所有代码
--cov-reset 重置已累积的 cov 源配置
--cov-report=TYPE 生成的覆盖率报告类型:term|term-missing|annotate|html|xml|json|markdown 等(可指定输出路径,示例:html:./cov_html)
--cov-config=PATH 覆盖率配置文件路径,默认.coveragerc
--no-cov-on-fail 测试失败时不生成覆盖率报告,默认 False
--no-cov 完全禁用覆盖率报告(调试时用),默认 False
--cov-fail-under=MIN 总覆盖率低于 MIN 时测试失败
--cov-append 追加覆盖率数据(不删除原有数据),默认 False
--cov-branch 启用分支覆盖率统计
--cov-precision=COV_PRECISION 覆盖报告的精度(小数位数)
--cov-context=CONTEXT 使用的动态上下文,目前仅支持 "test"
元数据参数 --metadata=key value 添加额外元数据(键值对)
--metadata-from-json=METADATA_FROM_JSON 从 JSON 字符串添加额外元数据
--metadata-from-json-file=METADATA_FROM_JSON_FILE 从 JSON 文件添加额外元数据
失败重跑参数 --force-reruns=FORCE_RERUNS 强制所有测试重跑指定次数(忽略标记)
--only-rerun=ONLY_RERUN 仅重跑匹配正则的错误(可多次使用)
--reruns=RERUNS 失败测试的重跑次数,默认 0
--reruns-delay=RERUNS_DELAY 重跑间隔时间(秒)
--rerun-except=RERUN_EXCEPT 不重跑匹配正则的错误(可多次使用)
--fail-on-flaky 若不稳定测试重跑后通过,测试整体以退出码 7 失败
分布式测试参数 -n, --numprocesses numprocesses --dist=load --tx=NUM*popen 的简写;值为 logical/auto 时自动检测 CPU 数;与 --pdb 同用时强制为 0(禁用)
--maxprocesses=maxprocesses 限制 --numprocesses=auto/logical 时的最大工作进程数
--max-worker-restart=MAXWORKERRESTART 崩溃后可重启的最大工作进程数(0 禁用)
--dist=distmode 测试分发模式:each(所有环境运行每个测试)、load(负载均衡)、loadscope(按作用域分组)、loadfile(按文件分组)、loadgroup(按 xdist_group 标记分组)、worksteal(工作窃取)、no(默认,进程内运行)
--loadscope-reorder loadscope 模式下按作用域用例数重排测试(提升并行度,可能打乱顺序)
--no-loadscope-reorder 禁用 loadscope 重排(保留测试顺序)
--tx=xspec 添加测试执行环境(示例:--tx popen//python=python2.5、--tx ssh=user@xxx//chdir=testcache)
--px=xspec 添加代理网关(示例:--px id=proxy//socket=ip:port --tx 5*popen//via=proxy)
-d --dist=load 的简写(负载均衡分发测试)
--rsyncdir=DIR 添加要同步到远程 tx 节点的目录
--rsyncignore=GLOB 添加 rsync 同步时忽略的通配符规则
--testrunuid=TESTRUNUID 为所有工作进程设置统一的 testrun_uid 夹具值(默认自动生成唯一值)
--maxschedchunk=MAXSCHEDCHUNK load 模式下单次调度的最大测试数(1:逐个调度,适合慢测试;默认不限)
-f, --looponfail 子进程中运行测试:监听文件修改,重跑失败用例直到全部通过

补充说明

  1. 表格中 "可多次使用" 表示参数可重复传入(如--ignore=path1 --ignore=path2);
  2. 部分参数依赖第三方插件(如--html依赖 pytest-html,--reruns依赖 pytest-rerunfailures,-n依赖 pytest-xdist);
  3. 配置文件参数(如 markers、addopts 等)是 pytest.ini/pyproject.toml 等配置文件中的选项,非命令行参数,未列入表格;
  4. 环境变量(如 PYTEST_ADDOPTS、CI 等)是系统级配置,未列入表格。

5. 全局配置文件:pytest.ini

5.1. 常用参数

放在项目根目录,替代重复的命令行参数,优先于命令行生效,示例如下:

ini 复制代码
[pytest]
; 全局默认参数(每次运行pytest自动带上-vs)
addopts = -vs
; 测试用例存放目录(只扫描tests文件夹)
testpaths = ./tests
; 测试文件命名规则(必须匹配实际文件名)
python_files = test_*.py
; 测试类命名规则(必须匹配实际类名)
python_classes = Test*
; 测试函数命名规则(必须匹配实际函数名)
python_functions = test_*
; 注册自定义标记(进阶用,避免警告)
markers =
    smoke: 冒烟用例(核心流程)
    normal: 普通用例
    exception: 异常场景用例

生效验证

配置后直接运行 pytest,无需加 -vs,终端会自动显示详细结果和 print 输出;若 tests 目录下有 test_demo.py,会自动扫描,无需指定文件名。

5.2. 全部参数

以下是 pytest 配置文件(pytest.ini/pyproject.toml/tox.ini/setup.cfg 等)专属参数:

配置项名称 类型 中文说明
markers 行列表(list) 为测试函数注册自定义标记(示例:markers = smoke: 冒烟用例 \nnormal: 普通用例)
empty_parameter_set_mark 字符串(str) 空参数集的默认标记
strict_config 布尔(bool) 解析配置文件的 pytest 段时,遇到警告直接抛出错误(严格模式)
strict_markers 布尔(bool) 未在 markers 中注册的标记会直接报错(严格校验标记)
strict 布尔(bool) 启用所有严格模式(包含 strict_config、strict_markers、strict_xfail、strict_parametrization_ids)
filterwarnings 行列表(list) 每行指定一个 warnings.filterwarnings 的过滤规则,在 - W/--pythonwarnings 之后处理
norecursedirs 参数列表(args) 递归查找测试时需要忽略的目录模式(示例:norecursedirs = .git venv pycache
testpaths 参数列表(args) 未指定文件 / 目录时,用于搜索测试的目录(示例:testpaths = tests/unit tests/integration)
collect_imported_tests 布尔(bool) 是否收集 testpaths 外导入模块中的测试用例
consider_namespace_packages 布尔(bool) 导入模块时考虑命名空间包
usefixtures 参数列表(args) 项目默认使用的夹具列表(所有测试自动应用这些夹具)
python_files 参数列表(args) Python 测试模块发现的通配符模式(示例:python_files = test_*.py *_test.py)
python_classes 参数列表(args) Python 测试类发现的前缀 / 通配符(示例:python_classes = Test*)
python_functions 参数列表(args) Python 测试函数 / 方法发现的前缀 / 通配符(示例:python_functions = test_*)
disable_test_id_escaping_and_forfeit_all_rights_to_community_support 布尔(bool) 禁用非 ASCII 字符的字符串转义(可能导致副作用,风险自负)
strict_parametrization_ids 布尔(bool) 检测到非唯一的参数集 ID 时抛出错误
console_output_style 字符串(str) 控制台输出样式:classic(经典)、progress(百分比进度)、count(计数)、progress-even-when-capture-no(强制显示进度)
verbosity_test_cases 字符串(str) 测试用例执行的详细程度(覆盖主 verbosity 级别,值越高输出越详细)
strict_xfail 布尔(bool) xfail 标记未显式指定 strict 参数时的默认值(默认 False,别名:xfail_strict)
tmp_path_retention_count 字符串(str) 按 tmp_path_retention_policy 保留的 tmp_path 目录数量
tmp_path_retention_policy 字符串(str) 控制 tmp_path 夹具创建的目录保留策略:all(全部)、failed(仅失败用例)、none(不保留)
enable_assertion_pass_hook 布尔(bool) 启用 pytest_assertion_pass 钩子(需删除已生成的 pyc 缓存文件)
truncation_limit_lines 字符串(str) 触发回溯信息截断的行数阈值
truncation_limit_chars 字符串(str) 触发回溯信息截断的字符数阈值
verbosity_assertions 字符串(str) 断言的详细程度(覆盖主 verbosity 级别,值越高断言失败时解释越详细)
junit_suite_name 字符串(str) JUnit 报告的测试套件名称
junit_logging 字符串(str) 将捕获的日志写入 JUnit 报告:no|log|system-out|system-err|out-err|all
junit_log_passing_tests 布尔(bool) 为通过的测试捕获日志并写入 JUnit 报告
junit_duration_report 字符串(str) 报告的时长类型:total(总时长)、call(调用时长)
junit_family 字符串(str) 生成的 XML 遵循的 schema:legacy|xunit1|xunit2
doctest_optionflags 参数列表(args) doctest 的选项标志
doctest_encoding 字符串(str) doctest 文件使用的编码
cache_dir 字符串(str) 缓存目录路径
log_level 字符串(str) --log-level 的默认值
log_format 字符串(str) --log-format 的默认值
log_date_format 字符串(str) --log-date-format 的默认值
log_cli 布尔(bool) 启用测试运行时的实时日志显示(live logging)
log_cli_level 字符串(str) --log-cli-level 的默认值
log_cli_format 字符串(str) --log-cli-format 的默认值
log_cli_date_format 字符串(str) --log-cli-date-format 的默认值
log_file 字符串(str) --log-file 的默认值
log_file_mode 字符串(str) --log-file-mode 的默认值
log_file_level 字符串(str) --log-file-level 的默认值
log_file_format 字符串(str) --log-file-format 的默认值
log_file_date_format 字符串(str) --log-file-date-format 的默认值
log_auto_indent 字符串(str) --log-auto-indent 的默认值
faulthandler_timeout 字符串(str) 测试超时(秒)时转储所有线程的回溯信息
faulthandler_exit_on_timeout 布尔(bool) 测试超时时退出测试进程
verbosity_subtests 字符串(str) 子测试的详细程度(值越高,通过的子测试也会输出信息;失败的子测试始终报告)
addopts 参数列表(args) 额外的命令行参数(运行 pytest 时自动追加,示例:addopts = -vs --html=report.html)
minversion 字符串(str) 项目要求的最低 pytest 版本(示例:minversion = 7.0)
pythonpath 路径列表(paths) 添加到 sys.path 的路径(多个路径用逗号 / 换行分隔)
required_plugins 参数列表(args) pytest 运行必须存在的插件(缺失则报错,示例:required_plugins = pytest-html>=3.0 pytest-rerunfailures)
anyio_mode 字符串(str) AnyIO 插件模式:strict|auto
asyncio_mode 字符串(str) --asyncio-mode 的默认值
asyncio_debug 布尔(bool) 为默认事件循环启用 asyncio 调试模式(默认值)
asyncio_default_fixture_loop_scope 字符串(str) 执行异步夹具的 asyncio 事件循环默认作用域
asyncio_default_test_loop_scope 字符串(str) 执行异步测试的 asyncio 事件循环默认作用域
render_collapsed 字符串(str) 打开报告时默认折叠的行
max_asset_filename_length 字符串(str) HTML 报告中附加文件的最大文件名长度
environment_table_redact_list 行列表(list) 环境表中需要脱敏的变量正则列表(匹配的变量值会被隐藏)
initial_sort 字符串(str) 报告默认排序的列
generate_report_on_test 布尔(bool) 每个测试执行后立即生成 HTML 报告(而非全部测试结束后)
reruns 字符串(str) --reruns 的默认值(失败测试重跑次数)
reruns_delay 字符串(str) --reruns-delay 的默认值(重跑间隔秒数)
rsyncdirs 路径列表(paths) 远程分布式测试时需要 rsync 同步的相对路径列表
rsyncignore 路径列表(paths) rsync 同步时忽略的相对路径通配符列表
looponfailroots 路径列表(paths) --looponfail 模式下监听文件修改的目录,默认当前目录

补充说明

  1. 配置项类型说明:
    • 行列表(linelist):配置文件中每行一个值(如 markers);
    • 参数列表(args):用空格 / 逗号分隔多个值;
    • 路径列表(paths):文件 / 目录路径列表,支持相对 / 绝对路径;
  2. 配置文件优先级:pytest.toml > pytest.ini > tox.ini > setup.cfg > pyproject.toml;
  3. 布尔类型值在 ini 文件中用true/false1/0表示,在 toml 文件中用true/false
  4. 字符串类型若包含空格,需用引号包裹(如addopts = "-vs --html=report.html")。

三、Pytest 进阶特性

1. 自定义标记(Mark):分类执行用例

1.1. 核心场景

实际项目中,测试用例分「冒烟用例」「回归用例」「异常用例」,需按需执行(如上线前只跑冒烟用例)。

1.2. 完整实现步骤

步骤 1:注册标记(在 pytest.ini 中)
ini 复制代码
[pytest]
addopts = -vs
testpaths = ./tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# 注册标记(必须!否则会报未知标记警告)
markers =
    smoke: 冒烟用例(核心流程,必跑)
    normal: 普通用例(功能覆盖)
    exception: 异常场景用例(边界/错误输入)
步骤 2:给用例打标记

tests/test_login.py 中编写带标记的用例:

python 复制代码
"""登录模块测试用例(带标记)"""
import pytest

# 模拟登录函数
def login(username, password):
    if username == "admin" and password == "123456":
        return {"code": 200, "msg": "登录成功"}
    elif username == "":
        return {"code": 400, "msg": "用户名不能为空"}
    else:
        return {"code": 401, "msg": "用户名或密码错误"}

# 冒烟用例:正确账号密码登录(核心流程)
@pytest.mark.smoke
def test_login_success():
    result = login("admin", "123456")
    assert result["code"] == 200, "正确账号登录失败"
    assert result["msg"] == "登录成功", "登录提示语错误"

# 普通用例:错误密码登录
@pytest.mark.normal
def test_login_wrong_pwd():
    result = login("admin", "654321")
    assert result["code"] == 401, "错误密码登录状态码错误"

# 异常用例:空用户名登录
@pytest.mark.exception
def test_login_empty_username():
    result = login("", "123456")
    assert result["code"] == 400, "空用户名登录状态码错误"

# 多标记:同时属于冒烟和普通用例
@pytest.mark.smoke
@pytest.mark.normal
def test_login_remember_pwd():
    # 模拟记住密码功能
    assert True, "记住密码功能正常"
步骤 3:执行指定标记的用例
bash 复制代码
# 只执行冒烟用例(上线前必跑)
pytest -m smoke

# 执行冒烟 或 异常用例
pytest -m "smoke or exception"

# 执行冒烟 且 普通用例(多标记用例)
pytest -m "smoke and normal"
执行结果解读(以 pytest -m smoke 为例):
cmd 复制代码
collected 4 items / 2 deselected / 2 selected
tests/test_login.py::test_login_success PASSED
tests/test_login.py::test_login_remember_pwd PASSED

========================== 2 passed, 2 deselected in 0.02s ==========================
  • 选中 2 个 smoke 标记的用例,排除 2 个非冒烟用例;
  • 若未注册标记,会提示 PytestUnknownMarkWarning,但不影响执行(建议注册,规范且无警告)。

2. Fixture 固件:灵活的前后置操作

2.1. 核心场景

测试用例执行前需「初始化数据 / 连接数据库 / 启动浏览器」,执行后需「清理数据 / 关闭连接 / 关闭浏览器」,Fixture 替代传统 setup/teardown,支持不同作用域,更灵活。

2.2. 基础 Fixture(函数级)

python 复制代码
"""Fixture基础示例:函数级前后置"""
import pytest

# 定义Fixture(默认scope="function",每个用例执行前/后运行)
@pytest.fixture
def init_login_data():
    print("\n【前置操作】初始化登录测试数据:生成随机账号密码")
    test_data = {
        "username": "admin",
        "password": "123456",
        "expected_code": 200
    }
    yield test_data  # 返回数据给用例,yield后是后置操作
    print("\n【后置操作】清理登录测试数据:删除临时账号")

# 使用Fixture:直接将Fixture名作为参数传入用例
def test_login_with_fixture(init_login_data):
    # 调用Fixture返回的测试数据
    username = init_login_data["username"]
    password = init_login_data["password"]
    expected_code = init_login_data["expected_code"]
    
    # 执行登录函数(复用之前的login函数)
    result = login(username, password)
    
    # 断言
    assert result["code"] == expected_code, "登录失败"

运行 pytest test_login.py -v -s,结果如下:

cmd 复制代码
tests/test_login.py::test_login_with_fixture 
【前置操作】初始化登录测试数据:生成随机账号密码
PASSED
【后置操作】清理登录测试数据:删除临时账号

========================== 1 passed in 0.01s ==========================
  • 每个用例执行前先运行 yield 前的代码(前置),执行后运行 yield 后的代码(后置);
  • Fixture 可返回数据,用例直接接收,无需重复定义测试数据。

2.3. Fixture 作用域(实战常用)

作用域 说明 适用场景 示例
function 每个测试函数 / 方法执行一次 初始化临时数据(如每次登录新账号) 上述 init_login_data
class 每个测试类执行一次 类内所有用例共享一个数据库连接 下文 init_db_class
module 每个测试文件执行一次 整个文件共享一个浏览器实例 init_browser
session 整个测试会话执行一次 所有用例共享一个数据库连接 init_db_session

类级 Fixture 示例

python 复制代码
import pytest

# 类级Fixture:每个测试类只执行一次前后置
@pytest.fixture(scope="class")
def init_db_class():
    print("\n【类前置】连接数据库(类级)")
    db_conn = "数据库连接对象"
    yield db_conn
    print("\n【类后置】关闭数据库连接(类级)")

# 测试类:使用类级Fixture
class TestUser:
    # 第一个用例:执行类前置
    def test_query_user(self, init_db_class):
        print("查询用户信息")
        assert init_db_class == "数据库连接对象"
    
    # 第二个用例:不执行类前置(复用连接)
    def test_update_user(self, init_db_class):
        print("更新用户信息")
        assert init_db_class == "数据库连接对象"

运行结果:

cmd 复制代码
tests/test_user.py::TestUser::test_query_user 
【类前置】连接数据库(类级)
查询用户信息
PASSED
tests/test_user.py::TestUser::test_update_user 
更新用户信息
PASSED
【类后置】关闭数据库连接(类级)
  • 类内两个用例共享 Fixture,只执行一次前置 / 后置,节省资源。

2.4.全局 Fixture(conftest.py

核心优势:无需导入,所有测试文件自动生效,适合全局通用的前后置(如数据库连接、浏览器启动)。

步骤 1:创建 conftest.py(项目根目录)
python 复制代码
"""全局Fixture文件:所有测试文件可直接使用"""
import pytest

# 会话级Fixture:整个测试过程只执行一次
@pytest.fixture(scope="session")
def init_db_session():
    print("\n【全局前置】连接数据库(会话级)")
    db_conn = "全局数据库连接对象"
    yield db_conn
    print("\n【全局后置】关闭数据库连接(会话级)")

# 函数级Fixture:所有用例可使用的通用数据
@pytest.fixture
def common_data():
    return {
        "base_url": "https://api.test.com",
        "timeout": 10
    }
步骤 2:在任意测试文件中使用(无需导入)
python 复制代码
"""测试接口用例:使用全局Fixture"""
def test_query_api(init_db_session, common_data):
    # 使用会话级Fixture的数据库连接
    print(f"数据库连接:{init_db_session}")
    # 使用通用数据Fixture
    print(f"接口基础URL:{common_data['base_url']}")
    assert common_data["timeout"] == 10

运行结果:

cmd 复制代码
tests/test_api.py::test_query_api 
【全局前置】连接数据库(会话级)
数据库连接:全局数据库连接对象
接口基础URL:https://api.test.com
PASSED
【全局后置】关闭数据库连接(会话级)

3.参数化:批量生成用例

3.1.核心场景

测试同一功能的多组数据(如登录的正确 / 错误 / 空账号场景),无需重复编写用例,批量验证。

3.2.基础参数化

python 复制代码
"""参数化基础示例:登录多场景验证"""
import pytest

# 定义测试数据:(用户名, 密码, 预期状态码, 预期提示语)
test_login_data = [
    ("admin", "123456", 200, "登录成功"),  # 场景1:正确账号
    ("admin", "654321", 401, "用户名或密码错误"),  # 场景2:错误密码
    ("", "123456", 400, "用户名不能为空"),  # 场景3:空用户名
    ("test", "123456", 401, "用户名或密码错误"),  # 场景4:不存在的账号
]

# 模拟登录函数
def login(username, password):
    if username == "admin" and password == "123456":
        return {"code": 200, "msg": "登录成功"}
    elif username == "":
        return {"code": 400, "msg": "用户名不能为空"}
    else:
        return {"code": 401, "msg": "用户名或密码错误"}

# 参数化装饰器:(参数名, 测试数据)
@pytest.mark.parametrize("username, password, expected_code, expected_msg", test_login_data)
def test_login_param(username, password, expected_code, expected_msg):
    # 执行登录函数
    result = login(username, password)
    # 断言状态码和提示语
    assert result["code"] == expected_code, f"场景:{username}/{password},状态码错误"
    assert result["msg"] == expected_msg, f"场景:{username}/{password},提示语错误"

运行 pytest test_login.py -v -s,结果如下:

cmd 复制代码
collected 4 items
tests/test_login.py::test_login_param[admin-123456-200-登录成功] PASSED
tests/test_login.py::test_login_param[admin-654321-401-用户名或密码错误] PASSED
tests/test_login.py::test_login_param[-123456-400-用户名不能为空] PASSED
tests/test_login.py::test_login_param[test-123456-401-用户名或密码错误] PASSED

========================== 4 passed in 0.03s ==========================
  • 自动生成 4 个用例,每个用例对应一组数据;
  • 用例名称中显示参数值,便于定位失败场景(如某组数据失败,可直接看到是哪个账号 / 密码)。

3.3.多组参数组合(笛卡尔积)

适用于 "多维度参数验证"(如不同账号 + 不同密码组合):

python 复制代码
"""多组参数组合:笛卡尔积"""
import pytest

# 第一组参数:用户名列表
@pytest.mark.parametrize("username", ["admin", "test"])
# 第二组参数:密码列表
@pytest.mark.parametrize("password", ["123456", "654321"])
def test_login_combine(username, password):
    print(f"\n测试组合:用户名={username},密码={password}")
    result = login(username, password)
    # 断言:只有admin/123456返回200,其余返回401
    if username == "admin" and password == "123456":
        assert result["code"] == 200
    else:
        assert result["code"] == 401

运行结果:

cmd 复制代码
collected 4 items
tests/test_login.py::test_login_combine[123456-admin] 
测试组合:用户名=admin,密码=123456
PASSED
tests/test_login.py::test_login_combine[123456-test] 
测试组合:用户名=test,密码=123456
PASSED
tests/test_login.py::test_login_combine[654321-admin] 
测试组合:用户名=admin,密码=654321
PASSED
tests/test_login.py::test_login_combine[654321-test] 
测试组合:用户名=test,密码=654321
PASSED
  • 2 个用户名 × 2 个密码 = 4 个组合,自动生成 4 个用例;
  • 适合验证多维度参数的覆盖场景。

4.代码中运行 Pytest(pytest.main ())

适用于集成到自动化框架,灵活控制执行逻辑:

python 复制代码
"""在代码中运行Pytest,自定义执行逻辑"""
import pytest

if __name__ == "__main__":
    # 场景1:执行冒烟用例 + 生成HTML报告
    pytest.main([
        "-vs",
        "-m", "smoke",
        "--html=smoke_report.html",
        "--self-contained-html"
    ])

    # 场景2:执行指定文件 + 失败重试
    # pytest.main([
    #     "-vs",
    #     "tests/test_login.py",
    #     "--reruns=3"
    # ])

    # 场景3:执行所有用例并并行
    # pytest.main([
    #     "-vs",
    #     "-n", "auto"
    # ])

运行该文件(如 run_test.py),效果与终端执行命令一致,适合编写自动化脚本。

5. 插件生态:扩展核心功能

5.1. 生成 HTML 可视化报告(pytest-html)

bash 复制代码
# 安装插件
pip install pytest-html==3.2.0

# 运行用例并生成报告
pytest --html=report.html --self-contained-html
  • --self-contained-html:将 CSS / 图片嵌入报告,避免打开时样式丢失;

  • 运行完成后,项目根目录生成 report.html,打开可看到:

    • 用例总数、通过数、失败数、跳过数;
    • 每个用例的执行时间、失败原因(带详细日志);
    • 可视化统计图表(通过率、耗时分布)。

5.2. 失败重试(pytest-rerunfailures)

bash 复制代码
# 安装插件
pip install pytest-rerunfailures==11.1.2

# 运行用例:失败用例重试3次,每次间隔2秒
pytest --reruns=3 --reruns-delay=2
  • 适用于 "偶发失败" 的场景(如网络波动导致接口测试失败);
  • 重试后成功则标记为 PASSED,仍失败则标记为 FAILED

5.3. 并行执行(pytest-xdist)

bash 复制代码
# 安装插件
pip install pytest-xdist==3.5.0

# 按CPU核心数并行执行(提升执行效率)
pytest -n auto
  • 适用于用例数量多的场景(如 100 个用例串行需 10 分钟,并行只需 2-3 分钟);
  • -n auto:自动识别 CPU 核心数,也可指定数量(如 -n 4 用 4 个进程)。

四、Pytest 高级特性(工程化落地)

1. YAML 数据驱动(解耦测试数据与代码)

1.1. 核心价值

实际项目中,测试数据(如登录的多组账号、接口的入参 / 预期结果)若写死在代码里,修改 / 维护成本高;YAML 数据驱动将数据与代码分离,只需修改 YAML 文件即可更新测试场景,适配大规模用例管理。

1.2. 完整实现步骤

步骤 1:安装依赖
bash 复制代码
# PyYAML:解析YAML文件
pip install PyYAML==6.0.3
步骤 2:编写 YAML 测试数据文件

在项目根目录创建 data/test_login.yaml,存放登录测试数据:

yaml 复制代码
# 登录模块测试数据
login_success:  # 场景1:正确登录
  username: "admin"
  password: "123456"
  expected_code: 200
  expected_msg: "登录成功"

login_wrong_pwd:  # 场景2:错误密码
  username: "admin"
  password: "654321"
  expected_code: 401
  expected_msg: "用户名或密码错误"

login_empty_user:  # 场景3:空用户名
  username: ""
  password: "123456"
  expected_code: 400
  expected_msg: "用户名不能为空"
步骤 3:编写读取 YAML 的工具函数

创建 utils/yaml_util.py,封装读取 YAML 的通用方法:

python 复制代码
"""YAML文件读取工具类"""
import yaml
import os

def read_yaml(file_path):
    """
    读取YAML文件
    :param file_path: YAML文件路径
    :return: 解析后的字典/列表
    """
    # 校验文件是否存在
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"YAML文件不存在:{file_path}")
    
    # 读取并解析YAML
    with open(file_path, "r", encoding="utf-8") as f:
        data = yaml.safe_load(f)
    return data

# 测试工具函数(可选)
if __name__ == "__main__":
    test_data = read_yaml("../data/test_login.yaml")
    print(test_data)

工具函数执行结果

cmd 复制代码
{
    'login_success': {'username': 'admin', 'password': '123456', 'expected_code': 200, 'expected_msg': '登录成功'},
    'login_wrong_pwd': {'username': 'admin', 'password': '654321', 'expected_code': 401, 'expected_msg': '用户名或密码错误'},
    'login_empty_user': {'username': '', 'password': '123456', 'expected_code': 400, 'expected_msg': '用户名不能为空'}
}
步骤 4:结合 Pytest 参数化实现数据驱动

创建 tests/test_login_yaml.py

python 复制代码
"""YAML数据驱动的登录测试用例"""
import pytest
from utils.yaml_util import read_yaml
from tests.test_login import login  # 复用之前的login函数

# 读取YAML测试数据
login_data = read_yaml("../data/test_login.yaml")
# 将YAML字典转换为参数化所需的列表格式
test_data_list = [
    (
        login_data["login_success"]["username"],
        login_data["login_success"]["password"],
        login_data["login_success"]["expected_code"],
        login_data["login_success"]["expected_msg"]
    ),
    (
        login_data["login_wrong_pwd"]["username"],
        login_data["login_wrong_pwd"]["password"],
        login_data["login_wrong_pwd"]["expected_code"],
        login_data["login_wrong_pwd"]["expected_msg"]
    ),
    (
        login_data["login_empty_user"]["username"],
        login_data["login_empty_user"]["password"],
        login_data["login_empty_user"]["expected_code"],
        login_data["login_empty_user"]["expected_msg"]
    )
]

# 参数化:使用YAML数据
@pytest.mark.parametrize("username, password, expected_code, expected_msg", test_data_list)
def test_login_yaml(username, password, expected_code, expected_msg):
    result = login(username, password)
    # 断言
    assert result["code"] == expected_code, f"场景:{username}/{password} 状态码错误"
    assert result["msg"] == expected_msg, f"场景:{username}/{password} 提示语错误"
步骤 5:运行用例 & 结果解读
bash 复制代码
pytest tests/test_login_yaml.py -v -s

执行结果

cmd 复制代码
collected 3 items
tests/test_login_yaml.py::test_login_yaml[admin-123456-200-登录成功] PASSED
tests/test_login_yaml.py::test_login_yaml[admin-654321-401-用户名或密码错误] PASSED
tests/test_login_yaml.py::test_login_yaml[-123456-400-用户名不能为空] PASSED

========================== 3 passed in 0.02s ==========================

实战价值 :后续新增 / 修改登录场景,只需修改 test_login.yaml,无需改动测试代码,降低维护成本。

2. 日志模块集成(可追溯的测试过程)

2.1. 核心价值

print 输出仅在终端可见,且无法分级 / 持久化;集成 Python 标准 logging 模块,可实现:

  • 日志分级(DEBUG/INFO/WARNING/ERROR):调试时看详细日志,运行时看关键日志;
  • 日志持久化:保存到文件,用例失败后可追溯完整执行过程;
  • 日志格式化:包含时间、模块、级别、内容,便于定位问题。

2.2. 完整实现步骤

步骤 1:编写日志配置文件

创建 config/log_config.py,封装日志配置:

python 复制代码
"""日志配置模块:全局通用"""
import logging
import os
from logging.handlers import RotatingFileHandler

# 日志保存路径(不存在则创建)
log_dir = "./logs"
if not os.path.exists(log_dir):
    os.makedirs(log_dir)

def setup_logger():
    """
    配置日志:同时输出到终端 + 保存到文件
    :return: 配置好的logger对象
    """
    # 1. 创建logger对象
    logger = logging.getLogger("pytest_auto_test")
    logger.setLevel(logging.DEBUG)  # 全局日志级别(最低级别,保证所有日志都能捕获)
    logger.handlers.clear()  # 避免重复添加处理器
    
    # 2. 定义日志格式
    formatter = logging.Formatter(
        "%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S"
    )
    
    # 3. 控制台处理器(输出INFO及以上级别)
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    console_handler.setFormatter(formatter)
    
    # 4. 文件处理器(输出DEBUG及以上级别,按大小分割)
    file_handler = RotatingFileHandler(
        filename=os.path.join(log_dir, "auto_test.log"),
        maxBytes=10*1024*1024,  # 单个日志文件最大10MB
        backupCount=5,  # 最多保留5个备份文件
        encoding="utf-8"
    )
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(formatter)
    
    # 5. 添加处理器到logger
    logger.addHandler(console_handler)
    logger.addHandler(file_handler)
    
    return logger

# 全局logger对象(所有模块可直接导入使用)
logger = setup_logger()
步骤 2:在测试用例中使用日志

修改 tests/test_login_yaml.py,替换 print 为日志:

python 复制代码
"""集成日志的登录测试用例"""
import pytest
from utils.yaml_util import read_yaml
from tests.test_login import login
from config.log_config import logger  # 导入全局logger

# 读取YAML数据(同上,省略)
login_data = read_yaml("../data/test_login.yaml")
test_data_list = [...]  # 同上,省略

@pytest.mark.parametrize("username, password, expected_code, expected_msg", test_data_list)
def test_login_yaml(username, password, expected_code, expected_msg):
    # 记录测试场景(INFO级别)
    logger.info(f"开始执行登录测试场景:用户名={username},密码={password}")
    
    try:
        # 执行登录函数
        result = login(username, password)
        logger.debug(f"登录接口返回结果:{result}")  # DEBUG级别:详细返回值
        
        # 断言
        assert result["code"] == expected_code
        assert result["msg"] == expected_msg
        logger.info(f"登录测试场景通过:用户名={username}")
    
    except AssertionError as e:
        # 记录失败日志(ERROR级别)
        logger.error(f"登录测试场景失败:{e}")
        raise  # 重新抛出异常,保证pytest捕获失败
步骤 3:运行用例 & 日志效果
bash 复制代码
pytest tests/test_login_yaml.py -v

终端日志输出(INFO 级别):

cmd 复制代码
2026-03-13 15:30:00 - pytest_auto_test - INFO - test_login_yaml.py:20 - 开始执行登录测试场景:用户名=admin,密码=123456
2026-03-13 15:30:00 - pytest_auto_test - INFO - test_login_yaml.py:28 - 登录测试场景通过:用户名=admin

日志文件(logs/auto_test.log)(包含 DEBUG 级别):

cmd 复制代码
2026-03-13 15:30:00 - pytest_auto_test - INFO - test_login_yaml.py:20 - 开始执行登录测试场景:用户名=admin,密码=123456
2026-03-13 15:30:00 - pytest_auto_test - DEBUG - test_login_yaml.py:25 - 登录接口返回结果:{'code': 200, 'msg': '登录成功'}
2026-03-13 15:30:00 - pytest_auto_test - INFO - test_login_yaml.py:28 - 登录测试场景通过:用户名=admin

实战价值:用例失败时,可通过日志文件查看完整执行过程(如接口返回值、参数是否正确),快速定位问题,而非仅依赖终端的断言错误。

3. 配置文件管理(多环境适配)

3.1. 核心价值

实际项目中存在「测试环境、预发环境、生产环境」,接口地址、数据库配置等需根据环境切换;将配置抽离为独立文件(如 config.yaml),通过参数控制环境,避免硬编码,适配多环境测试。

3.2. 完整实现步骤

步骤 1:编写多环境配置文件

创建 config/config.yaml

yaml 复制代码
# 多环境配置
dev:  # 开发环境
  base_url: "https://dev-api.test.com"
  db:
    host: "127.0.0.1"
    port: 3306
    user: "dev_user"
    password: "dev_pwd"

test:  # 测试环境(默认)
  base_url: "https://test-api.test.com"
  db:
    host: "192.168.1.100"
    port: 3306
    user: "test_user"
    password: "test_pwd"

prod:  # 生产环境(谨慎使用)
  base_url: "https://api.test.com"
  db:
    host: "10.0.0.1"
    port: 3306
    user: "prod_user"
    password: "prod_pwd"
步骤 2:封装配置读取工具

修改 utils/yaml_util.py,新增读取配置的方法:

python 复制代码
"""YAML文件读取工具类(新增配置读取)"""
import yaml
import os

def read_yaml(file_path):
    """读取普通YAML测试数据(原有方法)"""
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"YAML文件不存在:{file_path}")
    with open(file_path, "r", encoding="utf-8") as f:
        data = yaml.safe_load(f)
    return data

def read_config(env="test"):
    """
    读取多环境配置
    :param env: 环境名(dev/test/prod),默认test
    :return: 指定环境的配置字典
    """
    config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config/config.yaml")
    config_data = read_yaml(config_path)
    if env not in config_data:
        raise ValueError(f"不支持的环境:{env},可选环境:dev/test/prod")
    return config_data[env]

# 测试配置读取(可选)
if __name__ == "__main__":
    test_config = read_config("test")
    print(f"测试环境接口地址:{test_config['base_url']}")
    dev_config = read_config("dev")
    print(f"开发环境数据库地址:{dev_config['db']['host']}")

工具函数执行结果

cmd 复制代码
测试环境接口地址:https://test-api.test.com
开发环境数据库地址:127.0.0.1
步骤 3:在测试用例中使用多环境配置

修改 tests/test_login_yaml.py,适配多环境接口地址:

python 复制代码
"""多环境配置的登录测试用例"""
import pytest
import requests  # 模拟真实接口请求
from utils.yaml_util import read_yaml, read_config
from config.log_config import logger

# 读取YAML测试数据 + 环境配置
login_data = read_yaml("../data/test_login.yaml")
env_config = read_config(env="test")  # 可通过命令行参数动态指定
base_url = env_config["base_url"]

# 转换测试数据(原有逻辑,省略)
test_data_list = [...]

@pytest.mark.parametrize("username, password, expected_code, expected_msg", test_data_list)
def test_login_yaml(username, password, expected_code, expected_msg):
    logger.info(f"【{env_config}环境】开始执行登录测试:{username}/{password}")
    # 真实接口请求(结合环境配置的base_url)
    url = f"{base_url}/login"
    payload = {"username": username, "password": password}
    try:
        response = requests.post(url, json=payload, timeout=10)
        result = response.json()
        logger.debug(f"接口返回:{result}")
        
        # 断言
        assert result["code"] == expected_code
        assert result["msg"] == expected_msg
        logger.info(f"【{env_config}环境】登录测试通过:{username}")
    except Exception as e:
        logger.error(f"【{env_config}环境】登录测试失败:{e}")
        raise
步骤 4:动态指定环境运行用例

修改 run_test.py,支持通过命令行参数指定环境:

python 复制代码
"""支持多环境的测试执行入口"""
import pytest
import os
import sys
from config.log_config import logger
from utils.yaml_util import read_config

if __name__ == "__main__":
    # 1. 获取命令行传入的环境参数(默认test)
    env = sys.argv[1] if len(sys.argv) > 1 else "test"
    try:
        read_config(env)  # 校验环境是否合法
    except ValueError as e:
        logger.error(e)
        sys.exit(1)
    
    # 2. 记录环境信息
    logger.info(f"========== 开始执行【{env}】环境自动化测试 ==========")
    
    # 3. 执行参数(原有逻辑,新增环境标记)
    report_path = f"./reports/{env}_test_report.html"
    pytest_args = [
        "-vs",
        "-m", "smoke",
        f"--html={report_path}",
        "--self-contained-html",
        "--reruns=2",
        "-n", "auto"
    ]
    
    # 4. 执行测试(原有逻辑,省略)
    pytest.main(pytest_args)
步骤 5:运行用例 & 结果解读
bash 复制代码
# 执行测试环境用例
python run_test.py test

# 执行开发环境用例
python run_test.py dev

执行结果

cmd 复制代码
2026-03-13 16:00:00 - pytest_auto_test - INFO - run_test.py:15 - ========== 开始执行【test】环境自动化测试 ==========
2026-03-13 16:00:00 - pytest_auto_test - INFO - test_login_yaml.py:20 - 【test环境】开始执行登录测试:admin/123456
2026-03-13 16:00:01 - pytest_auto_test - DEBUG - test_login_yaml.py:28 - 接口返回:{'code': 200, 'msg': '登录成功'}
2026-03-13 16:00:01 - pytest_auto_test - INFO - test_login_yaml.py:32 - 【test环境】登录测试通过:admin

实战价值:无需修改代码,仅通过命令行参数即可切换测试环境,适配不同阶段的测试需求(如开发环境联调、测试环境回归)。

4. 失败用例重跑 & 精准重试(精细化控制)

4.1. 核心价值

pytest-rerunfailures 插件默认对所有失败用例重试,但实际场景中需「只重试网络类失败」「按标记重试」「限制重试间隔」,精细化控制重试逻辑,避免无效重试。

4.2. 完整实现步骤

步骤 1:安装插件(原有)
bash 复制代码
pip install pytest-rerunfailures==11.1.2
步骤 2:按标记 / 异常类型重试

修改 pytest.ini,配置精细化重试规则:

ini 复制代码
[pytest]
addopts = -vs --reruns 2 --reruns-delay 3  # 全局重试2次,间隔3秒
testpaths = ./tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
    smoke: 冒烟用例
    normal: 普通用例
    retry: 需要重试的用例(如接口类)
步骤 3:给指定用例标记重试
python 复制代码
"""精细化重试的接口测试用例"""
import pytest
import requests
from config.log_config import logger
from utils.yaml_util import read_config

env_config = read_config("test")
base_url = env_config["base_url"]

# 只给接口用例标记retry,且只重试ConnectionError异常
@pytest.mark.retry
def test_query_user():
    url = f"{base_url}/user/info"
    try:
        response = requests.get(url, timeout=5)
        assert response.status_code == 200
    except requests.exceptions.ConnectionError as e:
        logger.warning(f"网络连接失败,触发重试:{e}")
        raise  # 抛出异常触发重试
    except Exception as e:
        logger.error(f"非网络异常,不重试:{e}")
        raise  # 不重试,直接失败

# 普通用例:不重试
def test_calculate():
    assert 1 + 2 == 4  # 断言失败,直接失败,不重试
步骤 4:运行用例 & 结果解读
bash 复制代码
pytest tests/test_retry.py -v --reruns 2 --reruns-delay 3 -m retry

执行结果(模拟网络失败)

cmd 复制代码
tests/test_retry.py::test_query_user RERUN  # 第一次失败,重试
tests/test_retry.py::test_query_user RERUN  # 第二次失败,重试
tests/test_retry.py::test_query_user FAILED # 最终失败

========================== 1 failed in 9.01s ==========================

关键说明

  • --reruns 2:最多重试 2 次,总执行次数 = 1(原执行)+2(重试)=3 次;
  • --reruns-delay 3:每次重试间隔 3 秒,避免高频请求触发接口限流;
  • -m retry:只对标记为retry的用例重试,普通用例不重试。

实战价值:针对网络波动、接口限流等偶发失败场景精准重试,减少误报;普通用例(如计算、逻辑判断)失败不重试,提升测试效率。

5. 测试报告增强(Allure 报告)

Allure 是一款开源的测试报告生成框架,专为测试结果的可视化、可交互性设计,常与 Pytest、JUnit、TestNG 等测试框架集成,是 Python 自动化测试中主流的报告工具之一。

5.1. 核心价值

pytest-html 报告轻量化但功能简单,Allure 报告支持「用例分层、步骤展示、图表统计、失败截图 / 日志关联」,是企业级自动化测试的首选报告工具。

5.2. 完整实现步骤

步骤 1:安装Allure

下载链接https://github.com/allure-framework/allure2/releases(https://github.com/allure-framework/allure2/releases/download/2.30.0/allure 2.30.0.zip)

添加至环境变量

验证安装是否成功

cmd 复制代码
allure --version

出现 allure 版本则安装成功

步骤 2:安装客户端依赖
bash 复制代码
# 安装Allure pytest插件
pip install allure-pytest

# 下载Allure命令行工具(需配置环境变量)
# Windows:https://github.com/allure-framework/allure2/releases
# Mac:brew install allure
# Linux:sudo apt-get install allure

# 若已安装,可升级到最新版本
pip install --upgrade allure-pytest
步骤 3:编写带 Allure 步骤的用例
python 复制代码
"""Allure增强报告的测试用例"""
import pytest
import allure
from utils.yaml_util import read_yaml
from config.log_config import logger

# 读取测试数据
login_data = read_yaml("../data/test_login.yaml")

@allure.epic("用户模块")  # 大模块(如项目级)
@allure.feature("登录功能")  # 功能模块
@allure.story("正确账号密码登录")  # 具体场景
@pytest.mark.smoke
def test_login_success():
    # 拆分测试步骤(Allure报告中可展开查看)
    with allure.step("步骤1:准备测试数据"):
        username = login_data["login_success"]["username"]
        password = login_data["login_success"]["password"]
        logger.info(f"测试数据:{username}/{password}")
    
    with allure.step("步骤2:执行登录操作"):
        result = login(username, password)  # 复用login函数
    
    with allure.step("步骤3:断言结果"):
        assert result["code"] == 200
        assert result["msg"] == "登录成功"
    
    # 附加测试数据到报告
    allure.attach(str(login_data["login_success"]), "测试数据", allure.attachment_type.JSON)
步骤 4:生成 Allure 报告
bash 复制代码
# 步骤1:生成Allure原始数据
pytest tests/test_allure.py -v --alluredir=./reports/allure-results

# 步骤2:生成HTML报告(本地预览)
allure serve ./reports/allure-results

# 步骤3:生成静态HTML报告(便于分享)
allure generate ./reports/allure-results -o ./reports/allure-report --clean --encoding utf-8
步骤 5:Allure 报告效果
  • 左侧导航:按 epic/feature/story 分层展示用例,可快速筛选;
  • 用例详情:显示每个 allure.step 的执行结果,失败步骤标红;
  • 附件:可查看附加的测试数据、日志、截图;
  • 统计图表:展示用例通过率、执行时长、失败分布。

实战价值:Allure 报告结构化展示测试结果,便于团队协作(如开发查看失败步骤、测试负责人统计通过率),是企业级自动化测试的标准配置。

6. 自动化测试实战(完整流程封装)

3.1. 核心场景

结合「Fixture + YAML 数据驱动 + 日志 + 报告」,搭建一套可落地的接口自动化测试框架,覆盖 "前置准备→用例执行→结果记录→报告生成" 全流程。

3.2. 框架目录结构(规范且易维护)

复制代码
pytest_auto_test/
├── config/                # 配置目录
│   └── log_config.py      # 日志配置
├── data/                  # 测试数据目录
│   └── test_login.yaml    # 登录测试数据
├── logs/                  # 日志输出目录
├── reports/               # 测试报告目录
├── tests/                 # 测试用例目录
│   ├── test_login.py      # 登录功能基础用例
│   └── test_login_yaml.py # YAML数据驱动用例
├── utils/                 # 工具类目录
│   └── yaml_util.py       # YAML读取工具
├── conftest.py            # 全局Fixture
├── pytest.ini             # pytest全局配置
└── run_test.py            # 测试执行入口

3.3. 测试执行入口(run_test.py)

python 复制代码
"""自动化测试执行入口:一键运行所有用例"""
import pytest
import os
from config.log_config import logger

if __name__ == "__main__":
    # 1. 记录开始日志
    logger.info("========== 自动化测试开始执行 ==========")
    
    # 2. 定义执行参数
    report_path = "./reports/test_report.html"
    pytest_args = [
        "-vs",                  # 详细输出 + 显示日志
        "-m", "smoke",          # 只执行冒烟用例
        "--html=" + report_path, # 生成HTML报告
        "--self-contained-html", # 报告样式内嵌
        "--reruns=2",           # 失败重试2次
        "-n", "auto"            # 并行执行
    ]
    
    # 3. 执行测试
    try:
        pytest.main(pytest_args)
        logger.info("========== 自动化测试执行完成 ==========")
    except Exception as e:
        logger.error(f"自动化测试执行异常:{e}")
        raise

    # 4. 验证报告生成
    if os.path.exists(report_path):
        logger.info(f"测试报告已生成:{os.path.abspath(report_path)}")
    else:
        logger.warning("测试报告生成失败!")

3.4. 运行入口文件 & 完整流程

bash 复制代码
python run_test.py

执行流程

  1. 初始化全局日志 →
  2. 加载 conftest.py 中的全局 Fixture(如数据库连接)→
  3. 读取 YAML 测试数据 →
  4. 并行执行冒烟用例 →
  5. 失败用例自动重试 →
  6. 生成 HTML 报告 →
  7. 记录执行日志 →
  8. 输出报告路径。

最终效果

  • 日志文件:记录所有执行步骤和用例结果;
  • HTML 报告:可视化展示用例通过率、失败原因、执行时间;
  • 一键执行:无需手动输入复杂命令,适合非技术人员 / 定时任务执行。
相关推荐
imzed1 小时前
使用 Playwright + Pytest 构建 Web UI 自动化测试框架
python·自动化·pytest
普通网友2 小时前
pytest一些常见的插件
开发语言·python·pytest
时空系2 小时前
Python 高性能高压缩打包器 —— 基于 JianPy 语义分析引擎
python
交友如交2 小时前
对于Prompt的思考:从“手写”到提示词采样、A/B Test 与自动化评测
人工智能·自动化·prompt
cndes2 小时前
给Miniconda换源,让包下载更迅速
开发语言·python
Amy187021118232 小时前
告别“爬楼抄表”时代:电梯计量自动化改造,让每一度电都“开口说话”
运维·自动化
Metaphor6922 小时前
使用 Python 添加、隐藏和删除 PDF 图层
python·pdf·图层
丨白色风车丨2 小时前
【Python 计算机视觉】基于 Dlib+OpenCV 实现实时人眼疲劳检测(闭眼预警)
python·opencv·计算机视觉
曦尧3 小时前
开源后台编程 Agent 系统 background-agents:架构拆解与落地判断
ai·自动化