当前位置: 首页 > article >正文

自动化测试之Pytest框架(万字详解)

Pytest测试框架

  • 一、前言
  • 二、安装
    • 2.1 命令行安装
    • 2.2 验证安装
  • 三、pytest设计测试用例注意点
    • 3.1 命名规范
    • 3.2 断言清晰
    • 3.3 fixture
    • 3.4 参数化设置
    • 3.5 测试隔离
    • 3.6 异常处理
    • 3.7 跳过或者预期失败
    • 3.8 mocking
    • 3.9 标记测试
  • 四、以案例初入pytest
    • 4.1 第一个pytest测试
    • 4.2 多个测试分组一个类里面
    • 4.3 将多个测试用例分组
    • 4.4 pytest运行中命令行运行可选参数
  • 五、配置文件pytest.ini
  • 六、conftest文件
    • 6.1 conftest的关键点
    • 6.2 conftest案例
  • 七、mark属性标记
    • 7.1 内置/自定义标记
    • 7.2 @pytest.mark.parametrize:参数化测试函数
  • 八、Fixture装饰器
    • 8.1 基本概念
    • 8.2 Fixture方法解析
    • 8.3 Fixture的创建和使用
    • 8.4 调用Fixture的方式
  • 九 、pytest跳过测试用例方法
    • 9.1 skip -- 跳过测试用例
    • 9.2 skipif -- 有条件跳过测试用例
  • 十、失败重跑
    • 10.1 使用 --last-failed 选项
    • 10.2 使用 --failed-first 选项
    • 10.3 使用 pytest-rerunfailures 插件
  • 十一、pytest执行顺序
  • 十二、hooks(钩子函数)
    • 12.1 钩子函数的四个阶段
      • 12.1.1 配置阶段 (Configuration)
      • 12.2 收集阶段 (Collection)
      • 12.3 运行阶段 (Running)
      • 12.4 总结阶段 (Summary)
    • 12.2 钩子函数关键点
  • 十三、Allure测试报告
    • 13.1 前言
    • 13.2 安装 Allure
    • 13.3 配置 pytest 以使用 Allure
    • 13.4 Allure装饰器函数
    • 13.5 执行自动化用例 生成allure报告所需文件
    • 13.6 查看测试报告的两种方式
      • 13.6.1 直接打开默认浏览器展示报告
      • 13.6.2 从结果生成报告
  • 十四、pytest中管理日志
    • 14.1 日志级别
    • 14.2 使用logging模块
    • 14.3 将日志保存到文件
    • 14.4 控制日志捕获的行为
    • 14.5 示例

一、前言

pytest 是一个功能强大且易于使用的Python测试框架,它允许开发者编写简单或复杂的函数式测试。pytest 的设计理念是让测试过程尽可能的简单和直观,同时提供丰富的插件生态系统来扩展其功能。

  • 介绍:

    • 易用性:pytest 不需要额外的导入语句来标记测试函数(如unittest中的test_前缀),你可以直接使用标准的断言语句(assert)来进行测试,这使得测试代码更加简洁、可读
    • 自动发现测试:pytest 能够自动找到并运行测试。默认情况下,它会查找文件名以test_开头或结尾的模块,以及任何以Test开头的类(无需继承自特定的父类)。在这些模块或类中,它会执行所有以test_开头的方法或函数
    • 参数化测试:pytest 支持参数化测试,这意味着你可以用不同的输入数据多次运行同一个测试函数,而不需要为每个数据点编写单独的测试函数
    • 详细的报告和输出:当测试失败时,pytest 提供了清晰的错误信息和追踪,帮助你快速定位问题所在
    • 丰富的插件生态:pytest 拥有一个活跃的社区和大量的第三方插件,可以用来增强测试的功能,例如集成覆盖率报告、与CI/CD工具对接、支持异步测试等
    • 兼容其他测试框架:pytest 可以运行unittest和nose风格的测试,所以如果你有旧的测试代码,通常可以直接使用pytest来运行它们,而不需要重写
    • 内置的fixture机制:pytest 引入了fixture的概念,这是一个非常强大的特性,用于设置前置条件(比如创建数据库连接、初始化对象等),并且可以在多个测试之间共享
    • 命令行选项:pytest 提供了许多有用的命令行选项,让你能够灵活地控制测试行为,比如选择运行特定的测试、跳过某些测试、根据关键字筛选测试等等

二、安装

2.1 命令行安装

pip install pytest

2.2 验证安装

pytest --version

三、pytest设计测试用例注意点

3.1 命名规范

  • 文件名:文件名要以test_开头或结尾 例如:test_login.py
  • 函数名:函数名要以test_开头,这样子有助与python自动取搜索他
  • 如果使用类来组织测试,类名应以 Test 开头,并且不应继承自任何特定的基类(除非是为了使用某些特性)

3.2 断言清晰

  • 使用 Python 内置的 assert 语句来进行断言。pytest 会提供详细的失败信息,因此尽量让断言语句尽可能直接明了

3.3 fixture

  • fixture是pytest中非常重要的概念,用于设置测试环境,我们要合理的使用fixture,减少代码的重复使用,提高测试效率

3.4 参数化设置

  • 利用 @pytest.mark.parametrize 装饰器可以为同一个测试函数提供多组输入数据,从而避免编写多个类似的测试函数

3.5 测试隔离

  • 使用setup和teardowm来准备测试环境

3.6 异常处理

  • 如果你的测试预期某个操作会抛出异常,可以使用 pytest.raises 上下文管理器来检查是否确实发生了预期的异常

3.7 跳过或者预期失败

  • 对于暂时无法通过的测试,可以使用 @pytest.mark.skip 或 @pytest.mark.xfail 标记,以便在不影响整体测试结果的情况下继续开发

3.8 mocking

  • 当测试需要依赖外部系统(如数据库、网络服务等)时,考虑使用 unittest.mock 或者第三方库如 pytest-mock 来模拟这些依赖,确保测试的快速性和稳定性

3.9 标记测试

  • 使用 @pytest.mark 可以为测试添加标签,比如 slow, network, database 等,然后可以根据这些标签选择性地运行测试

