pytest 是一个功能强大且灵活的 Python 测试框架,身为一个开发者学好怎么写测试是基本的必要条件,以下是我对 pytest 的使用心得。
开发环境
- Windows 11 Home
- PyCharm 2024.3.5
- Python 3.13.2
- uv 0.6.10
- pytest 8.3.5
- pytest-cov 6.1.0
- pytest-html>=4.1.1
安装环境
用 uv 建立 python 专案
uv init Lab.Py.TestProject
cd Lab.Py.TestProject
在 python 专案安装测试套件
uv add pytest
测试起手式
基本原则
建立测试档案
- 测试类别名称需以 Test 开头。
- 测试函数名称需以 test 开头。
被测目标物
# calculator.py
class Calculator:
"""
一个简单的计算器类,提供基本的数学运算功能。
提供的运算包括加法、减法、乘法和除法。
使用範例:
calc = Calculator()
result = calc.add(5, 3)
result = calc.subtract(5, 3)
result = calc.multiply(5, 3)
result = calc.divide(5, 3)
"""
def add(self,a, b):
return a + b
def subtract(self,a, b):
return a - b
def multiply(self,a, b):
return a * b
def divide(self,a, b):
if b == 0:
raise ValueError("除数不能为零")
return a / b
撰写测试
範例程式码:
# calculator_test.py
import pytest
from calculator import Calculator
class TestCalculator:
def test_add(self):
target = Calculator()
assert target.add(2, 3) == 5
def test_subtract(self):
target = Calculator()
assert target.subtract(5, 3) == 2
def test_multiply(self):
target = Calculator()
assert target.multiply(4, 3) == 12
def test_divide(self):
target = Calculator()
assert target.divide(10, 2) == 5
验证/断言
assert
结果跟期望值比对
assert target.divide(10, 2) == 5
pytest.raises
检测是否抛出预期的例外。
def test_divide_by_zero(self):
target = Calculator()
with pytest.raises(ValueError):
target.divide(10, 0)
执行测试
测试多个档案
如果专案包含多个测试档案,在终端机中执行以下指令:
uv run pytest
指定特定档案
uv run pytest test_example.py
显示详细测试结果
uv run pytest -v
仅执行失败的测试
uv run pytest --lf
产生 HTML 测试报告
在 python 专案安装测试报告套件
uv add pytest-html
执行测试并输出报告:
uv run pytest --html=report.html
report.html 效果如下
产生 Allure 测试报告
在作业系统安装 allure
scoop install allure
在 python 专案安装 allure-pytest 套件
uv add allure-pytes
执行测试并产生 allure result
uv run pytest --alluredir=allure-results
挂起 allure server
allure serve allure-results
执行结果如下:
测试涵盖率
uv run pytest --cov=calculator
参数化测试
使用 装饰子 decorator @pytest.mark.parametrize 定义传入参数值、验证值,就可以批次执行测试,类比 .NET 的测试框架 MsTest、XUnit、NUnit
# test_parametrize.py
import pytest
from calculator import Calculator
@pytest.mark.parametrize("first, second, expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
])
def test_add(first, second, expected):
calculator = Calculator()
assert calculator.add(first, second) == expected
执行结果如下
fixture/scope
fixture 是用来定义测试,需要共用资源的修饰子,共用的範围 scope 有以下层级:
- function
- class
- module
- session
- 预设的 scope="function"
- 测试方法依赖 @fixture 方法,例如,setup_and_cleanup_function
每一个测试 function 执行前后设置及清理
当 scope="function",会在每一个 function 分别执行一次设置和清理
# test_fixtures.py
import pytest
from calculator import Calculator
@pytest.fixture(scope="function")
def setup_and_cleanup_function():
print("\n")
print("每一个 function 个别执行一次设定")
yield
print("\n")
print("每一个 function 个别执行一次清理")
def test_add_1(setup_and_cleanup_function):
target = Calculator()
assert target.add(2, 3) == 5
class TestCalculator:
def test_add(self, setup_and_cleanup_function):
target = Calculator()
assert target.add(2, 3) == 5
... 省略
每一个测试类别 (class) 执行前后设置及清理
当 scope="class",会在第一个测试方法前执行一次设置,最后一个方法执行后执行一次清理
# test_fixtures_class.py
import pytest
from calculator import Calculator
@pytest.fixture(scope="class")
def setup_and_cleanup_module():
print("\n")
print("每一个 class 执行一次设置\n")
yield
print("\n")
print("每一个 class 执行一次清理\n")
def test_add_1(setup_and_cleanup_module):
target = Calculator()
assert target.add(2, 3) == 5
class TestCalculator:
def test_add_2(self, setup_and_cleanup_module):
target = Calculator()
assert target.add(2, 3) == 5
当前 .py 档的 class 和 function 只执行一次设置和清理
当 scope="module",当前 .py 档,class 和 function 只执行一次设置和清理
# test_fixtures_module.py
import pytest
from calculator import Calculator
@pytest.fixture(scope="module")
def setup_and_cleanup_module():
print("\n")
print("当前 .py 档,class 和 function 只执行一次设置\n")
yield
print("\n")
print("当前 .py 档,class 和 function 只执行一次清理\n")
def test_add_1(setup_and_cleanup_module):
target = Calculator()
assert target.add(2, 3) == 5
class TestCalculator:
def test_add_2(self, setup_and_cleanup_module):
target = Calculator()
assert target.add(2, 3) == 5
... 省略
每一个测试 Session (package) 执行一次设置及清理
scope="session" 是在当前这个测试 Session (package) 的前后进行设置 / 清理工作,可以包含多个 .py 档
# test_fixtures_session.py
import pytest
from calculator import Calculator
@pytest.fixture(scope="session")
def setup_and_cleanup_session():
print("\n")
print("每一个测试 session 只执行一次设置\n")
yield
print("\n")
print("每一个测试 session 只执行一次清理\n")
def test_add_1(setup_and_cleanup_session):
target = Calculator()
assert target.add(2, 3) == 5
class TestCalculator:
def test_add_2(self, setup_and_cleanup_session):
target = Calculator()
assert target.add(2, 3) == 5
... 省略
按下 F5 或是 Alt+F5 观察,设置和清理的生命週期
或是用以下脚本观察
uv run pytest -v -s test_fixtures_module.py
分类测试方法
当测试方法越来越多的时候,分类它们有助于阅读跟查找、执行
当使用 @pytest.mark.demo 标记分类,demo 代表分类名称
# test_fixtures_mark.py
import pytest
from calculator import Calculator
@pytest.mark.demo
class TestCalculator:
def test_add(self):
target = Calculator()
assert target.add(2, 3) == 5
def test_subtract(self):
target = Calculator()
assert target.subtract(5, 3) == 2
def test_multiply(self):
target = Calculator()
assert target.multiply(4, 3) == 12
def test_divide(self):
target = Calculator()
assert target.divide(10, 2) == 5
@pytest.mark.skip
def test_divide_by_zero(self):
target = Calculator()
with pytest.raises(ValueError):
target.divide(10, 0)
调用测试时,指定要执行哪一个分类,传入 -m demo,这样一来就只会执行 demo 类别的测试
uv run pytest -v -s -m demo
执行结果如下:
跳过测试方法
装饰子 pytest.mark.skip,可以用来标记不执行,当执行测试时,会略过它
@pytest.mark.skip(reason="搞不定,先跳过")
def test_divide_by_zero(self):
target = Calculator()
with pytest.raises(ValueError):
target.divide(10, 0)
执行结果如下:
範例位置
https://github.com/yaochangyu/sample.dotblog/tree/master/Test/Lab.Py.TestProject
参考
https://docs.pytest.org/en/stable/getting-started.html
若有谬误,烦请告知,新手发帖请多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET