PyTest框架详解、集成Allure报告及在自动化测试中的应用
零、参考文章
本篇在下面文章基础上修改、补充和整理,感谢各文章原作者大佬的撰写:
pytest框架-详解(学习pytest框架这一篇就够了)-CSDN博客
自动化测试基础——allure下载安装及配置及pytest + allure-pytest插件生成allure企业级测试报告及企业级定制-CSDN博客
Pytest测试框架最常用的13个插件_pytest常用插件-CSDN博客
【pytest】二、pytest之全局配置文件pytest.ini,及命令执行参数详解-CSDN博客
官方文档:
一、pytest框架的约束
1. python命名规范
- py文件全部小写,多个英文用_隔开
- class名首字母大写,驼峰
- 函数和方法名小写,多个英文用_隔开
- 全局变量,前面要加global
- 常量字母必须全大写,如:AGE_OF_NICK
2. pytest命名规范
- 模块名(py文件)必须是以test_开头或者_test结尾
- 测试类(class)必须以Test开头,并且不能带init方法,类里的方法必须以test_开头
- 测试用例(函数)必须以test_开头
二、运行方式
1. 主函数运行
import pytest
def test_01():
print("啥也没有")
if __name__=='__main__':
pytest.main()
| **参数 ** | **描述 ** | **案例 ** |
|---|---|---|
| -v | 输出调试信息(如打印执行过程) | pytest.main(["-v", "testcase/test_one.py", "testcase/test_two.py"]) |
| -s | 输出更详细信息(含文件名、用例名,保留 print输出) |
pytest.main(["-vs", "testcase/test_one.py", "testcase/test_two.py"]) |
| -n | 多线程 / 分布式运行测试用例 | pytest.main(["-vs", "-n=4", "testcase/"]) ( 4为指定线程数) |
| -x | 任意用例失败后立即停止测试 | pytest.main(["-vsx", "testcase/test_one.py"]) |
| --maxfail | 累计 N 个用例失败后停止测试 | pytest.main (["-vs", "--maxfail=2", "testcase/test_one.py"]) (注:原案例 -x=2为写法错误) |
| --html=report.html | 生成 HTML 格式测试报告 | pytest.main(["-vs", "--html=./report.html", "testcase/test_one.py"]) |
| -m | 通过标记表达式筛选用例执行 | pytest.main (["-vs", "-m'smoke'", "testcase/"]) (需先通过 @pytest.mark.smoke标记用例) |
| -k | 通过用例名称关键词筛选(支持 and/or逻辑) |
pytest.main(["-vs", "-k 'login or logout'", "testcase/"]) |
2. 命令行运行
| **参数 ** | **描述 ** | **可用案例 ** |
|---|---|---|
| -v | 输出调试信息(如用例执行过程、路径) | pytest -v ./testcase/test_one.py |
| -q | 输出简单信息(仅显示最终统计结果) | pytest -q ./testcase/test_one.py |
| -s | 输出更详细信息(含文件名、用例名,保留 print输出) |
pytest -s ./testcase/test_one.py |
| -n | 多线程 / 分布式运行测试用例 | pytest -n 4 ./testcase/(需安装 pytest-xdist插件, 4为指定线程数) |
| -x | 任意用例失败后立即停止测试 | pytest -x ./testcase/test_one.py |
| --maxfail | 累计 N 个用例失败后停止测试 | pytest --maxfail=2 ./testcase/test_one.py |
| --html=report.html | 生成 HTML 格式测试报告 | pytest ./testcase/test_one.py --html=./report/report.html |
| -k | 根据用例名称关键词筛选(支持 and/or逻辑) |
pytest -k "MyClass and not method" ./testcase/ |
3. 【常用】pytest.ini配置文件方式运行
pytest.ini 是 pytest 的 **全局配置文件 ** ,放置在项目根目录下,可替代重复的命令行参数 / pytest.main() 参数,运行时 pytest 会自动读取配置。 ** 不建议文件中包含中文 **
pytest.ini示例:
[pytest]
# 1. 用例搜索配置
testpaths = tests # 所有测试用例放在 tests 目录下
python_files = test_*.py # 测试文件以 test_ 开头
python_classes = Test* # 测试类以 Test 开头
python_functions = test_* # 测试方法以 test_ 开头
# 2. 全局执行参数(组合常用功能)
addopts = -v -s --tb=short --html=./reports/test_report.html --cov=./src --cov-report=html:./reports/coverage
# 3. 注册测试标记(避免使用中文)
markers =
smoke: 冒烟测试(必跑)
critical: 核心功能(阻塞发布)
slow: 耗时用例(可选跑)
skip_linux: 跳过 Linux 环境
regression: regression tests
api: api tests
web: web ui tests
# 4. 覆盖率细化配置
cov_fail_under = 85 # 覆盖率低于 85% 则测试失败
cov_exclude =
src/config/*
src/*_test.py
# 5. HTML 报告美化
html_title = 电商项目自动化测试报告
html_description = 覆盖登录、下单、支付核心流程
配置优先级: ** 命令行参数 > ** pytest.ini ** 配置 > 默认规则 ** (如命令行输入 pytest -q 会覆盖配置中的 -v )
addopts配置:参数详解
- -s:表示输出调试信息,用于显示测试函数中print()打印的信息
- -v:未加前只打印模块名,加v后打印类名、模块名、方法名,显示更详细的信息
- -q:表示只显示整体测试结果
- -vs:这两个参数可以一起使用
- -n:支持多线程或者分布式运行测试用例(前提需安装:pytest-xdist插件)
- –html:生成html的测试报告(前提需安装:pytest-html插件) 如:pytest -vs --html ./reports/result.html
- --reruns num: 用例失败后重跑,跑几次(前提需安装:pytest-rerunfailures插件) 如:pytest -vs --reruns=2
- -x:表示只要出现一个用例失败报错则停止执行,如:pytest -vs -x
- –maxfail:表示出现几个用例失败报错,则终止测试,如:pytest -vs --maxfail=2
- -k:模糊匹配,运行测试用例名称中包含某个字符串的测试用例: 如: pytest -vs -k “ao or userPage”
1) 注册mark标记
我们可以在测试用例上输入@pytest.mark.login来对用例进行标记,但有时手误可能输入成@pytest.mark.loggin;这不会引起程序错误,它会以为你新加了一个标记:loggin。
为了避免这种拼写错误,避免遗漏执行测试用例。可以在ini文件中,对所有用到的标记做注册,这样程序中添加未注册的标记时就会报错。
:后面的文字,是对该标记做的说明。
markers =
demo : marks tests as demo
smoke: marks tests as smoke
uat : marks tests as uat
test : marks tests as test
login:marks login cases as test
指定 pytest 最低版本号
minversion = 5.0
testpaths配置
1)pytest默认是搜索执行当前目录下的所有以test_开头的测试用例;我们可以在pytest.ini配置testpaths = test_case/test_001.py,则只执行当前配置的文件夹下或文件里的指定用例
2)可配置多个,空格隔开:python_files = test_.py haha_.py
;测试用例文件夹,可自己配置,
;../pytestproject为上一层的pytestproject文件夹
;./testcase为pytest.ini当前目录下的同级文件夹
testpaths =./testcase
;配置测试搜索的模块文件名称
python_files = test*.py
;配置测试搜索的测试类名
python_classes = Test*
;配置测试搜索的测试函数名
python_functions = test
pytest常用的插件
Pytest测试框架最常用的13个插件_pytest常用插件-CSDN博客
三、conftest.py文件
1. conftest文件特点
- pytest 会默认读取 conftest.py里面的所有 fixture
- conftest.py 文件名称是固定的,不能改动
- conftest.py 只对同一个 package 下的所有测试用例生效
- 不同目录可以有自己的 conftest.py,一个项目中可以有多个 conftest.py
- 测试用例文件中不需要手动 import conftest.py,pytest 会自动查找
四、pytest中的fixture装饰器
1. 前言
虽然setup和teardown可以执行一些前置和后置操作,但是这种是针对整个脚本全局生效的
如果有以下场景:1.用例一需要执行登录操作;2.用例二不需要执行登录操作;3.用例三需要执行登录操作,则setup和teardown则不满足要求。fixture可以让我自定义测试用例的前置条件
2. fixture优势
- 命名方式灵活,不限于setup和teardown两种命名
- conftest.py可以实现数据共享,不需要执行import 就能自动找到fixture
- scope=module,可以实现多个.py文件共享前置
- scope=“session” 以实现多个.py 跨文件使用一个 session 来完成多个用例
3. fixture调用方式
@pytest.fixture(scope = "function",params=None,autouse=False,ids=None,name=None)
scope:控制 fixture 的复用范围(默认 function,可选 class/module/session);params:传入可迭代对象实现 fixture 参数化,测试用例会自动多轮执行;autouse:是否自动调用 fixture(默认 False,True 则作用域内用例无需手动传参);ids:配合 params 给每个参数值设标识,优化测试结果可读性;name:给 fixture 设置别名,用例通过别名调用(原函数名失效)。
4. fixture作用范围
| **取值 ** | **范围说明 ** |
|---|---|
| function | 函数级 每一个函数或方法都会调用 |
| class | 函数级 模块级 每一个.py 文件调用一次 |
| module | 模块级 每一个.py 文件调用一次 |
| session | 会话级 每次会话只需要运行一次,会话内所有方法及类,模块都共享这个方法 |
5. fixture参数-scope
用于控制Fixture的作用范围
作用类似于Pytest的setup/teardown
默认取值为function(函数级别),控制范围的排序为:session > module > class > function
1) scope = “function”
① 作为参数传入
import pytest
# fixture函数(类中) 作为多个参数传入
@pytest.fixture()
def login():
print("打开浏览器")
a = "account"
return a
@pytest.fixture()
def logout():
print("关闭浏览器")
class TestLogin:
#传入lonin fixture
def test_001(self, login):
print("001传入了loging fixture")
assert login == "account"
#传入logout fixture
def test_002(self, logout):
print("002传入了logout fixture")
def test_003(self, login, logout):
print("003传入了两个fixture")
def test_004(self):
print("004未传入仍何fixture哦")
if __name__ == '__main__':
pytest.main()

从运行结果可以看出,fixture做为参数传入时,会在执行函数之前执行该fixture函数。再将值传入测试函数做为参数使用,这个场景多用于登录
② fixture互相调用
import pytest
# fixtrue作为参数,互相调用传入
@pytest.fixture()
def account():
a = "account"
print("第一层fixture")
return a
#Fixture的相互调用一定是要在测试类里调用这层fixture才会生次,普通函数单独调用是不生效的
@pytest.fixture()
def login(account):
print("第二层fixture")
class TestLogin:
def test_1(self, login):
print("直接使用第二层fixture,返回值为{}".format(login))
def test_2(self, account):
print("只调用account fixture,返回值为{}".format(account))
if __name__ == '__main__':
pytest.main()

1.即使fixture之间支持相互调用,但普通函数直接使用fixture是不支持的,一定是在测试函数内调用才会逐级调用生效
2.有多层fixture调用时,最先执行的是最后一层fixture,而不是先执行传入测试函数的fixture
3.上层fixture的值不会自动return,这里就类似函数相互调用一样的逻辑
2) scope = "class"
- 当测试类内的每一个测试方法都调用了fixture,fixture只在该class下所有测试用例执行前执行一次
- 测试类下面只有一些测试方法使用了fixture函数名,这样的话,fixture只在该class下第一个使用fixture函数的测试用例位置开始算,后面所有的测试用例执行前只执行一次。而该位置之前的测试用例就不管。
import pytest
# fixture作用域 scope = 'class'
@pytest.fixture(scope='class')
def login():
print("scope为class")
class TestLogin:
def test_1(self, login):
print("用例1")
def test_2(self, login):
print("用例2")
def test_3(self, login):
print("用例3")
if __name__ == '__main__':
pytest.main()

- 可见调用该fixture的用例仅第一个调用了一次
3) scope = "module"
- 与class相同,只从.py文件开始引用fixture的位置生效
- 需要注意的是class是对单个类产生作用,module是按照当个文件产生作用
import pytest
# fixture scope = 'module'
@pytest.fixture(scope='module')
def login():
print("fixture范围为module")
def test_01():
print("用例01")
def test_02(login):
print("用例02")
class TestLogin():
def test_1(self):
print("用例1")
def test_2(self):
print("用例2")
def test_3(self):
print("用例3")
if __name__ == '__main__':
pytest.main()

- 关于class和module区别小结
- 用
scope="class":当 fixture 是 **测试类专属资源 ** (如类内测试需要的统一配置、临时对象),且不同类需要独立资源时(比如不同类测试不同功能,避免状态污染)。 - 用
scope="module":当 fixture 是 **模块级公共资源 ** (如数据库连接、配置文件读取、耗时的初始化操作),且模块内所有测试可以复用同一资源时(减少重复初始化,提升测试效率)。
- 用
scope= "session"
- session的作用范围是针对.py级别的,module是对当前.py生效,seesion是对多个.py文件生效
- session只作用于一个.py文件时,作用相当于module
- 所以session多数与contest.py文件一起使用,做为全局Fixture
6. fixture参数详解-autouse
- 默认False
- 若为True,刚每个测试函数都会自动调用该fixture,无需传入fixture函数名
- 由此我们可以总结出调用fixture的三种方式:
- 1.函数或类里面方法直接传fixture的函数参数名称
- 2.使用装饰器@pytest.mark.usefixtures()修饰
- 3.autouse=True自动调用,无需传仍何参数,作用范围跟着scope走(谨慎使用)
- 让我们来看一下,当autouse=ture的效果:
- 这里使用了原文的图片

7. fixture参数详解params、ids
- Fixture的可选形参列表,支持列表传入
- 默认None,每个param的值
- fixture都会去调用执行一次,类似for循环
- 可与参数ids一起使用,作为每个参数的标识,详见ids
- 被Fixture装饰的函数要调用是采用:Request.param(固定写法,如下图)
- 举个栗子:
- 由于这里原文也没有给出代码,这里我写了个简单的demo来举例,顺便连同ids一起展示
import pytest
@pytest.fixture(params=['user1', 'user2', 'user3'], ids=['ids_user1', 'ids_user2', 'ids_user3'])
def login(request):
return request.param
def test_login(login):
print(f"登录用户:{login}")
结果:
============================= test session starts =============================
collecting ... collected 3 items
fixture_params.py::test_login[ids_user1] PASSED [ 33%]登录用户:user1
fixture_params.py::test_login[ids_user2] PASSED [ 66%]登录用户:user2
fixture_params.py::test_login[ids_user3] PASSED [100%]登录用户:user3
============================== 3 passed in 0.02s ==============================
可见这里就像遍历一样调用了params里面的每一个参数,ids用于生成了测试名称
8. fixture参数-name
- fixture的重命名
- 通常来说使用 fixture 的测试函数会将 fixture 的函数名作为参数传递,但是 pytest 也允许将fixture重命名
- 如果使用了name,那只能将name传如,函数名不再生效
- 调用方法:@pytest.mark.usefixtures(‘fixture1’,‘fixture2’)
举栗:
import pytest
@pytest.fixture(name="new_fixture")
def test_name():
pass
# 使用name参数后,传入重命名函数,执行成功
def test_1(new_fixture):
print("使用name参数后,传入重命名函数,执行成功")
# 使用name参数后,仍传入函数名称,会失败
def test_2(test_name):
print("使用name参数后,仍传入函数名称,会失败")
if __name__ == '__main__':
pytest.main()

五、pytest跳过测试用例skip、skipif
1. @pytest.mark.skip
跳过执行测试用例,有可选参数 reason:跳过的原因,会在执行结果中打印
- @pytest.mark.skip可以加在函数上,类上,类方法上
- 如果加在类上面,类里面的所有测试用例都不会执行
import pytest
@pytest.fixture(autouse=True)
def login():
print("====登录====")
def test_case01():
print("我是测试用例11111")
@pytest.mark.skip(reason="不执行该用例!!因为没写好!!")
def test_case02():
print("我是测试用例22222")
class Test1:
def test_1(self):
print("%% 我是类测试用例1111 %%")
@pytest.mark.skip(reason="不想执行")
def test_2(self):
print("%% 我是类测试用例2222 %%")
@pytest.mark.skip(reason="类也可以跳过不执行")
class TestSkip:
def test_1(self):
print("%% 不会执行 %%")
if __name__ == '__main__':
pytest.main()

2. pytest.skip
作用:在测试用例执行期间强制跳过不再执行剩余内容
类似:在Python的循环里面,满足某些条件则break 跳出循环
def test_function():
n = 1
while True:
print(f"这是我第{n}条用例")
n += 1
if n == 5:
pytest.skip("我跑五次了不跑了")
- pytest.skip提供有两个参数,支持模块级别的跳过
- msg:跳过原因
- allow_module_level=True:允许在模块级别跳过(默认False,只能在测试函数中使用)
import pytest
import os
# 跳过无GPU环境的测试
if not os.environ.get("CUDA_AVAILABLE"):
pytest.skip("需要GPU环境", allow_module_level=True)
# 跳过特定操作系统的测试
if sys.platform != "linux":
pytest.skip("仅支持Linux系统", allow_module_level=True)
3. 条件跳过pytest.mark.skipif
根据特定条件跳过单个测试函数或类。
- condition:跳过条件,为True时跳过
- reason:跳过的原因说明
import pytest
class Test_ABC:
def setup_class(self):
print("------->setup_class")
def teardown_class(self):
print("------->teardown_class")
def test_a(self):
"""正常执行的测试用例"""
print("------->test_a")
assert 1
@pytest.mark.skipif(condition=2 > 1, reason="条件成立,跳过该函数")
def test_b(self):
"""被跳过的测试用例"""
print("------->test_b")
assert 0

4. 通过标记
个人理解有点像Java的自定义注解,AOP
- 可以将 pytest.mark.skip 和 pytest.mark.skipif 赋值给一个标记变量
- 在不同模块之间共享这个标记变量
- 若有多个模块的测试用例需要用到相同
- 的 skip 或 skipif ,可以用一个单独的文件去管理这些通用标记,然后适用于整个测试用例集
# 标记
skipmark = pytest.mark.skip(reason="不能在window上运行=====")
skipifmark = pytest.mark.skipif(sys.platform == 'win32', reason="不能在window上运行啦啦啦=====")
@skipmark
class TestSkip_Mark(object):
@skipifmark
def test_function(self):
print("测试标记")
def test_def(self):
print("测试标记")
@skipmark
def test_skip():
print("测试标记")
5. pytest.importorskip( modname: str, minversion: Optional[str] = None, reason: Optional[str] = Nonse )
缺少导入则跳过测试
参数列表
- modname:模块名
- minversion:版本号
- reason:跳过原因,默认不给也行
pexpect = pytest.importorskip("pexpect", minversion="0.3")
@pexpect
def test_import():
print("test")
6. 自定义标记mark
- pytest可以支持自定义标记,自定义标记可以把一个web项目划分为多个模块,然后指定模块名称执行
- 譬如我们可以标明哪些用例在window上执行,哪些用例在mac上执行,在运行的时候指定mark就行
- 例如划分不同的测试项目
import pytest
@pytest.mark.model
def test_model_a():
print("执行test_model_a")
@pytest.mark.regular
def test_regular_a():
print("test_regular_a")
@pytest.mark.model
def test_model_b():
print("test_model_b")
@pytest.mark.regular
class TestClass:
def test_method(self):
print("test_method")
def testnoMark():
print("testnoMark")
使用运行例如
pytest -s -m model [文件名.py]
- 运行后我们会发现有一些警告
- 我们需要创建一个pytest.ini文件加上自定义的mark
- **pytest.ini 需要和运行的测试用例同一个目录,或在根目录下作用于全局 **
[pytest]
markers =
model: this is model mark
- 如果不想标记 model 的用例
pytest -s -m " not model" test_one.py
- **如果想执行多个自定义标记的用例 **
pytest -s -m “model or regular” 08_mark.py
六、pytest参数化 @pytest.mark.parametrize
pytest允许在多个级别启用测试化参数:
1)pytest.fixture()允许fixture有参数化功能
2)pytest.mark.parametrize 允许在测试函数和类中定义多组参数和fixtures
3)pytest_generate_tests允许定义自定义参数化方案或扩展
import pytest
@pytest.mark.parametrize("test_input,expected,msg", [("3+5", 8, "3+5等于8"), ("2+4", 6, "2+4等于6"), ("6*9", 42, "6*9等于42")])
def test_eval(test_input, expected, msg):
print(f"测试数据{test_input},期望结果{expected},测试信息{msg}")
assert eval(test_input) == expected
============================= test session starts =============================
collecting ... collected 3 items
parametrize.py::test_eval[3+5-8-3+5\u7b49\u4e8e8]
parametrize.py::test_eval[2+4-6-2+4\u7b49\u4e8e6]
parametrize.py::test_eval[6*9-42-6*9\u7b49\u4e8e42]
========================= 1 failed, 2 passed in 0.29s =========================
PASSED [ 33%]测试数据3+5,期望结果8,测试信息3+5等于8
PASSED [ 66%]测试数据2+4,期望结果6,测试信息2+4等于6
FAILED [100%]测试数据6*9,期望结果42,测试信息6*9等于42
parametrize.py:3 (test_eval[6*9-42-6*9\u7b49\u4e8e42])
54 != 42
预期:42
实际:54
<点击以查看差异>
test_input = '6*9', expected = 42, msg = '6*9等于42'
@pytest.mark.parametrize("test_input,expected,msg", [("3+5", 8, "3+5等于8"), ("2+4", 6, "2+4等于6"), ("6*9", 42, "6*9等于42")])
def test_eval(test_input, expected, msg):
print(f"测试数据{test_input},期望结果{expected},测试信息{msg}")
> assert eval(test_input) == expected
E AssertionError: assert 54 == 42
E + where 54 = eval('6*9')
parametrize.py:7: AssertionError
- 方便测试函数对测试属于的获取。
- 方法:parametrize(argnames, argvalues, indirect=False, ids=None, scope=None)
- 常用参数:
- argnames:参数名
- argvalues:参数对应值,类型必须为list
- 常用参数:
- 方法:parametrize(argnames, argvalues, indirect=False, ids=None, scope=None)
- 当参数为一个时格式:[value]
- 当参数个数大于一个时,格式为:[(param_value1,param_value2…),(param_value1,param_value2…)]
- 使用方法:
- @pytest.mark.parametrize(argnames,argvalues)
- ️ 参数值为N个,测试方法就会运行N次
- “笛卡尔积”,多个参数化装饰器
- 一个函数或一个类可以装饰多个 @pytest.mark.parametrize
- 这种方式,最终生成的用例数是 nm,比如上面的代码就是:参数a的数据有 3 个,参数b的数据有 2 个,所以最终的用例数有 32=6 条
- 当参数化装饰器有很多个的时候,用例数都等于 nnnn…
import pytest
# 笛卡尔积,组合数据
data_1 = [1, 2, 3]
data_2 = ['a', 'b']
@pytest.mark.parametrize('a', data_1)
@pytest.mark.parametrize('b', data_2)
def test_parametrize_1(a, b):
print(f'笛卡尔积 测试数据为 : {a},{b}')
============================= test session starts =============================
collecting ... collected 6 items
demo10.py::test_parametrize_1[a-1] PASSED [ 16%]笛卡尔积 测试数据为 : 1,a
demo10.py::test_parametrize_1[a-2] PASSED [ 33%]笛卡尔积 测试数据为 : 2,a
demo10.py::test_parametrize_1[a-3] PASSED [ 50%]笛卡尔积 测试数据为 : 3,a
demo10.py::test_parametrize_1[b-1] PASSED [ 66%]笛卡尔积 测试数据为 : 1,b
demo10.py::test_parametrize_1[b-2] PASSED [ 83%]笛卡尔积 测试数据为 : 2,b
demo10.py::test_parametrize_1[b-3] PASSED [100%]笛卡尔积 测试数据为 : 3,b
============================== 6 passed in 0.03s ==============================
- 带标记的参数化测试
# 标记参数化
@pytest.mark.parametrize("test_input,expected", [
("3+5", 8),
("2+4", 6),
pytest.param("6 * 9", 42, marks=pytest.mark.xfail),
pytest.param("6*6", 42, marks=pytest.mark.skip)
])
def test_mark(test_input, expected):
assert eval(test_input) == expected
pytest.param():为单组数据添加标记,支持多个标记(如marks=[xfail, slow]);xfail:标记 “预期失败”,实际失败时显示 XFAIL(不影响结果),意外通过时显示 XPASS(可加strict=True让 XPASS 转为失败);skip:直接跳过用例执行,适用于无需测试的场景(如环境不支持)。pytest.param():支持给单组数据添加单个 / 多个标记,多标记用列表包裹(如marks=[xfail, smoke]);- 自定义标记需在
pytest.ini中注册(markers = smoke: 冒烟测试用例),否则运行会报警告; skipif的条件建议添加reason,执行报告中会显示跳过原因,提升可维护性。
- 常见的参数化标记
| **标记 ** | **核心作用 ** |
|---|---|
pytest.mark.skip |
强制跳过当前参数组对应的用例,适用于无需测试的场景(如功能下线)。 |
pytest.mark.skipif(条件, reason="说明") |
条件跳过:满足条件时跳过用例,reason 必填(说明跳过原因,便于排查)。 |
pytest.mark.xfail |
预期失败:用例执行失败时标记为 XFAIL(不影响整体测试结果),通过则为 XPASS。 |
pytest.mark.xfail(strict=True) |
严格模式:XPASS(意外通过)会转为 FAILED,强制校验预期失败的用例。 |
pytest.mark.自定义标记(如smoke) |
分类标记:将参数组归类(如冒烟 / 核心用例),可通过 pytest -m smoke执行。 |
七、失败重试
- 安装依赖
pip install pytest-rerunfailures
- 重试机制对比
| **机制 ** | **命令 ** | **执行时机 ** | **适用场景 ** |
|---|---|---|---|
| **失败立即重试 ** | <font style="color:rgb(79, 79, 79);">--reruns=n |
用例失败后立即重试n次 | 网络抖动、偶发失败 |
| **失败后重新运行 ** | <font style="color:rgb(79, 79, 79);">--lf |
所有用例执行完成后,重新运行失败的 | 环境问题修复后 |
- 示例
- 命令行参数(推荐)
if __name__ == "__main__":
# 失败立即重试2次,每次间隔1秒
pytest.main(['-v', '--reruns=2', '--reruns-delay=1', 'test_file.py'])
- 特点用例标记
import pytest
import random
@pytest.mark.flaky(reruns=3, reruns_delay=2) # 失败重试3次,间隔2秒
def test_flaky_example():
"""这个测试可能偶发失败,需要重试"""
assert random.choice([True, False]) # 50%概率失败
- 多轮执行
if __name__ == "__main__":
# 第一轮:执行所有测试
pytest.main(['-v', 'test_suite.py'])
# 第二轮:只执行上一轮失败的用例
pytest.main(['-v', '--lf', 'test_suite.py'])
- 需要注意的是:
# 可能导致性能问题
# 不推荐:如果全部成功,会重复执行所有用例
pytest.main(['-s', 'test_all.py'])
pytest.main(['-s', '--lf', 'test_all.py']) # 可能重复执行
# 推荐:使用reruns避免重复执行
pytest.main(['-s', '--reruns=2', 'test_all.py'])
- 优先使用 --reruns 处理偶发失败
- --lf 适合在修复环境问题后使用
- 重试次数不宜过多(通常1-3次)
- 合理设置延时,避免给系统造成压力
- 参考示例
import pytest
class TestExample:
@pytest.mark.flaky(reruns=2, reruns_delay=1)
def test_network_request(self):
"""网络请求测试,失败重试2次"""
# 模拟网络请求代码
assert make_network_request() == "success"
def test_normal_case(self):
"""普通测试,不需要重试"""
assert 1 + 1 == 2
if __name__ == "__main__":
# 执行所有测试,失败重试1次
pytest.main(['-v', '--reruns=1', 'test_example.py'])
八、pytest断点调试
import pdb; pdb.set_trace()
1. 核心功能
import pdb; pdb.set_trace() 是Python内置调试器(pdb)的断点语句,在pytest测试用例中插入该语句,执行到此处时会暂停用例运行,进入交互式调试环境,可逐行执行代码、查看变量值、排查用例逻辑错误(适配所有pytest用例场景,包括参数化、fixture调用等)。
2. 示例代码(调试参数化测试用例)
import pytest
import pdb
@pytest.mark.parametrize("test_input,expected", [
("3+5", 8),
("6 * 9", 42, marks=pytest.mark.xfail)
])
def test_mark(test_input, expected):
# 插入pdb断点:执行到该行时暂停,进入调试模式
import pdb; pdb.set_trace()
result = eval(test_input) # 计算表达式结果
assert result == expected # 断言结果是否符合预期
3. 执行与调试步骤
- 运行命令:
pytest 测试文件.py -s(必须加-s,否则pytest会捕获输入,无法和pdb交互); - 执行到断点时,终端出现
(Pdb)提示符,进入交互式调试; - 调试完成后,输入
c继续执行/q退出调试。
4. 常用pdb调试命令(核心)
| 命令 | 简写 | 作用 |
|---|---|---|
next |
n |
单步执行下一行(不进入函数/方法内部,适合快速跳过无关代码) |
step |
s |
单步执行下一行(遇到函数/eval等会进入内部,适合调试表达式/函数逻辑) |
print 变量名 |
p |
打印变量值(如p test_input、p result,排查参数/结果是否符合预期) |
continue |
c |
继续执行代码,直到下一个断点(若无则执行完当前用例) |
list |
l |
查看断点附近的代码上下文(便于定位当前执行位置) |
quit |
q |
退出pdb调试,终止当前用例执行(后续用例也会停止) |
help |
h |
查看所有pdb命令的帮助信息(忘记命令时使用) |
5. pytest中使用pdb的关键注意点
- 运行参数:必须带
-s(禁用pytest的输出捕获),否则无法输入pdb命令; - 参数化用例:每个参数组执行到断点都会暂停,需逐个调试(可先通过
-k筛选单个用例调试); - 简洁写法(Python3.7+):
breakpoint()可直接替代import pdb; pdb.set_trace(),无需手动导入pdb; - 清理断点:调试完成后,务必删除/注释断点语句,避免影响用例正常执行。
九、pytest获取用例执行性能数据
1. 核心方式
获取pytest用例执行耗时/性能数据有3类核心方式,适配不同场景(从轻量排查到精细化分析):
- 原生
--durations参数(无插件,快速看慢用例); pytest-benchmark插件(精细化性能统计+阈值校验);- 自定义fixture(灵活统计/输出耗时)。
2. 内置--durations参数
pytest原生参数,运行后输出指定数量“最慢用例”的耗时(含用例执行、fixture初始化/销毁耗时),适合快速排查慢用例。
# 输出所有用例耗时(按耗时降序)
pytest 测试文件.py -v --durations=0
# 输出最慢的5个用例耗时
pytest 测试文件.py -v --durations=5
========================= slowest 2 durations =========================
0.85s call test_perf.py::test_slow_api # 用例执行耗时
0.12s setup test_perf.py::test_slow_api # fixture初始化耗时
3. pytest-benchmark插件(精细化性能分析)
专业性能测试插件,统计用例/函数的执行时间(平均值、中位数、标准差等),支持性能阈值校验(超过阈值则用例失败)。
- 安装插件:
pip install pytest-benchmark; - 示例代码(含阈值校验):
import pytest
# 测试函数执行性能,设置耗时阈值
def test_benchmark_func(benchmark):
# 包裹要测试性能的代码(如函数/接口调用)
result = benchmark(lambda: sum(range(100000)))
# 设置最大耗时阈值:5微秒(超过则用例失败)
benchmark.max_time = 5e-6
assert result == 4999950000
- 运行命令:
pytest 测试文件.py -v; - 核心输出(关键指标):
-------------------------------- benchmark: 1 tests --------------------------------
Name (time in us) Mean StdDev Max
---------------------------------------------------
test_benchmark_func 4.25 0.12 4.50
---------------------------------------------------
4. 自定义fixture统计(灵活适配业务)
通过fixture记录用例开始/结束时间,计算耗时,可自定义输出格式(打印、写入日志/文件)。
- 示例代码(自动统计所有用例耗时):
import pytest
import time
# autouse=True:所有用例自动执行该fixture
@pytest.fixture(autouse=True)
def record_perf():
start = time.time() # 记录开始时间
yield # 执行测试用例
# 用例执行后计算耗时并打印
elapsed = time.time() - start
print(f"\n【性能数据】用例耗时:{elapsed:.4f} 秒")
# 测试用例(模拟耗时操作)
def test_custom_perf():
time.sleep(0.2)
assert 1 == 1
- 运行命令(需加-s显示打印内容):
pytest 测试文件.py -s
- 输出示例:
test_custom_perf
【性能数据】用例耗时:0.2005 秒
PASSED
5. 注意点
- 所有方式若需显示耗时/打印内容,需加
-s参数禁用pytest的输出捕获; --durations适合日常排查,pytest-benchmark适合性能测试,自定义fixture适合业务定制化统计;pytest-benchmark的时间单位支持自动转换(us/ms/s),无需手动换算。
十、生成Allure测试报告
Allure的安装配置建议在网上检索最新的教程进行,此处略过。安装完成后记得安装allure-pytest插件
1. 配置pytest.ini
文件其他配置见前文“运行方式”章节的第三点“【常用】pytest.ini配置文件方式运行”
# --alluredir=./temps:设置allure生成临时的json格式的报告存放的路径
# --clean-alluredir:清空上一次运行的记录
addopts = -vs --alluredir=./temps --clean-alluredir
**pytest测试框架的主函数入口配置 **
| **参数 ** | **作用 ** |
|---|---|
| generte | 生成报告 |
| temps | allure生成临时的json格式的报告存放的路径 |
| -o | 生成allure报告的目录 |
| report | 生成allure报告存放的目录 |
| -c,–clean | 清空 |
import os
import pytest
# 运行pytest测试框架的主函数
if __name__ == '__main__':
pytest.main()
# 调用allure生成报告
os.system("allure generate ./temps -o ./report --clean")
- 也可以进行一些封装
def generate_report(results_dir, report_dir, clean=True):
"""生成Allure报告"""
print("=" * 50)
print("开始生成Allure测试报告")
print("=" * 50)
# 清理之前的报告
if clean and os.path.exists(report_dir):
shutil.rmtree(report_dir)
print(f"清理旧报告目录: {report_dir}")
# 检查测试结果是否存在
if not os.path.exists(results_dir) or not os.listdir(results_dir):
print(f"错误: 测试结果目录 {results_dir} 不存在或为空")
return False
# 生成报告
generate_cmd = f"allure generate {results_dir} -o {report_dir} --clean"
if run_command(generate_cmd):
print(f"报告生成成功: {report_dir}")
return True
else:
print("报告生成失败")
return False
**<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">注意 ****:需要通过pytest主函数执行才会生成allure的html测试报告 **
**查看allure生成的html测试报告 **
allure serve [存放报告的目录]
例如:
allure serve reports/allure-results
2. 修改报告语言

3. 修改Allure报告的logo
把下面的名称复制到allure安装目录下的config目录下的allure.yml文件中
- custom-logo-plugin


- 看到这里原本的logo的位置有变化或者消失了就是成功了

- 将准备好的log图片放到allure安装目录下的plugins\custom-logo-plugin\static定制log的插件路径下

- 修改allure安装目录下的plugins\custom-logo-plugin\static定制log的插件路径下的styles.css文件
.side-nav__brand {
background: url('mmexport1763915912689.png') no-repeat left center !important;
margin-left: 10px;
}
**通过修改这两个参数来调整log图片的大小和位置:
**<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">margin-left: 10px; **和 ****<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">height: 90px; **
重新运行pytest生成allure报告

4. 报告模块内容整理
本章大部分使用原文的原图,来源见开头的“参考文章”
1) 对测试项目、模块、测试点整理
(1)项目名称:@allure.epic()
(2)模块名称(特性):@allure.feature()
(3)接口名称(测试点)(分组):@allure.story()
(4)用例标题:有两种方式(两种效果是一样的,方式二更加灵活)
- 方式一:@allure.title()
- 方式二:allure.dynamic.title()
方式一:
import allure
@allure.epic("项目名称-智考1.0")
@allure.feature("模块-用户管理模块")
class TestFirstClass():
@allure.story("用户登陆接口")
@allure.title("用例名称-验证成功登陆")
def test_login(self):
print("登陆")
@allure.story("用户注册接口")
@allure.title("用例名称-验证成功注册")
def test_register(self):
print("注册")
@allure.story("添加用户")
@allure.title("用例名称-验证成功添加用户")
def test_add_user(self):
print("添加用户")

方式二:
import allure
@allure.epic("项目名称-智考1.0")
@allure.feature("模块-用户管理模块")
class TestFirstClass():
@allure.story("用户登陆接口")
def test_login(self):
allure.dynamic.title("用例名称-验证成功登陆")
print("登陆")
@allure.story("用户注册接口")
def test_register(self):
allure.dynamic.title("用例名称-验证成功注册")
print("注册")
@allure.story("添加用户")
def test_add_user(self):
allure.dynamic.title("用例名称-验证成功添加用户")
print("添加用户")
- 效果是一样的
- 注意每次修改都要重新生成报告
2) 设置测试点优先级
一般情况下如果没有指定默认是normal
- BLOCKER:致命的(@allure.severity(allure.severity_level.BLOCKER))
- CRITICAL:严重的(@allure.severity(allure.severity_level.CRITICAL))
- NORMAL:正常的(@allure.severity(allure.severity_level.NORMAL))
- MINOR:轻微的(@allure.severity(allure.severity_level.MINOR))
- TRIVIAL:不重要的(@allure.severity(allure.severity_level.TRIVIAL))
import allure
@allure.epic("项目名称-智考1.0")
@allure.feature("模块-用户管理模块")
class TestFirstClass():
@allure.story("用户登陆接口")
@allure.severity(allure.severity_level.BLOCKER)
def test_login(self):
allure.dynamic.title("用例名称-验证成功登陆")
print("登陆")
@allure.story("用户注册接口")
@allure.severity(allure.severity_level.CRITICAL)
def test_register(self):
allure.dynamic.title("用例名称-验证成功注册")
print("注册")
@allure.story("添加用户")
@allure.severity(allure.severity_level.NORMAL)
def test_add_user(self):
allure.dynamic.title("用例名称-验证成功添加用户")
print("添加用户")
@allure.story("删除用户")
@allure.severity(allure.severity_level.MINOR)
def test_delete_user(self):
allure.dynamic.title("用例名称-验证成功删除用户")
print("删除用户")
@allure.story("修改用户")
@allure.severity(allure.severity_level.TRIVIAL)
def test_update_user(self):
allure.dynamic.title("用例名称-验证成功修改用户")
print("修改用户")

3) 增加测试用例描述
**测试用例的描述定制:有两种方式 **
- 方式一:
<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">@allure.description() - 方式二:
<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">allure.dynamic.description()
使用方法同上,不在赘述
4) 增加用例链接
**<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">@allure.link() ****:接口访问链接 ****<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">@allure.issue() ****:bug链接 ****<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">@allure.testcase() ****:测试用例链接 **
使用方法同上,不在赘述

5) 增加测试步骤描述
**测试步骤定制:两种方式( **<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">常用第二种 **) **
- 方式一:
<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">@allure.step() - 方式二:
<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">with allure.step():
使用方法同上,不在赘述。仅展示第二种,使用更灵活
import allure
@allure.epic("项目名称-智考1.0")
@allure.feature("模块-用户管理模块")
class TestFirstClass():
@allure.story("用户登陆接口")
@allure.severity(allure.severity_level.BLOCKER)
@allure.link("接口访问链接")
@allure.issue("bug链接")
@allure.testcase("测试用例链接")
def test_login(self):
allure.dynamic.title("用例名称-验证成功登陆")
allure.dynamic.description("这是验证登陆是否成功")
with allure.step("第一步:输入用户名"):
print("输入用户名")
with allure.step("第二步:输入密码"):
print("输入密码")
with allure.step("第三步:点击登陆"):
print("点击登陆")
print("登陆")
6) 增加显示错误附件(截图)
import allure
@allure.epic("项目名称-智考1.0")
@allure.feature("模块-用户管理模块")
class TestFirstClass():
@allure.story("用户登陆接口")
@allure.severity(allure.severity_level.BLOCKER)
@allure.link("接口访问链接")
@allure.issue("bug链接")
@allure.testcase("测试用例链接")
def test_login(self):
allure.dynamic.title("用例名称-验证成功登陆")
allure.dynamic.description("这是验证登陆是否成功")
print("登陆")
# 测试步骤
for i in range(1, 6):
with allure.step("第" + str(i) + "步"):
pass
# 错误截图
with open("D:\\error.png", mode="rb") as f:
result = f.read()
allure.attach(body=result, name="错误截图", attachment_type=allure.attachment_type.PNG)

7) 增加测试步骤描述
**<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">allure.attach("文本内容", name="文本名称", attachment_type=allure.attachment_type.TEXT) **
import allure
@allure.epic("项目名称-智考1.0")
@allure.feature("模块-用户管理模块")
class TestFirstClass():
@allure.story("用户登陆接口")
@allure.severity(allure.severity_level.BLOCKER)
@allure.link("接口访问链接")
@allure.issue("bug链接")
@allure.testcase("测试用例链接")
def test_login(self):
allure.dynamic.title("用例名称-验证成功登陆")
allure.dynamic.description("这是验证登陆是否成功")
print("登陆")
# 测试步骤
for i in range(1, 6):
with allure.step("第" + str(i) + "步"):
pass
# 错误截图
with open("D:\\error.png", mode="rb") as f:
result = f.read()
allure.attach(body=result, name="错误截图", attachment_type=allure.attachment_type.PNG)
# 接口自动化:文本
# 请求四要素
allure.attach("接口地址:https://www.baidu.com", name="文本1", attachment_type=allure.attachment_type.TEXT)
allure.attach("接口参数:{一般从yaml中获取}", name="文本2", attachment_type=allure.attachment_type.TEXT)
allure.attach("接口请求方式:get", name="文本3", attachment_type=allure.attachment_type.TEXT)
allure.attach("请求头:{一般从yaml中获取}", name="文本4", attachment_type=allure.attachment_type.TEXT)
# 响应内容
allure.attach("响应文本:{一般从yaml中获取}", name="文本5", attachment_type=allure.attachment_type.TEXT)
allure.attach("接口执行结果:成功/失败", name="文本6", attachment_type=allure.attachment_type.TEXT)

十一、pytest中的日志管理
pytest的日志管理基于Python内置的logging模块,可通过配置实现日志分级、多目标输出(控制台/文件)、格式定制等,适配测试过程记录、问题排查、结果归档等场景。以下是系统配置方法与核心功能实现:
1. 基础日志配置(全局生效)
通过conftest.py或pytest.ini配置全局日志规则,确保所有测试用例、fixture共享同一套日志设置。
1) 核心配置项
| 配置项 | 作用 |
|---|---|
level |
日志级别(DEBUG < INFO < WARNING < ERROR < CRITICAL),低于该级别的日志不输出 |
format |
日志格式(可包含时间、模块、级别、内容等) |
handlers |
日志输出目标(控制台StreamHandler、文件FileHandler、轮转文件RotatingFileHandler等) |
2) 配置示例(conftest.py)
import logging
import os
from logging.handlers import RotatingFileHandler
import pytest
def setup_logging():
"""初始化全局日志配置"""
# 1. 创建日志器(logger)
logger = logging.getLogger("pytest_logger") # 自定义logger名称,避免与其他模块冲突
logger.setLevel(logging.DEBUG) # 全局日志级别(最低级别, handlers可设置更高级别)
logger.handlers.clear() # 清除默认handler,避免重复输出
# 2. 定义日志格式(时间-模块-级别-内容)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(module)s:%(lineno)d - %(levelname)s - %(message)s"
)
# 3. 控制台输出handler(仅输出INFO及以上级别)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO) # 控制台只看重要信息
console_handler.setFormatter(formatter)
# 4. 文件输出handler(输出所有级别,按大小轮转)
log_dir = "./logs"
os.makedirs(log_dir, exist_ok=True)
file_handler = RotatingFileHandler(
f"{log_dir}/pytest.log",
maxBytes=5*1024*1024, # 单个文件最大5MB
backupCount=3, # 保留3个备份
encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG) # 文件记录详细日志(含DEBUG)
file_handler.setFormatter(formatter)
# 5. 添加handler到logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)
return logger
# 全局logger,所有用例可直接导入使用
logger = setup_logging()
# pytest会话启动时确认日志配置
def pytest_sessionstart(session):
logger.info("=== 测试会话开始 ===")
2. 在测试用例中使用日志
通过导入全局logger,在测试用例、fixture中记录关键操作、参数、结果等信息。
1) 示例:用例与fixture中的日志记录
import pytest
from conftest import logger # 导入全局logger
@pytest.fixture
def login_fixture():
logger.debug("fixture:开始准备登录环境") # DEBUG级:详细调试信息
username = "test_user"
logger.info(f"fixture:使用账号[{username}]登录") # INFO级:关键操作
yield username
logger.info("fixture:登录环境清理完成")
def test_order(login_fixture):
username = login_fixture
logger.info(f"用例[{test_order.__name__}]:用户[{username}]开始下单")
try:
# 模拟下单操作
order_id = "ORD12345"
logger.debug(f"用例:生成订单ID[{order_id}]")
assert order_id.startswith("ORD")
logger.info(f"用例[{test_order.__name__}]:下单成功")
except AssertionError as e:
logger.error(f"用例[{test_order.__name__}]:下单失败,错误:{str(e)}", exc_info=True) # ERROR级:记录异常
raise # 重新抛出异常,不影响pytest的用例结果统计
3. 结合pytest参数与配置文件
通过命令行参数或pytest.ini,动态调整日志级别、格式,无需修改代码。
1) 命令行参数(临时调整)
# 调整全局日志级别为DEBUG(覆盖代码中的设置)
pytest test_demo.py -s --log-level=DEBUG
# 自定义控制台日志格式
pytest test_demo.py -s --log-format="%(asctime)s %(levelname)s: %(message)s"
# 输出日志到指定文件(替代代码中的file_handler)
pytest test_demo.py -s --log-file=./logs/cli_log.log
2) pytest.ini配置(固定规则)
[pytest]
# 全局日志级别
log_level = INFO
# 日志格式
log_format = %(asctime)s - %(levelname)s - %(message)s
# 日志文件路径
log_file = ./logs/pytest_ini.log
# 日志文件编码
log_file_encoding = utf-8
4. 用例执行状态日志
通过钩子函数pytest_runtest_makereport,自动记录所有用例的执行结果(成功/失败/跳过)到日志。
# conftest.py 中添加
import pytest
from conftest import logger
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""自动记录用例执行状态到日志"""
outcome = yield
rep = outcome.get_result()
# 只记录用例执行阶段(call)的结果
if rep.when == "call":
case_name = item.nodeid # 用例唯一标识(文件::类::方法)
if rep.passed:
logger.info(f"用例[{case_name}]:执行成功")
elif rep.failed:
logger.error(f"用例[{case_name}]:执行失败,原因:{rep.longreprtext}")
elif rep.skipped:
logger.warning(f"用例[{case_name}]:被跳过,原因:{rep.longreprtext}")
5. 注意事项
- 日志级别设计:
DEBUG:开发调试用(如参数值、中间结果);INFO:核心流程记录(如用例开始/结束、关键操作);WARNING:非致命问题(如超时重试);ERROR:用例失败、异常(需记录详细堆栈,exc_info=True)。
- 避免重复日志:
- 初始化logger时用
logger.handlers.clear()清除默认handler; - 自定义logger名称(如
"pytest_logger"),避免与其他库的logger冲突。
- 初始化logger时用
- 与Allure报告集成:
用allure.attach将关键日志附加到报告(如失败时的ERROR日志):
import allure
# 用例失败时,将日志附加到Allure报告
allure.attach(logger.handlers[1].stream.getvalue(), name="执行日志", attachment_type=allure.attachment_type.TEXT)
当然我们也可以尝试按模块记录日志

一篇关于pytest框架的详细学习、使用说明,包含集成Allure报告和自动化测试应用的方法。
浙公网安备 33010602011771号