四、以案例初入pytest

4.1 第一个pytest测试

创建一个名为test_demo的文件名,其中有一个函数 一个测试

def func(x):
    return x + 1

def test_answer():
    assert func(3) == 5  # 断言

在命令行输入pytest运行,以下是输出结果

$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item

test_sample.py F                                                     [100%]

================================= FAILURES =================================
_______________________________ test_answer ________________________________

    def test_answer():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

test_sample.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_sample.py::test_answer - assert 4 == 5
============================ 1 failed in 0.12s =============================

4.2 多个测试分组一个类里面

创建一个名为test_demo的文件名,创建一个类 其中有两个函数

class TestClass:
    def test_one(self):
        x = "this"
        assert "h" in x

    def test_two(self):
        x = "hello"
        assert hasattr(x, "check") # 断言x是否具有名为check的属性或方法

在命令行输入pytest运行,以下是输出结果

$ pytest -q test_class.py
.F                                                                   [100%]
================================= FAILURES =================================
____________________________ TestClass.test_two ____________________________

self = <test_class.TestClass object at 0xdeadbeef0001>

    def test_two(self):
        x = "hello"
>       assert hasattr(x, "check")
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

test_class.py:8: AssertionError
========================= short test summary info ==========================
FAILED test_class.py::TestClass::test_two - AssertionError: assert False
1 failed, 1 passed in 0.12s

其中第一条是成功 第二条是失败 失败原因就是在x不具有check的属性 观察失败原因主要看断言中的中间值

4.3 将多个测试用例分组

好处:

测试组织
仅在特定类中共享用于测试的装置
在班级层面上应用标记,并让它们隐式地应用于所有测试

class TestClassDemoInstance:
    value = 0

    def test_one(self):
        self.value = 1
        assert self.value == 1

    def test_two(self):
        assert self.value == 1
$ pytest -k TestClassDemoInstance -q
.F                                                                   [100%]
================================= FAILURES =================================
______________________ TestClassDemoInstance.test_two ______________________

self = <test_class_demo.TestClassDemoInstance object at 0xdeadbeef0002>

    def test_two(self):
>       assert self.value == 1
E       assert 0 == 1
E        +  where 0 = <test_class_demo.TestClassDemoInstance object at 0xdeadbeef0002>.value

test_class_demo.py:9: AssertionError
========================= short test summary info ==========================
FAILED test_class_demo.py::TestClassDemoInstance::test_two - assert 0 == 1
1 failed, 1 passed in 0.12s

4.4 pytest运行中命令行运行可选参数

参数功能
-v增加输出的详细程度
-q减少输出信息
-k EXPRESSION根据表达式选择运行哪些测试,例如 -k ‘not slow’ 可以跳过标记为 slow 的测试
-x遇到第一个失败就退出
–html=REPORT.html生成HTML格式的测试报告,需要安装 pytest-html 插件
–maxfail=NUM在达到指定数量的失败后停止测试
-m MARKEXPR只运行带有指定标记的测试,例如 -m slow
-n NUM 或 --numprocesses=NUM使用多个进程并行运行测试,需要安装 pytest-xdist 插件
-s不捕获标准输出和错误输出,允许直接看到 print 调用的结果
–ignore=path忽略指定路径下的测试文件

五、配置文件pytest.ini

pytest.ini 文件是 pytest 的配置文件之一,用于定义项目的全局设置和选项。通过这个文件,你可以定制化测试行为,指定插件、命令行选项以及其他配置项,而无需每次都手动在命令行中输入这些参数

[pytest]
# 基本配置选项
addopts = -ra -q --tb=short
testpaths = tests/
markers =
    slow: marks tests as slow (deselect with '-m "not slow"')
    serial: marks tests that should run in serial
python_files = test_*.py *_test.py
python_classes = Test* *Tests
python_functions = test_*

# 插件配置
plugins = myplugin, otherplugin

# 环境变量
env =
     ENV_VAR=value1
     OTHER_ENV_VAR=value2

# 代码覆盖率配置(需要安装 pytest-cov)
addopts += --cov=myproject --cov-report=term-missing

# 并行测试配置(需要安装 pytest-xdist)
addopts += -n auto

# 设置默认的编码为 utf-8
console_output_encoding = utf-8
file_system_encoding = utf-8

# 设置收集器忽略某些路径
norecursedirs = .git .tox dist build

# 自定义日志格式(需要安装 pytest-log-clerk 或类似插件)
log_cli = True
log_cli_level = INFO
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
log_cli_date_format = %Y-%m-%d %H:%M:%S

关键配置项解释

  • addopts:添加默认的命令行选项。这可以包含任何有效的 pytest 命令行参数。例如,-ra 表示显示所有错误摘要,–tb=short 设置回溯输出风格
  • testpaths:指定要搜索测试的目录,默认情况下 pytest 会递归搜索当前目录及其子目录中的所有匹配文件
  • markers:定义自定义标记的帮助信息,使得其他开发者更容易理解标记的意义
  • python_files:指定哪些文件名模式被视为测试文件
  • python_classes:指定哪些类名模式被视为测试类
  • python_functions:指定哪些函数名模式被视为测试函数
  • plugins:加载额外的插件。通常不需要显式声明,因为大多数插件会自动注册
  • norecursedirs:排除不希望递归搜索的目录
  • log_cli 和相关日志配置:控制命令行日志输出的行为(需要适当的插件支持)
  • coverage 和并行测试配置:可以通过 addopts 添加与 pytest-cov 或 pytest-xdist 相关的选项

六、conftest文件

conftest.py 文件是 Pytest 框架中的一个特殊文件,用于包含 fixture(固定装置)和其他配置代码。Pytest 会自动加载名为 conftest.py 的文件中定义的 fixtures 和插件,而不需要在测试模块中显式导入它们。这个文件通常用来存放那些被多个测试文件共享的配置和设置

6.1 conftest的关键点

  • 位置:conftest.py 文件应当放置在你的测试文件所在的目录或其父目录中。Pytest 会递归地查找这些文件
  • 作用域:定义在 conftest.py 中的 fixture 可以被该文件所在目录及其子目录下的所有测试文件使用
  • 内容:可以包含 fixtures、hooks(钩子函数)和其他配置选项。它不应该包含实际的测试代码
  • 命名:文件名必须严格为 conftest.py,否则 Pytest 将不会识别它
  • 初始化代码:如果需要执行一些一次性的初始化代码(比如设置日志记录、数据库连接等),可以在 conftest.py 中定义

6.2 conftest案例

例如,在 conftest.py 中定义一个 fixture,用来作为登录模块用例的前置操作

import pytest
from seleium import webdirver

@pytest.fixture(scope='class')
def login():
	driver = webdriver.Chrome()
	driver.get('http://127.0.0.1')
	driver.maximzie_window()
	driver.implicitly_wait(10)
	yield driver
	driver quit()

七、mark属性标记

通过使用pytest.mark帮助程序,您可以轻松地在测试函数上设置元数据

7.1 内置/自定义标记

  • usefixtures - 在测试函数或类上使用fixture

  • filterwarnings-过滤测试函数的某些警告

  • skip-总是跳过测试函数

  • skipif - 如果满足某个条件,则跳过测试函数

  • xfail - 如果满足某个条件,则产生“预期失败”结果

  • 参数化-对同一个测试函数执行多次调用

自定义标记

自定义标记就如上述pytest.ini文件,自定义标记

[pytest]
markers =
    slow: marks tests as slow (deselect with '-m "not slow"')
    serial

7.2 @pytest.mark.parametrize:参数化测试函数

pytest.mark.parametrize 是 pytest 框架提供的一个装饰器,用于参数化测试函数。它允许你定义多个参数集,然后针对每个参数集运行测试函数,这样可以有效地减少代码重复,并且使得测试更加灵活和易于维护

使用 @pytest.mark.parametrize 装饰器时,你需要提供两个参数:

  1. 第一个参数是一个字符串,其中包含逗号分隔的参数名列表
  2. 第二个参数是一个元组列表(或者可迭代的对象),每个元组代表一组测试数据,这些数据会依次传递给测试函数的相应参数

示例

import pytest

def add(x, y):
    return x + y

@pytest.mark.parametrize("x, y, expected", [
    (1, 2, 3),
    (0, 5, 5),
    (-1, -1, -2),
    (3.2, 4.8, 8.0),
])
def test_add(x, y, expected):
    assert add(x, y) == expected

在这里parametrize 装饰器定义了四组x, y, expected元组,以便teat_add依次运行得出四组结果

test_example.py::test_add[1-2-3] PASSED
test_example.py::test_add[0-5-5] PASSED
test_example.py::test_add[-1--1--2] PASSED
test_example.py::test_add[3.2-4.8-8.0] FAILED

八、Fixture装饰器

8.1 基本概念

在 pytest 中,fixture 是一种用于设置测试环境的机制。它们可以用来执行一些前置或后置操作(例如:准备数据、启动服务、清理状态等),并且可以在多个测试之间共享。fixture 的设计使得代码复用和测试之间的依赖关系更加清晰,同时也让测试函数本身保持简洁

8.2 Fixture方法解析

fixture方法:

fixture(callable_or_scope=None, *args, scope="function", params=None, autouse=False, ids=None, name=None)
  • scope:fixture的作用域,默认为function;
  • autouse:默认:False,需要用例手动调用该fixture;如果是True,所有作用域内的测试用例都会自动调用该fixture;
  • name:装饰器的名称,同一模块的fixture相互调用建议写不同的name

作用域(scope):

  • (scope):决定了 fixture 的生命周期
  • function (默认):每个测试函数调用一次
  • class:每个测试类调用一次
  • module:每个模块加载时调用一次
  • session:整个测试会话期间只调用一次

参数化(params):

  • 可以为 fixture 提供参数,类似于参数化测试

自动应用(autouse):

  • 如果一个测试函数需要某个 fixture,pytest 会自动调用它,无需显式地传递

依赖注入(request ):

  • 一个 fixture 可以依赖于另一个 fixture,并通过参数传递来实现这种依赖关系

8.3 Fixture的创建和使用

你可以通过装饰器 @pytest.fixture 来定义一个 fixture 函数。下面是一个简单的例子

import pytest

@pytest.fixture
def sample_data():
    # 前置操作,比如初始化数据
    data = {"value": 42}
    yield data
    # 后置操作,比如清理资源
    print("Cleanup after test")

要使用fixture,只需要将其作为参数传递给测试函数

def test_with_fixture(sample_data):
    assert sample_data["value"] == 42

在这个例子中,sample_data 是一个 fixture 函数,它会在测试 test_with_fixture 运行之前被调用,提供了一个包含特定数据的字典给测试函数。yield 关键字之后的代码是后置操作,在测试完成后执行

8.4 调用Fixture的方式

方式一:直接作为测试函数的参数
这是最常见和推荐的方式。你只需要将 fixture 名称作为参数传递给测试函数或类的方法,pytest 就会自动为你调用该 fixture

import pytest

@pytest.fixture
def sample_data():
    print("Setting up fixture")
    return {"value": 42}

def test_with_fixture(sample_data):
    print(f"Testing with data: {sample_data}")
    assert sample_data["value"] == 42

执行结果

============================= test session starts ==============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /path/to/your/project
collected 1 item

test_example.py::test_with_fixture Setting up fixture
Testing with data: {'value': 42}
PASSED

============================== 1 passed in X.XX seconds ===============================

对于类中的方法,也可以同样地使用

class TestClass:
    def test_method(self, sample_data):
        assert sample_data["value"] == 42

方式二:使用 pytest.mark.usefixtures 标记
如果你不想在每个测试函数中都列出所有的 fixtures,或者你需要为多个测试函数应用同一个 fixture,可以使用 pytest.mark.usefixtures 来标记这些测试函数或整个测试类

import pytest

@pytest.fixture
def setup():
    print("Setup fixture called")
    yield
    print("Teardown fixture called")


@pytest.fixture
def another_setup():
    print("\nAnother setup fixture called")
    yield
    print("Teardown another setup fixture called")


@pytest.mark.usefixtures("setup")
def test_one():
    print("Test one running")

@pytest.mark.usefixtures("setup")
def test_two():
    print("Test two running")

执行结果

============================= test session starts ==============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /path/to/your/project
collected 2 items

test_example.py::test_one Setup fixture called
Test one running
PASSED
Teardown fixture called

test_example.py::test_two Setup fixture called
Test two running
PASSED
Teardown fixture called

============================== 2 passed in X.XX seconds ===============================

你也可以一次性为多个测试函数或整个测试类添加多个 fixtures

@pytest.mark.usefixtures("setup", "another_setup")
class TestClass:
    def test_method(self):
        pass

    def test_another_method(self):
        pass

执行结果

============================= test session starts ==============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /path/to/your/project
collected 2 items

test_example.py::TestClass::test_method 
Setup fixture called

Another setup fixture called

Test method running
PASSED
Teardown another setup fixture called
Teardown setup fixture called

test_example.py::TestClass::test_another_method 
Setup fixture called

Another setup fixture called

Test another method running
PASSED
Teardown another setup fixture called
Teardown setup fixture called

============================== 2 passed in X.XX seconds ===============================

方式三:自动应用 (autouse=True)
当你定义一个 fixture 时,可以通过设置 autouse=True 参数使其自动应用于所有测试函数,而不需要显式地将其作为参数传递或使用 pytest.mark.usefixtures 标记

import pytest

@pytest.fixture(autouse=True)
def always_used_fixture():
    print("This fixture is automatically applied to all tests.")

def test_without_explicit_dependency():
    print("Running a test without explicitly depending on the fixture.")

执行结果

============================= test session starts ==============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /path/to/your/project
collected 1 item

test_example.py::test_with_autouse Setting up autouse fixture
Setting up sample_data fixture
Testing with autouse and sample_data fixture data: {'value': 42}
PASSED

============================== 1 passed in X.XX seconds ===============================

九 、pytest跳过测试用例方法

9.1 skip – 跳过测试用例

  • 您可以标记无法在某些平台上运行或预计会失败的测试功能,以便 pytest 可以相应地处理它们并提供测试会话的摘要,同时保持测试套件为绿色

  • 跳过意味着您仅希望测试在满足某些条件时才能通过,否则 pytest 应完全跳过运行测试。常见示例是在非 Windows 平台上跳过仅限 Windows 的测试,或跳过依赖于当前不可用的外部资源(例如数据库)的测试

使用 pytest.mark.skip 装饰器

pytest.mark.skip() 通常用于在定义测试函数时标记该函数应该被跳过,而不是在函数内部使用
如果你想在定义测试函数时提供跳过的原因,可以使用带有 reason 参数的 pytest.mark.skip 装饰器

import pytest

@pytest.mark.skip(reason="This test is skipped because it's not ready yet.")
def test_skip_with_reason():
    print("This test should be skipped and you should see the reason why.")

使用 pytest.skip() 在函数内部跳过

如果你需要根据某些运行时条件来决定是否跳过测试,可以在测试函数内部使用 pytest.skip() 函数

import pytest

def test_skip_inside_function():
    condition = False  # 这里可以是任何条件判断
    if not condition:
        pytest.skip("Skipping this test based on a runtime condition.")
    print("This part of the test will only run if the condition is True.")

结合两种方法

下面是一个完整的例子,展示了如何使用 pytest.mark.skip 和 pytest.skip():

import pytest

# 使用装饰器跳过测试并提供原因
@pytest.mark.skip(reason="This test is not implemented yet.")
def test_skip_with_reason():
    print("This test should be skipped.")

# 根据条件在函数内部跳过测试
def test_skip_inside_function():
    condition = False  # 这里可以是任何条件判断
    if not condition:
        pytest.skip("Skipping this test based on a runtime condition.")
    print("This part of the test will only run if the condition is True.")

# 正常测试用例作为对比
def test_normal_case():
    print("Running a normal test case.")
    assert True

执行结果

# 输入
pytest -v -s test_example.py

# 结果
============================= test session starts ==============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /path/to/your/project
collected 3 items

test_example.py::test_skip_with_reason SKIPPED (This test is not implemented yet.)
test_example.py::test_skip_inside_function 
Skipping this test based on a runtime condition.
SKIPPED
test_example.py::test_normal_case Running a normal test case.
PASSED

============================== 1 passed, 2 skipped in X.XX seconds ===============================

9.2 skipif – 有条件跳过测试用例

  • pytest.mark.skipif 是 pytest 提供的一个装饰器,用于根据给定条件跳过测试用例。如果提供的条件为 True,则该测试将被跳过;如果条件为 False,则测试会正常运行。你可以通过传递一个布尔表达式和一个可选的 reason 参数来解释为什么跳过测试

使用 pytest.mark.skipif

import pytest

# 如果条件为 True,则跳过测试
@pytest.mark.skipif(True, reason="This test is skipped because the condition is True.")
def test_skipif_with_true_condition():
    print("This test should be skipped.")

# 如果条件为 False,则测试不会被跳过
@pytest.mark.skipif(False, reason="This test will not be skipped because the condition is False.")
def test_skipif_with_false_condition():
    print("This test should run.")

依赖外部条件
通常,你会使用 skipif 来检查一些外部条件,比如环境变量、操作系统类型或第三方库的存在等
以下是在 Python3.8 之前的解释器上运行时标记要跳过的测试函数的示例

import sys
import pytest

# 根据 Python 版本跳过测试
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python 3.8 or higher")
def test_requires_python_38():
    print("Running a test that requires Python 3.8 or higher.")

结合多个条件
你还可以将多个条件组合起来,或者在 fixture 中使用 skipif

import pytest

# 定义一个 fixture,它可以根据条件跳过所有使用它的测试
@pytest.fixture
def check_environment():
    if some_condition:  # 替换为实际条件判断
        pytest.skip("Skipping due to environment configuration.")

# 使用 fixture 的测试函数
def test_with_check_environment(check_environment):
    print("This test runs only if the environment check passes.")

# 结合多个条件
@pytest.mark.skipif(
    sys.platform == "win32" and sys.version_info < (3, 8),
    reason="This test requires Python 3.8 or higher on Windows."
)
def test_combined_conditions():
    print("Running a test with combined conditions.")

输出结果

============================= test session starts ==============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /path/to/your/project
collected 3 items

test_example.py::test_with_check_environment 
This test runs only if the environment check passes.
PASSED
test_example.py::test_combined_conditions 
Running a test with combined conditions.
PASSED
test_example.py::test_normal_case 
Running a normal test case.
PASSED

============================== 3 passed in X.XX seconds ===============================

十、失败重跑

10.1 使用 --last-failed 选项

  • 这个选项会只运行上一次测试会话中失败的测试用例,而跳过所有通过的测试用例。这对于快速重新运行失败的测试非常有用

案例:
首先,让我们创建 50 个测试调用,其中只有 2 个失败

# content of test_50.py
import pytest


@pytest.mark.parametrize("i", range(50))
def test_num(i):
    if i in (17, 25):
        pytest.fail("bad luck")

如果您第一次运行该程序,您将看到两个失败:

$ pytest -q
.................F.......F........................                   [100%]
================================= FAILURES =================================
_______________________________ test_num[17] _______________________________

i = 17

    @pytest.mark.parametrize("i", range(50))
    def test_num(i):
        if i in (17, 25):
>           pytest.fail("bad luck")
E           Failed: bad luck

test_50.py:7: Failed
_______________________________ test_num[25] _______________________________

i = 25

    @pytest.mark.parametrize("i", range(50))
    def test_num(i):
        if i in (17, 25):
>           pytest.fail("bad luck")
E           Failed: bad luck

test_50.py:7: Failed
========================= short test summary info ==========================
FAILED test_50.py::test_num[17] - Failed: bad luck
FAILED test_50.py::test_num[25] - Failed: bad luck
2 failed, 48 passed in 0.12s

如果你使用以下命令运行它–lf:

$ pytest --lf
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items
run-last-failure: rerun previous 2 failures

test_50.py FF                                                        [100%]

================================= FAILURES =================================
_______________________________ test_num[17] _______________________________

i = 17

    @pytest.mark.parametrize("i", range(50))
    def test_num(i):
        if i in (17, 25):
>           pytest.fail("bad luck")
E           Failed: bad luck

test_50.py:7: Failed
_______________________________ test_num[25] _______________________________

i = 25

    @pytest.mark.parametrize("i", range(50))
    def test_num(i):
        if i in (17, 25):
>           pytest.fail("bad luck")
E           Failed: bad luck

test_50.py:7: Failed
========================= short test summary info ==========================
FAILED test_50.py::test_num[17] - Failed: bad luck
FAILED test_50.py::test_num[25] - Failed: bad luck
============================ 2 failed in 0.12s =============================

10.2 使用 --failed-first 选项

  • 这个选项会在测试会话开始时首先运行上次失败的测试用例,然后再运行其他的测试用例。这有助于尽早发现问题,并且可以继续运行其他测试以确保没有引入新的问题

案例:
我们将编写三个测试函数:两个会成功,一个会失败。然后我们将运行这些测试,并在修复失败的测试后再次运行它们,以显示 --failed-first 的效果

import pytest

def test_success_one():
    print("Running test_success_one")
    assert True

def test_success_two():
    print("Running test_success_two")
    assert True

def test_failure():
    print("Running test_failure")
    assert False, "This test is supposed to fail."

第一步:初次运行测试
首先,我们运行所有测试来确定哪些测试失败了

============================= test session starts ==============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /path/to/your/project
collected 3 items

test_example.py::test_success_one Running test_success_one
PASSED
test_example.py::test_success_two Running test_success_two
PASSED
test_example.py::test_failure Running test_failure
FAILED

=================================== FAILURES ===================================
_______________________________ test_failure _________________________________

    def test_failure():
        print("Running test_failure")
>       assert False, "This test is supposed to fail."
E       AssertionError: This test is supposed to fail.
E       assert False

test_example.py:10: AssertionError
============================== short test summary info ===============================
FAILED test_example.py::test_failure - AssertionError: This test is supposed to fail.
============================== 2 passed, 1 failed in X.XX seconds ===============================

第二步:修复失败的测试
现在我们修复 test_failure 函数中的错误:

def test_failure():
    print("Running test_failure (fixed)")
    assert True, "This test has been fixed."

第三步:使用 --failed-first 重新运行测试
接下来,我们使用 --failed-first 选项来确保上次失败的测试优先运行。这有助于尽早发现问题是否已经被解决

============================= test session starts ==============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /path/to/your/project
collected 3 items

test_example.py::test_failure Running test_failure (fixed)
PASSED
test_example.py::test_success_one Running test_success_one
PASSED
test_example.py::test_success_two Running test_success_two
PASSED

============================== 3 passed in X.XX seconds ===============================

10.3 使用 pytest-rerunfailures 插件

  • 如果你需要在同一个测试会话中多次重试失败的测试,可以安装并使用 pytest-rerunfailures 插件。这个插件允许你指定一个次数,当测试失败时它会自动重试指定的次数

安装插件

pip install pytest-rerunfailures

安装前提

pytest(>=5.3) and python>=3.6

查看安装版本

pip show pytest-rerunfailures

pytest-rerunfailures方法使用

  • 命令行参数:-reruns n(重新运行次数) - rerruns -delay m (等待运行次数)

使用装饰器

@pytest.mark.flaky(reruns=5,reruns_delay=2)

命令行案例:
我们将编写三个测试函数:两个会成功,一个会失败。然后我们将运行这些测试,并在修复失败的测试后再次运行它们,以显示 pytest-rerunfailures 如何工作

import pytest

def test_success_one():
    print("Running test_success_one")
    assert True

def test_success_two():
    print("Running test_success_two")
    assert True

def test_failure():
    print("Running test_failure")
    # 这个断言会在第一次执行时失败,但在后续重试中通过
    if not hasattr(test_failure, "retry_count"):
        test_failure.retry_count = 0
    test_failure.retry_count += 1
    if test_failure.retry_count < 3:
        assert False, f"This test is supposed to fail on retry {test_failure.retry_count}"
    else:
        print("This test has been fixed and now passes.")
        assert True

