一、媒介
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 下令行安装
2.2 验证安装
三、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 测试隔离
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 装饰器时,你需要提供两个参数:
- 第一个参数是一个字符串,其中包含逗号分隔的参数名列表
- 第二个参数是一个元组列表(或者可迭代的对象),每个元组代表一组测试数据,这些数据会依次传递给测试函数的相应参数
示例
- 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 直接打开默认浏览器展示报告
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")
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |