PyTest框架详解、集成Allure报告及在自动化测试中的应用

零、参考文章

本篇在下面文章基础上修改、补充和整理,感谢各文章原作者大佬的撰写:

pytest框架-详解(学习pytest框架这一篇就够了)-CSDN博客

自动化测试基础——allure下载安装及配置及pytest + allure-pytest插件生成allure企业级测试报告及企业级定制-CSDN博客

Pytest测试框架最常用的13个插件_pytest常用插件-CSDN博客

【pytest】二、pytest之全局配置文件pytest.ini,及命令执行参数详解-CSDN博客

官方文档:

pytest documentation

pytest 文档 - pytest 测试框架

一、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)
  1. scope :控制 fixture 的复用范围(默认 function,可选 class/module/session);
  2. params :传入可迭代对象实现 fixture 参数化,测试用例会自动多轮执行;
  3. autouse :是否自动调用 fixture(默认 False,True 则作用域内用例无需手动传参);
  4. ids :配合 params 给每个参数值设标识,优化测试结果可读性;
  5. 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
  • 当参数为一个时格式:[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
  1. pytest.param() :为单组数据添加标记,支持多个标记(如 marks=[xfail, slow] );
  2. xfail :标记 “预期失败”,实际失败时显示 XFAIL(不影响结果),意外通过时显示 XPASS(可加 strict=True 让 XPASS 转为失败);
  3. skip :直接跳过用例执行,适用于无需测试的场景(如环境不支持)。
  4. pytest.param() :支持给单组数据添加单个 / 多个标记,多标记用列表包裹(如 marks=[xfail, smoke] );
  5. 自定义标记需在 pytest.ini 中注册( markers = smoke: 冒烟测试用例 ),否则运行会报警告;
  6. 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'])
  1. 优先使用 --reruns 处理偶发失败
  2. --lf 适合在修复环境问题后使用
  3. 重试次数不宜过多(通常1-3次)
  4. 合理设置延时,避免给系统造成压力
  • 参考示例
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. 执行与调试步骤

  1. 运行命令:pytest 测试文件.py -s必须加-s,否则pytest会捕获输入,无法和pdb交互);
  2. 执行到断点时,终端出现(Pdb)提示符,进入交互式调试;
  3. 调试完成后,输入c继续执行/q退出调试。

4. 常用pdb调试命令(核心)

命令 简写 作用
next n 单步执行下一行(不进入函数/方法内部,适合快速跳过无关代码)
step s 单步执行下一行(遇到函数/eval等会进入内部,适合调试表达式/函数逻辑)
print 变量名 p 打印变量值(如p test_inputp result,排查参数/结果是否符合预期)
continue c 继续执行代码,直到下一个断点(若无则执行完当前用例)
list l 查看断点附近的代码上下文(便于定位当前执行位置)
quit q 退出pdb调试,终止当前用例执行(后续用例也会停止)
help h 查看所有pdb命令的帮助信息(忘记命令时使用)

5. pytest中使用pdb的关键注意点

  1. 运行参数:必须带-s(禁用pytest的输出捕获),否则无法输入pdb命令;
  2. 参数化用例:每个参数组执行到断点都会暂停,需逐个调试(可先通过-k筛选单个用例调试);
  3. 简洁写法(Python3.7+):breakpoint() 可直接替代import pdb; pdb.set_trace(),无需手动导入pdb;
  4. 清理断点:调试完成后,务必删除/注释断点语句,避免影响用例正常执行。

九、pytest获取用例执行性能数据

1. 核心方式

获取pytest用例执行耗时/性能数据有3类核心方式,适配不同场景(从轻量排查到精细化分析):

  1. 原生--durations参数(无插件,快速看慢用例);
  2. pytest-benchmark插件(精细化性能统计+阈值校验);
  3. 自定义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插件(精细化性能分析)

专业性能测试插件,统计用例/函数的执行时间(平均值、中位数、标准差等),支持性能阈值校验(超过阈值则用例失败)。

  1. 安装插件:pip install pytest-benchmark
  2. 示例代码(含阈值校验):
import pytest

# 测试函数执行性能,设置耗时阈值
def test_benchmark_func(benchmark):
    # 包裹要测试性能的代码(如函数/接口调用)
    result = benchmark(lambda: sum(range(100000)))
    # 设置最大耗时阈值:5微秒(超过则用例失败)
    benchmark.max_time = 5e-6
    assert result == 4999950000
  1. 运行命令:pytest 测试文件.py -v
  2. 核心输出(关键指标):
-------------------------------- 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. 注意点

  1. 所有方式若需显示耗时/打印内容,需加-s参数禁用pytest的输出捕获;
  2. --durations适合日常排查,pytest-benchmark适合性能测试,自定义fixture适合业务定制化统计;
  3. 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. 修改报告语言

把下面的名称复制到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

  1. BLOCKER:致命的(@allure.severity(allure.severity_level.BLOCKER))
  2. CRITICAL:严重的(@allure.severity(allure.severity_level.CRITICAL))
  3. NORMAL:正常的(@allure.severity(allure.severity_level.NORMAL))
  4. MINOR:轻微的(@allure.severity(allure.severity_level.MINOR))
  5. 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.pypytest.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. 注意事项

  1. 日志级别设计
    • DEBUG:开发调试用(如参数值、中间结果);
    • INFO:核心流程记录(如用例开始/结束、关键操作);
    • WARNING:非致命问题(如超时重试);
    • ERROR:用例失败、异常(需记录详细堆栈,exc_info=True)。
  2. 避免重复日志
    • 初始化logger时用logger.handlers.clear()清除默认handler;
    • 自定义logger名称(如"pytest_logger"),避免与其他库的logger冲突。
  3. 与Allure报告集成
    allure.attach将关键日志附加到报告(如失败时的ERROR日志):
import allure
# 用例失败时,将日志附加到Allure报告
allure.attach(logger.handlers[1].stream.getvalue(), name="执行日志", attachment_type=allure.attachment_type.TEXT)

当然我们也可以尝试按模块记录日志

posted @ 2025-12-06 15:41  Falamo寒枝  阅读(169)  评论(0)    收藏  举报