第一步:初次运行测试
首先,我们运行所有测试来确定哪些测试失败了,并查看重试机制是否按预期工作

执行命令

pytest --reruns 3 --reruns-delay 1 -v -s test_example.py

这里,–reruns 3 表示每个失败的测试最多重试 3 次,–reruns-delay 1 表示每次重试之间等待 1 秒

预期结果

============================= test session starts ==============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /path/to/your/project
collected 3 items

test_example.py::test_success_one Running test_success_one
PASSED
test_example.py::test_success_two Running test_success_two
PASSED
test_example.py::test_failure Running test_failure
FAILED

---------------------------------- Captured stdout call ----------------------------------
Running test_failure

=================================== FAILURES ===================================
_______________________________ test_failure _________________________________

    def test_failure():
        print("Running test_failure")
        if not hasattr(test_failure, "retry_count"):
            test_failure.retry_count = 0
        test_failure.retry_count += 1
        if test_failure.retry_count < 3:
>           assert False, f"This test is supposed to fail on retry {test_failure.retry_count}"
E           AssertionError: This test is supposed to fail on retry 1
E           assert False

test_example.py:16: AssertionError
----------------------------- RERUN test_failure ------------------------------
test_example.py::test_failure (re-run 1) Running test_failure
FAILED

---------------------------------- Captured stdout call ----------------------------------
Running test_failure

=================================== FAILURES ===================================
_______________________________ test_failure _________________________________

    def test_failure():
        print("Running test_failure")
        if not hasattr(test_failure, "retry_count"):
            test_failure.retry_count = 0
        test_failure.retry_count += 1
        if test_failure.retry_count < 3:
>           assert False, f"This test is supposed to fail on retry {test_failure.retry_count}"
E           AssertionError: This test is supposed to fail on retry 2
E           assert False

test_example.py:16: AssertionError
----------------------------- RERUN test_failure ------------------------------
test_example.py::test_failure (re-run 2) Running test_failure
PASSED

---------------------------------- Captured stdout call ----------------------------------
Running test_failure
This test has been fixed and now passes.

============================== short test summary info ===============================
FAILED test_example.py::test_failure - AssertionError: This test is supposed to fail on retry 1
FAILED test_example.py::test_failure (re-run 1) - AssertionError: This test is supposed to fail on retry 2
PASSED test_example.py::test_failure (re-run 2)
============================== 2 passed, 1 failed in X.XX seconds ===============================

在这个输出中,我们可以看到 test_failure 在前两次重试中失败了,但在第三次重试中通过了。

总结

  • 初次运行:展示了哪些测试通过了,哪些失败了,并且展示了重试机制
  • 重试机制:test_failure 测试在前两次重试中失败,但在第三次重试中通过了
  • 使用 pytest-rerunfailures 插件:确保失败的测试可以在同一个测试会话中多次重试,从而减少由于环境或其他不稳定因素导致的假阳性失败

十一、pytest执行顺序

在 pytest 中,测试函数的执行顺序默认是按照它们在文件中的定义顺序。然而,有时候你可能希望控制测试的执行顺序,例如确保某些依赖关系得以满足或优化测试运行时间。pytest 提供了多种方式来控制测试执行顺序,包括使用 @pytest.mark.order 装饰器(需要安装 pytest-ordering 插件)和内置的 pytest-order 插件

安装插件
pip install pytest-order

案例:
我们将创建几个测试函数,并使用 @pytest.mark.order 来指定它们的执行顺序

import pytest

@pytest.mark.order(2)
def test_second():
    print("Running second test")
    assert True

@pytest.mark.order(1)
def test_first():
    print("Running first test")
    assert True

@pytest.mark.order(3)
def test_third():
    print("Running third test")
    assert True

def test_unordered():
    print("Running unordered test")
    assert True

在这个例子中,我们指定了三个测试的执行顺序:test_first 会最先运行,然后是 test_second,最后是 test_third。test_unordered 没有指定顺序,因此它将根据其在文件中的位置决定执行顺序,通常是在所有有序测试之后执行

预期结果

============================= test session starts ==============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /path/to/your/project
collected 4 items

test_example.py::test_first Running first test
PASSED
test_example.py::test_second Running second test
PASSED
test_example.py::test_third Running third test
PASSED
test_example.py::test_unordered Running unordered test
PASSED

============================== 4 passed in X.XX seconds ===============================

在这个输出中,可以看到测试按照我们指定的顺序执行:test_first -> test_second -> test_third,而 test_unordered 在最后执行

不同的排序策略

  • 除了使用数字来指定顺序外,pytest-order 还支持其他排序策略,比如按字母顺序、反向顺序等。你还可以结合多个装饰器来实现更复杂的排序逻辑。

示例:按字母顺序执行

  • 如果你想要按字母顺序执行测试,可以使用 @pytest.mark.order(“alphabetical”):
@pytest.mark.order("alphabetical")
def test_a():
    print("Running test_a")
    assert True

@pytest.mark.order("alphabetical")
def test_b():
    print("Running test_b")
    assert True

十二、hooks(钩子函数)

12.1 钩子函数的四个阶段

12.1.1 配置阶段 (Configuration)

描述:

  • 在这个阶段,pytest 解析命令行参数、读取配置文件,并进行必要的初始化工作。你可以通过这个阶段的钩子函数来添加自定义选项或修改全局配置

常用钩子函数:

  • pytest_addoption(parser):
    • 用途:向命令行接口添加自定义选项
    • 示例:
def pytest_addoption(parser):
    parser.addoption("--runslow", action="store_true", help="run slow tests")
  • pytest_configure(config):
    • 用途:在所有测试开始前进行全局配置,比如注册 markers 或设置其他全局状态。
    • 示例:
def pytest_configure(config):
    config.addinivalue_line("markers", "slow: mark test as slow to run")

12.2 收集阶段 (Collection)

描述:

  • pytest 在此阶段会搜索并收集所有符合标准的测试项(test items)。你可以在这一阶段修改哪些测试会被收集,或者改变它们的属性。

常用钩子函数:

  • pytest_collect_file(path, parent):
    • 用途:自定义文件收集器,允许 pytest 收集非标准测试文件
    • 示例:
def pytest_collect_file(parent, path):
    if path.ext == ".yaml" and path.basename.startswith("test_"):
        return YamlFile.from_parent(parent, fspath=path)
  • pytest_collection_modifyitems(session, config, items):
    • 用途:修改收集到的测试项列表,例如根据条件过滤或重新排序测试项。
    • 示例:
def pytest_collection_modifyitems(items):
    items.sort(key=lambda item: item.name)  # 按名称排序

12.3 运行阶段 (Running)

描述:

  • 这是实际执行测试的阶段。pytest 会依次调用每个测试项的 setup、call 和 teardown 方法。你可以在这些方法中插入额外的逻辑,如日志记录、性能监控等

常用钩子函数:

  • pytest_runtest_protocol(item, nextitem):
    • 用途:控制整个测试协议,包括 setup、call 和 teardown。
  • pytest_runtest_setup(item):
    • 用途:在每个测试项执行之前调用,用于设置测试环境。
    • 示例:
def pytest_runtest_setup(item):
    print(f"Setting up {item.name}")
  • pytest_runtest_call(item):
    • 用途:在每个测试项的实际执行过程中调用。
    • 示例:
def pytest_runtest_call(item):
    print(f"Calling {item.name}")
  • pytest_runtest_teardown(item, nextitem):
    • 用途:在每个测试项执行之后调用,用于清理测试环境。
    • 示例:
def pytest_runtest_teardown(item, nextitem):
    print(f"Tearing down {item.name}")
  • pytest_report_teststatus(report, config):
    • 用途:自定义测试状态报告,改变测试通过、失败或跳过的显示方式。
    • 示例:
def pytest_report_teststatus(report, config):
    if report.when == 'call' and report.failed:
        return "failed", "F", "FAILED"

12.4 总结阶段 (Summary)

描述:

  • 在所有测试完成后,pytest 会生成一个总结报告,显示测试结果。你可以在此阶段添加自定义的总结信息,或者修改默认的输出格式。

常用钩子函数:

  • pytest_terminal_summary(terminalreporter, exitstatus, config):
    • 用途:在终端输出总结信息。
    • 示例:
def pytest_terminal_summary(terminalreporter, exitstatus, config):
    print("Custom summary information")
  • 四个阶段的关系
    • 配置阶段:为测试会话准备环境,确保一切就绪
    • 收集阶段:确定哪些测试需要运行,并构建测试项列表
    • 运行阶段:依次执行测试项,并处理每个测试的 setup、call 和 teardown
    • 总结阶段:提供测试结果的汇总信息,并结束测试会话

12.2 钩子函数关键点

1. 调用顺序:了解各个阶段的钩子函数调用顺序,以便在适当的时间点插入逻辑


2. 常见钩子函数:熟悉关键的钩子函数及其用途,以实现所需的定制化功能

3. request 对象:利用 request 对象提供的上下文信息来增强灵活性

许多钩子函数接收一个 request 对象作为参数,该对象提供了访问当前测试上下文的能力。request 对象非常强大,因为它包含了关于测试会话、节点、配置等方面的信息

  • request.config: 访问全局配置
  • request.node: 获取当前测试项的信息
  • request.addfinalizer(): 注册一个函数,在测试结束时调用

4. 插件兼容性:确保自定义插件与现有插件良好协作


5.文档和社区支持:充分利用官方文档和社区资源来解决问题和学习最佳实践


结合案例

  • 假设你想确保一些测试总是最先运行,而另一些则在最后运行。你可以结合 pytest_collection_modifyitems 和 @pytest.mark.order 来实现这一点
import pytest

def pytest_collection_modifyitems(items):
    # 定义一个排序键,确保带有 'order' 标记的测试按照指定顺序执行
    items.sort(key=lambda item: (getattr(item.get_closest_marker('order'), 'args', [0])[0], item.name))

@pytest.mark.order(1)
def test_first():
    print("Running first test")
    assert True

@pytest.mark.order(2)
def test_second():
    print("Running second test")
    assert True

def test_unordered():
    print("Running unordered test")
    assert True

在这个例子中,pytest_collection_modifyitems 确保了标记为 @pytest.mark.order 的测试按照指定顺序执行,而未标记的测试则排在其后

十三、Allure测试报告

13.1 前言

Allure 是一个灵活且功能强大的测试报告工具,支持多种编程语言和测试框架,包括 Python 的 pytest。它能够生成详细且美观的测试报告,帮助团队更好地理解和分析测试结果。以下是关于如何在 pytest 中集成 Allure 测试报告的关键点和步骤

13.2 安装 Allure

首先,你需要安装 Allure 和相关插件:

  • 安装 Allure 命令行工具:
    • 使用 Homebrew(MacOS):brew install allure
    • 使用 Chocolatey(Windows):choco install allure
    • 或者从 下载并手动安装。
  • 安装 pytest-allure-adaptor 插件:
    • 使用 pip 安装:pip install pytest-allure-adaptor

13.3 配置 pytest 以使用 Allure

1. 在命令行中启用 Allure
你可以直接在命令行中通过添加 --alluredir 参数来指定保存 Allure 结果的目录:

pytest --alluredir=/path/to/result/dir

2. 使用 pytest.ini 或 tox.ini 配置文件

你也可以将 Allure 配置添加到 pytest.ini 或 tox.ini 文件中,以便每次运行测试时自动应用:

[pytest]
addopts = --alluredir=allure-results

13.4 Allure装饰器函数

装饰器函数

方法参数参数说明
@allure.epic()epic描述定义项目、当有多个项目是使用。往下是feature
@allure.feature()模块名称用例按照模块区分,有多个模块时给每个起名字
@allure.story()用例名称一个用例的描述
@allure.title(用例的标题)用例标题一个用例的标题
@allure.testcase()测试用例连接的地址自动化用例对应的功能用例存放系统的地址
@allure.issue()缺陷地址对应缺陷管理系统里边的缺陷地址
@allure.description()用例描述对应测试用例的描述
@allure.step()测试步骤测试用例的操作步骤
@allure.severity()用例等级blocker 、critical 、normal 、minor 、trivial
@allure.link()定义连接用于定义一个需要在测试报告中展示的连接
@allure.attachment()附件添加测试报告附件

