Pytest

pytest 是 Python 中非常流行的测试框架,它有许多强大且灵活的功能,适用于各种测试需求,同时还有强大的插件支持。

常用插件:

  • allure-pytest 是一个集成在 Pytest 中用于生成 allure JSON数据的工具,搭配 allure 可以生成非常美观的报告。
  • pytest-xdist 是一个集成在 Pytest 中进程级别并行运行测试的工具。
  • ddt 是一个数据驱动测试工具,可以帮助我们使用更加丰富数据传参方式。
  • pytest-cov 是一个集成在 Pytest 中用于生成测试代码的覆盖率报告。
  • pytest-timer 是一个集成在 Pytest 中用于记录每个测试的执行时间。
  • pytest-sugar 是一个集成在 Pytest 中用于提供更美观的测试输出格式。
  • pytest-ordering 是一个集成在 Pytest 中用于控制测试用例的执行顺序。
  • pytest-forked 是一个集成在 Pytest 中用于每个测试用例中启动新的进程,从而确保测试隔离。

安装

pip install pytest

命名规范

  • 测试文件、测试方法:以 test_​ 开头。
  • 测试类名:以 TestXxx​ 开头。

测试结果

pytest​ 每个测试方法都是通过 assert​ 断言来判断测试执行结果的,如果断言不通过则测试结果为 Failed,如果断言通过则为 PASSED,如果中途出现其他报错信息,则为 Warning,也属于 Failed。

def test_assert_passed():
assert 1 == 1

def test_assert_field():
assert 1 == 2

def test_assert_warning():
assert 1 / 0 == 1

常用mark

参数化测试

可以通过 pytest.mark.parametrize​ 装饰器提供多个测试数据,避免重复编写类似的测试。

@pytest.mark.parametrize("num1, num2", [(1,2,3),(3,4,5)])
def test_addition(num1, num2):
assert num1 + num2 == expected

跳过测试

# 无条件跳过
@pytest.mark.skip(reason="暂时跳过")
def test_skip():
assert 1 + 1 == 2

# 有条件跳过
@pytest.mark.skipif(sys.platform == 'win32', reason="Windows 下不支持")
def test_skip_if():
assert 1 + 1 == 2

预期失败标记

# 预期这个测试会失败
@pytest.mark.xfail(reason="已知 Bug 待修复")
def test_example():
assert 1 + 1 == 3

添加标记

pytest​ 允许你为测试函数添加自定义标记,以便后续指定是否允许。

@pytest.mark.markName
def test_long_running():
assert True

测试夹具

pytest​ 的夹具用于创建测试前的环境或数据。夹具可以通过装饰器定义,并且可以在多个测试中复用。

@pytest.fixture(scope="module", autouse=True)
def setup_data():
print("\n[Setup] 准备测试环境")
yield {'username': 'testuser', 'password': 'password123'}
print("\n[Teardown] 清理测试环境")

def test_login(setup_data):
assert setup_data['username'] == 'testuser'

pytest​ 允许为夹具指定不同的作用域:

  • scope="function"​(默认值):每个测试用例都会调用一次夹具。
  • scope="module"​:每个模块只调用一次夹具。
  • scope="class"​:每个测试类调用一次夹具。
  • scope="session"​:整个测试会话只调用一次夹具。

pytest​ 还支持自动调用夹具,使用 autouse=True​ 让夹具自动应用到每个测试中。

pytest​ 通过yield关键字,可以在fixture中实现资源的创建和销毁。

预期异常

def test_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0

日志记录

import logging

def test_logging():
logging.basicConfig(level=logging.DEBUG)
logging.debug("This is a debug message")
assert True

捕获输出

def test_print(capsys):
print("Hello, pytest!")
captured = capsys.readouterr()
assert captured.out == "Hello, pytest!"

钩子函数

# 通过 pytest_configure 钩子函数自定义 pytest 配置
def pytest_configure(config):
print("pytest 配置已设置")

# 通过 pytest_runtest_protocol 钩子函数在每个测试开始前后执行操作
def pytest_runtest_protocol(item, nextitem):
print(f"运行测试: {item.name}")
return None

构建/销毁

  1. xxx_method​:在每个测试方法运行之前/后执行一次。
  2. xxx_class​:在测试类中的所有测试方法运行之前/后执行一次。
class TestExample:

@classmethod
def setup_class(cls):
print(f"Setup class: {cls.__name__}")

@classmethod
def teardown_class(cls):
print(f"Tear down class: {cls.__name__}")

def setup_method(self, method):
print(f"Setup method: {method.__name__}")

def teardown_method(self, method):
print(f"Tear down method: {method.__name__}")

运行

你可以通过指定文件、类、方法来运行特定的测试。

格式:文件名::类名::方法名

  1. 基本运行
pytest test_example.py::TestMathOperations::test_addition
  1. 生成 HTML 的测试报告。
pytest --html=./report.html
  1. 指定标记运行。
# 运行被markName标记的测试
pytest -m markName

# 运行除被markName标记的测试
pytest -m "not markName"

# 运行被markName1和markName2标记的测试
pytest -m "markName1 and markName2"

# 运行被markName1或markName2标记的测试
pytest -m "markName1 or markName2"
  1. python代码运行。
pytest.main(['-vs', '-m markName', '--html=./report.html', './test_cases'])
  1. 使用 allure-pytest​ 生成JSON数据,并生成 allure 的测试报告。
# 生成JSON数据
pytest --alluredir=./json_report

# 根据JSON数据生成HTML报告
allure generate ./json_report -o ./html_report --clean
  1. 使用 pytest-xdist​ 并行测试。
pytest -n [N|auto]

使用 auto​ 会使用全部CPU来执行。

  1. 使用 pytest-cov​ 生成覆盖率。
pytest --cov=moduleName --cov-report=html

会生成htmlcov​报告目录及.coverage​数据文件。

  1. 使用 pytest-timer​ 生成测试时间。
pytest --timer

# 显示耗时最长的前N个
pytest --timer-top-n N

# 显示过滤测试结果
pytest --timer-filter ok|warning|error
  1. 使用 pytest-forked​ 进行隔离测试。
pytest --forked