pytest 是一个功能强大且易于使用的Python测试框架,它允许开发者编写简单或复杂的函数式测试。pytest 的设计理念是让测试过程尽可能的简单和直观,同时提供丰富的插件生态系统来扩展其功能。
介绍:
pip install pytest
pytest --version
创建一个名为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 =============================
创建一个名为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的属性 观察失败原因主要看断言中的中间值
好处:
测试组织
仅在特定类中共享用于测试的装置
在班级层面上应用标记,并让它们隐式地应用于所有测试
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
参数 | 功能 |
---|---|
-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 的配置文件之一,用于定义项目的全局设置和选项。通过这个文件,你可以定制化测试行为,指定插件、命令行选项以及其他配置项,而无需每次都手动在命令行中输入这些参数
[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
关键配置项解释
conftest.py 文件是 Pytest 框架中的一个特殊文件,用于包含 fixture(固定装置)和其他配置代码。Pytest 会自动加载名为 conftest.py 的文件中定义的 fixtures 和插件,而不需要在测试模块中显式导入它们。这个文件通常用来存放那些被多个测试文件共享的配置和设置
例如,在 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()
通过使用pytest.mark帮助程序,您可以轻松地在测试函数上设置元数据
usefixtures - 在测试函数或类上使用fixture
filterwarnings-过滤测试函数的某些警告
skip-总是跳过测试函数
skipif - 如果满足某个条件,则跳过测试函数
xfail - 如果满足某个条件,则产生“预期失败”结果
参数化-对同一个测试函数执行多次调用
自定义标记
自定义标记就如上述pytest.ini
文件,自定义标记
[pytest]
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
serial
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
在 pytest 中,fixture 是一种用于设置测试环境的机制。它们可以用来执行一些前置或后置操作(例如:准备数据、启动服务、清理状态等),并且可以在多个测试之间共享。fixture 的设计使得代码复用和测试之间的依赖关系更加清晰,同时也让测试函数本身保持简洁
fixture方法:
fixture(callable_or_scope=None, *args, scope="function", params=None, autouse=False, ids=None, name=None)
作用域(scope):
参数化(params):
自动应用(autouse):
依赖注入(request ):
你可以通过装饰器 @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 关键字之后的代码是后置操作,在测试完成后执行
方式一:直接作为测试函数的参数
这是最常见和推荐的方式。你只需要将 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 可以相应地处理它们并提供测试会话的摘要,同时保持测试套件为绿色
跳过意味着您仅希望测试在满足某些条件时才能通过,否则 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 ===============================
使用 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 ===============================
案例:
首先,让我们创建 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 =============================
案例:
我们将编写三个测试函数:两个会成功,一个会失败。然后我们将运行这些测试,并在修复失败的测试后再次运行它们,以显示 --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 ===============================
安装插件
pip install pytest-rerunfailures
安装前提
pytest(>=5.3) and python>=3.6
查看安装版本
pip show pytest-rerunfailures
pytest-rerunfailures方法使用
使用装饰器
@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 在前两次重试中失败了,但在第三次重试中通过了。
总结
在 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.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
描述:
常用钩子函数:
def pytest_addoption(parser):
parser.addoption("--runslow", action="store_true", help="run slow tests")
def pytest_configure(config):
config.addinivalue_line("markers", "slow: mark test as slow to run")
描述:
常用钩子函数:
def pytest_collect_file(parent, path):
if path.ext == ".yaml" and path.basename.startswith("test_"):
return YamlFile.from_parent(parent, fspath=path)
def pytest_collection_modifyitems(items):
items.sort(key=lambda item: item.name) # 按名称排序
描述:
常用钩子函数:
def pytest_runtest_setup(item):
print(f"Setting up {item.name}")
def pytest_runtest_call(item):
print(f"Calling {item.name}")
def pytest_runtest_teardown(item, nextitem):
print(f"Tearing down {item.name}")
def pytest_report_teststatus(report, config):
if report.when == 'call' and report.failed:
return "failed", "F", "FAILED"
描述:
常用钩子函数:
def pytest_terminal_summary(terminalreporter, exitstatus, config):
print("Custom summary information")
1. 调用顺序:了解各个阶段的钩子函数调用顺序,以便在适当的时间点插入逻辑
3. request 对象:利用 request 对象提供的上下文信息来增强灵活性
许多钩子函数接收一个 request 对象作为参数,该对象提供了访问当前测试上下文的能力。request 对象非常强大,因为它包含了关于测试会话、节点、配置等方面的信息
4. 插件兼容性:确保自定义插件与现有插件良好协作
5.文档和社区支持:充分利用官方文档和社区资源来解决问题和学习最佳实践
结合案例
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 是一个灵活且功能强大的测试报告工具,支持多种编程语言和测试框架,包括 Python 的 pytest。它能够生成详细且美观的测试报告,帮助团队更好地理解和分析测试结果。以下是关于如何在 pytest 中集成 Allure 测试报告的关键点和步骤
首先,你需要安装 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
装饰器函数
方法 | 参数 | 参数说明 |
---|---|---|
@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() | 附件 | 添加测试报告附件 |
测试代码
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
allure serve ./result/
生成报告
打开报告
debug:打印全部日志,详细信息
info:打印info、warning、error、critical级别的日志,确认一切按预期运行
warning:打印warning、error、critical级别的日志
error:打印error、critical级别日志,或者一些更为严重,软件没能执行一些功能
critical:打印critical日志,一个严重的错误,表明程序可能无法正常的执行
等级顺序:
debug–》info–》warning–》error–》critical
你可以直接在测试代码中使用 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
有时你可能希望将日志保存到文件而不是仅限于终端输出。你可以通过配置 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
有时候你可能不想捕获某些特定的日志输出,或者想完全禁用日志捕获。你可以通过 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
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")