13.5 执行自动化用例 生成allure报告所需文件

测试代码

import pytest

def test_success():
    """this test succeeds"""
    assert True

def test_failure():
    """this test fails"""
    assert False

def test_skip():
    """this test is skipped"""
    pytest.skip('for a reason!')

def test_broken():
    raise Exception('oops')

运行

pytest --alluredir=./results

在这里插入图片描述

13.6 查看测试报告的两种方式

13.6.1 直接打开默认浏览器展示报告

allure serve ./result/

在这里插入图片描述

13.6.2 从结果生成报告

  • 生成报告

    • allure generate ./result/ -o ./report/ --clean (覆盖路径加–clean)
  • 打开报告

    • allure open -h 127.0.0.1 -p 8883 ./report/

十四、pytest中管理日志

14.1 日志级别

debug:打印全部日志,详细信息
info:打印info、warning、error、critical级别的日志,确认一切按预期运行
warning:打印warning、error、critical级别的日志
error:打印error、critical级别日志,或者一些更为严重,软件没能执行一些功能
critical:打印critical日志,一个严重的错误,表明程序可能无法正常的执行

等级顺序:

debug–》info–》warning–》error–》critical

14.2 使用logging模块

你可以直接在测试代码中使用 logging 模块来记录信息。pytest 会自动捕获这些日志并根据上述配置进行处理

import logging

def test_example():
    logger = logging.getLogger(__name__)
    logger.info("This is an info message")
    logger.debug("This is a debug message")
    assert True

14.3 将日志保存到文件

有时你可能希望将日志保存到文件而不是仅限于终端输出。你可以通过配置 logging 模块来实现这一点

import logging

# 配置日志记录器以写入文件
logging.basicConfig(filename='test.log', filemode='w', level=logging.INFO)

def test_logging_to_file():
    logger = logging.getLogger(__name__)
    logger.info("Logging to file")
    assert True

此外,你也可以在 pytest.ini 中配置日志输出到文件:

[pytest]
log_file = test.log
log_file_level = INFO

14.4 控制日志捕获的行为

有时候你可能不想捕获某些特定的日志输出,或者想完全禁用日志捕获。你可以通过 caplog fixture 来控制日志捕获的行为

def test_control_log_capture(caplog):
    caplog.set_level(logging.WARNING)  # 只捕获 WARNING 级别及以上的日志
    logging.info("This will not be captured")
    logging.warning("This will be captured")
    assert "captured" in caplog.text

14.5 示例

import logging
import os
import time

from config.conf import BASE_DIR
import colorlog

log_color_config = {
        'DEBUG': 'cyan',
        'INFO': 'green',
        'WARNING': 'yellow',
        'ERROR': 'red',
        'CRITICAL': 'bold_red'}

log = logging.getLogger('log_name')

consloe_handler = logging.StreamHandler()
daytime = time.strftime("%Y-%m-%d")
path = BASE_DIR + 'log/'if not os.path.exists(path):
    os.makedirs(path)

filename = path + f'/run_log_{daytime}.log'file_handle = logging.FileHandler(filename=filename, mode='a', encoding="utf-8")

log.setLevel(logging.DEBUG)
consloe_handler.setLevel(logging.DEBUG)
file_handle.setLevel(logging.INFO)

file_formatter = logging.Formatter(
    fmt='%(asctime)s - %(levelname)s - %(name)s - %(module)s:%(funcName)s:%(lineno)d - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S')

console_formatter = colorlog.ColoredFormatter(
    fmt='%(log_color)s%(levelname)-8s%(reset)s | %(log_color)s%(message)s%(reset)s | %(white)s%(asctime)s | %(blue)s%(name)s:%(filename)s:%(lineno)d%(reset)s',
    datefmt='%Y-%m-%d %H:%M:%S',  # 设置日期/时间格式    reset=True,  # 自动重置颜色到默认值    log_colors=log_color_config,  # 使用上面定义的日志等级颜色配置    secondary_log_colors={},  # 可选:为特定字段添加颜色    style='%'  # 使)
consloe_handler.setFormatter(console_formatter)
file_handle.setFormatter(file_formatter)


if not log.handlers:
    log.addHandler(consloe_handler)
    log.addHandler(file_handle)


consloe_handler.close()
file_handle.close()

if __name__ == '__main__':
    log.debug("debug")
    log.info("info")
    log.warning("warning")
    log.critical("critical")

http://www.kler.cn/a/465067.html

相关文章:

  • 基层医联体医院患者历史检验检查数据的快速Python编程分析
  • RIP配置实验
  • docker Error response from daemon
  • 腾讯云智能结构化 OCR:驱动多行业数字化转型的核心引擎
  • java中static和const和final的区别
  • GitHub Fork 和 Clone 的深度指南:操作解析与 Pull Request 完整流程20241231
  • 迈向云原生网络的初期
  • web期末作业网页设计实例代码 (建议收藏) HTML+CSS+JS (网页源码)
  • Rdis速通
  • 本地部署Whisper Web结合内网穿透实现远程访问本地语音转文本模型
  • 大模型在自动驾驶领域的应用和存在的问题
  • vuepress 项目支持ie运行
  • Flutter入门,Flutter基础知识总结。
  • 跨语言数据格式标准化在 HarmonyOS 开发中的实践
  • 详细教程:SQL2008数据库备份与还原全流程!
  • 【SQL server】教材数据库(7)
  • JAVA-制作小游戏期末实训
  • Python PySide + SQLite3 开发的 《️ POS点销管理系统》可用初型
  • Postman[8] 断言
  • edeg插件/扩展推荐:助力生活工作
  • 【视频笔记】基于PyTorch从零构建多模态(视觉)大模型 by Umar Jamil【持续更新】
  • CM3/CM4时钟系统
  • STM32-笔记28-蓝牙模块
  • SQL 总结
  • 125个Docker的常用命令
  • 数据库-MySQL-limit优化(全面 易理解)