第183集:pytest框架
一、pytest框架概述
pytest是一个功能强大的Python第三方测试框架,它提供了比unittest更简洁、更灵活的测试语法。pytest支持自动发现测试、参数化测试、fixture机制、丰富的插件生态等功能,是Python社区中最受欢迎的测试框架之一。
1.1 pytest的优势
相比unittest框架,pytest具有以下优势:
- 语法简洁:不需要继承特定的测试类,测试函数名以
test_开头即可 - 自动发现:自动发现测试文件和测试函数
- 丰富的断言:使用Python内置的断言语句,语法更自然
- fixture机制:灵活的测试固件管理,支持依赖注入
- 参数化测试:轻松实现数据驱动测试
- 丰富的插件:拥有超过800个插件,支持各种扩展功能
- 详细的错误信息:提供清晰、详细的测试失败信息
- 兼容unittest:可以运行unittest编写的测试用例
1.2 pytest的安装
pytest是一个第三方库,需要使用pip进行安装:
pip install pytest安装完成后,可以使用以下命令检查pytest的版本:
pytest --version二、pytest的基本用法
2.1 编写第一个pytest测试
让我们从一个简单的例子开始,学习如何使用pytest来测试一个计算器类。
2.1.1 被测试的代码
首先,我们创建一个简单的计算器类,用于计算两个数的和:
# calculator.py
class Calculator:
def add(self, a, b):
return a + b2.1.2 编写测试函数
接下来,我们创建一个测试文件,用于测试Calculator类的add方法。在pytest中,测试文件的名称通常以test_开头或结尾,测试函数的名称必须以test_开头:
# test_calculator.py
from calculator import Calculator
def test_add():
# 创建计算器实例
calc = Calculator()
# 测试用例1:两个正数相加
result = calc.add(1, 2)
assert result == 3
# 测试用例2:两个负数相加
result = calc.add(-1, -2)
assert result == -3
# 测试用例3:正数和负数相加
result = calc.add(1, -2)
assert result == -1
# 测试用例4:与零相加
result = calc.add(0, 5)
assert result == 5在上面的代码中,我们:
- 导入了被测试的Calculator类
- 定义了一个测试函数
test_add,函数名以test_开头 - 在测试函数中,创建Calculator实例,调用add方法
- 使用Python内置的
assert语句验证结果
2.1.3 运行测试
我们可以使用以下命令来运行测试:
# 运行当前目录下的所有测试
pytest
# 运行指定的测试文件
pytest test_calculator.py
# 运行指定的测试函数
pytest test_calculator.py::test_add运行测试后,我们会看到类似以下的输出:
============================= test session starts ==============================
platform win32 -- Python 3.10.0, pytest-7.2.0, pluggy-1.0.0
rootdir: C:\Users\username\tests
collected 1 item
test_calculator.py . [100%]
============================== 1 passed in 0.02s ===============================输出中的点号(.)表示测试通过,每个点号代表一个测试函数。如果测试失败,会显示F或E,表示测试失败或发生错误。
2.2 pytest的测试发现规则
pytest会自动发现测试文件和测试函数,遵循以下规则:
- 测试文件的名称以
test_开头或结尾,例如test_calculator.py或calculator_test.py - 测试类的名称以
Test开头,例如`class TestCalculator: - 测试类中定义的测试方法以
test_开头,例如`def test_add(self): - 测试函数的名称以
test_开头,例如`def test_add():
2.3 使用-v选项查看详细输出
我们可以使用-v选项来查看更详细的测试输出:
pytest -v test_calculator.py运行测试后,我们会看到类似以下的输出:
============================= test session starts ==============================
platform win32 -- Python 3.10.0, pytest-7.2.0, pluggy-1.0.0
rootdir: C:\Users\username\tests
collected 1 item
test_calculator.py::test_add PASSED [100%]
============================== 1 passed in 0.02s ===============================三、pytest的断言方法
pytest使用Python内置的断言语句,语法更自然、更简洁。pytest会自动捕获断言异常,并提供详细的错误信息。
3.1 常用断言语句
def test_assertions():
# 相等断言
assert 1 + 2 == 3
# 不相等断言
assert 1 + 2 != 4
# 布尔值断言
assert True
assert not False
# 包含断言
assert 3 in [1, 2, 3, 4, 5]
assert "hello" in "hello world"
# 不包含断言
assert 6 not in [1, 2, 3, 4, 5]
# 类型断言
assert isinstance(5, int)
assert isinstance("hello", str)
# 比较断言
assert 5 > 3
assert 5 >= 5
assert 3 < 5
assert 5 <= 5
# 异常断言
with pytest.raises(ZeroDivisionError):
result = 5 / 0
# 异常信息断言
with pytest.raises(ValueError) as excinfo:
raise ValueError("除数不能为零")
assert "除数不能为零" in str(excinfo.value)3.2 断言细节增强
pytest提供了pytest.approx函数,用于浮点数比较:
def test_float():
assert (0.1 + 0.2) == 0.3 # 可能会失败,因为浮点数精度问题
assert (0.1 + 0.2) == pytest.approx(0.3) # 正确的浮点数比较方式
# 设置相对误差和绝对误差
assert (0.1 + 0.2) == pytest.approx(0.3, rel=1e-9, abs=1e-9)四、pytest的fixture机制
fixture是pytest的核心功能之一,它提供了灵活的测试固件管理。fixture可以用来设置测试环境、提供测试数据、清理测试环境等。
4.1 定义fixture
fixture使用@pytest.fixture装饰器来定义:
import pytest
import tempfile
import os
@pytest.fixture
def calculator():
"""提供计算器实例的fixture"""
from calculator import Calculator
return Calculator()
@pytest.fixture
def temp_file():
"""提供临时文件的fixture"""
# 创建临时文件
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
f.write("Hello, pytest!")
temp_file_path = f.name
yield temp_file_path # 提供临时文件路径给测试函数
# 清理临时文件
if os.path.exists(temp_file_path):
os.remove(temp_file_path)4.2 使用fixture
测试函数可以通过参数名来使用fixture:
def test_add(calculator):
"""使用calculator fixture测试加法运算"""
assert calculator.add(1, 2) == 3
assert calculator.add(-1, -2) == -3
def test_file_content(temp_file):
"""使用temp_file fixture测试文件内容"""
with open(temp_file, "r") as f:
content = f.read()
assert content == "Hello, pytest!"4.3 fixture的作用域
fixture可以设置作用域,控制fixture的执行次数:
@pytest.fixture(scope="function") # 函数级,默认值,每个测试函数执行一次
def func_scope():
pass
@pytest.fixture(scope="class") # 类级,每个测试类执行一次
def class_scope():
pass
@pytest.fixture(scope="module") # 模块级,每个测试模块执行一次
def module_scope():
pass
@pytest.fixture(scope="session") # 会话级,整个测试会话执行一次
def session_scope():
pass4.4 fixture的依赖注入
fixture可以依赖其他fixture:
@pytest.fixture
def database():
"""提供数据库连接的fixture"""
print("创建数据库连接")
# 模拟数据库连接
db = {"users": []}
yield db
print("关闭数据库连接")
@pytest.fixture
def user_data(database):
"""提供用户数据的fixture,依赖database fixture"""
user = {"id": 1, "name": "张三"}
database["users"].append(user)
return user
def test_user_in_database(user_data, database):
"""测试用户是否在数据库中"""
assert user_data in database["users"]五、参数化测试
pytest支持参数化测试,可以轻松实现数据驱动测试。参数化测试使用@pytest.mark.parametrize装饰器来定义。
5.1 基本参数化
import pytest
from calculator import Calculator
def test_add():
"""不使用参数化的测试"""
calc = Calculator()
assert calc.add(1, 2) == 3
assert calc.add(-1, -2) == -3
assert calc.add(1, -2) == -1
assert calc.add(0, 5) == 5
# 使用参数化的测试
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3), # 测试用例1:两个正数相加
(-1, -2, -3), # 测试用例2:两个负数相加
(1, -2, -1), # 测试用例3:正数和负数相加
(0, 5, 5), # 测试用例4:与零相加
(999999, 1, 1000000) # 测试用例5:大数相加
])
def test_add_parametrized(a, b, expected):
"""使用参数化的测试"""
calc = Calculator()
assert calc.add(a, b) == expected5.2 多参数组合
参数化测试可以使用多个参数组合:
@pytest.mark.parametrize("a", [1, 2, 3])
@pytest.mark.parametrize("b", [4, 5, 6])
def test_multiply_parametrized(a, b):
"""测试乘法运算,参数组合为(1,4),(1,5),(1,6),(2,4),...,(3,6)"""
calc = Calculator()
assert calc.multiply(a, b) == a * b5.3 参数化fixture
fixture也可以参数化:
@pytest.fixture(params=["加法", "减法", "乘法", "除法"])
def operation(request):
"""提供操作类型的fixture"""
return request.param
def test_operation(operation):
"""测试不同的操作类型"""
assert operation in ["加法", "减法", "乘法", "除法"]六、pytest的测试组织
6.1 使用测试类
虽然pytest不需要继承特定的测试类,但我们仍然可以使用测试类来组织测试函数:
import pytest
from calculator import Calculator
class TestCalculator:
"""计算器测试类"""
def setup_method(self):
"""在每个测试方法运行前执行"""
self.calc = Calculator()
def teardown_method(self):
"""在每个测试方法运行后执行"""
del self.calc
def test_add(self):
"""测试加法运算"""
assert self.calc.add(1, 2) == 3
def test_subtract(self):
"""测试减法运算"""
assert self.calc.subtract(5, 3) == 2
def test_multiply(self):
"""测试乘法运算"""
assert self.calc.multiply(2, 3) == 6
def test_divide(self):
"""测试除法运算"""
assert self.calc.divide(6, 3) == 2
def test_divide_by_zero(self):
"""测试除数为零的情况"""
with pytest.raises(ValueError):
self.calc.divide(5, 0)6.2 跳过测试和预期失败
pytest提供了装饰器来控制测试的执行:
import pytest
import sys
@pytest.mark.skip(reason="这个测试暂时不运行")
def test_skip():
"""跳过的测试"""
assert False # 这个断言不会执行
@pytest.mark.skipif(sys.version_info < (3, 10), reason="需要Python 3.10或更高版本")
def test_skipif():
"""根据条件跳过的测试"""
assert True
@pytest.mark.xfail(reason="这个测试预期会失败")
def test_xfail():
"""预期会失败的测试"""
assert 1 + 1 == 3 # 这个断言预期会失败
@pytest.mark.xfail(strict=True, reason="这个测试必须失败")
def test_xfail_strict():
"""必须失败的测试,如果通过了会被标记为失败"""
assert 1 + 1 == 3七、pytest的测试报告
pytest提供了多种测试报告格式,常用的包括:
7.1 文本报告
默认的文本报告,可以使用-v或-vv选项来增加详细程度:
pytest -v test_calculator.py
pytest -vv test_calculator.py7.2 HTML报告
可以使用pytest-html插件生成HTML格式的测试报告:
# 安装pytest-html插件
pip install pytest-html
# 生成HTML报告
pytest --html=report.html test_calculator.py7.3 XML报告
可以生成JUnit XML格式的测试报告,方便集成到CI/CD系统中:
pytest --junitxml=report.xml test_calculator.py八、兼容unittest
pytest可以运行unittest编写的测试用例,不需要修改任何代码:
pytest test_unittest.py九、完整示例:使用pytest测试计算器
9.1 计算器类(calculator.py)
class Calculator:
"""一个简单的计算器类"""
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 / b9.2 测试文件(test_calculator.py)
import pytest
from calculator import Calculator
@pytest.fixture
def calculator():
"""提供计算器实例的fixture"""
return Calculator()
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3), # 两个正数相加
(-1, -2, -3), # 两个负数相加
(1, -2, -1), # 正数和负数相加
(0, 5, 5), # 与零相加
(999999, 1, 1000000) # 大数相加
])
def test_add(calculator, a, b, expected):
"""测试加法运算"""
assert calculator.add(a, b) == expected
@pytest.mark.parametrize("a, b, expected", [
(5, 3, 2), # 正常减法
(3, 5, -2), # 被减数小于减数
(0, 0, 0) # 零减零
])
def test_subtract(calculator, a, b, expected):
"""测试减法运算"""
assert calculator.subtract(a, b) == expected
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 6), # 正常乘法
(-2, 3, -6), # 负数乘法
(2, 0, 0) # 与零相乘
])
def test_multiply(calculator, a, b, expected):
"""测试乘法运算"""
assert calculator.multiply(a, b) == expected
@pytest.mark.parametrize("a, b, expected", [
(6, 3, 2), # 正常除法
(5, 2, 2.5), # 小数除法
(-6, 3, -2) # 负数除法
])
def test_divide(calculator, a, b, expected):
"""测试除法运算"""
assert calculator.divide(a, b) == expected
def test_divide_by_zero(calculator):
"""测试除数为零的情况"""
with pytest.raises(ValueError) as excinfo:
calculator.divide(5, 0)
assert "除数不能为零" in str(excinfo.value)9.3 运行测试
我们可以使用以下命令来运行测试:
pytest -v test_calculator.py运行测试后,我们会看到类似以下的输出:
============================= test session starts ==============================
platform win32 -- Python 3.10.0, pytest-7.2.0, pluggy-1.0.0
rootdir: C:\Users\username\tests
collected 10 items
test_calculator.py::test_add[1-2-3] PASSED [ 10%]
test_calculator.py::test_add[-1--2--3] PASSED [ 20%]
test_calculator.py::test_add[1--2--1] PASSED [ 30%]
test_calculator.py::test_add[0-5-5] PASSED [ 40%]
test_calculator.py::test_add[999999-1-1000000] PASSED [ 50%]
test_calculator.py::test_subtract[5-3-2] PASSED [ 60%]
test_calculator.py::test_subtract[3-5--2] PASSED [ 70%]
test_calculator.py::test_subtract[0-0-0] PASSED [ 80%]
test_calculator.py::test_multiply[2-3-6] PASSED [ 90%]
test_calculator.py::test_multiply[-2-3--6] PASSED [100%]
============================== 10 passed in 0.04s ==============================十、总结
- pytest是一个功能强大的Python第三方测试框架,语法简洁、灵活
- pytest支持自动发现测试文件和测试函数
- pytest使用Python内置的断言语句,语法更自然
- fixture机制提供了灵活的测试固件管理
- 参数化测试轻松实现数据驱动测试
- pytest拥有丰富的插件生态,支持各种扩展功能
- pytest可以运行unittest编写的测试用例
通过学习pytest框架,我们可以更加高效地编写和执行测试,提高代码的质量和可靠性。在接下来的几集中,我们将学习如何使用pytest进行单元测试、集成测试等更高级的测试技术。
下集预告:第184集将介绍单元测试,学习如何使用unittest和pytest框架编写单元测试,以及单元测试的最佳实践。