第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 + b

2.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

在上面的代码中,我们:

  1. 导入了被测试的Calculator类
  2. 定义了一个测试函数test_add,函数名以test_开头
  3. 在测试函数中,创建Calculator实例,调用add方法
  4. 使用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会自动发现测试文件和测试函数,遵循以下规则:

  1. 测试文件的名称以test_开头或结尾,例如test_calculator.pycalculator_test.py
  2. 测试类的名称以Test开头,例如`class TestCalculator:
  3. 测试类中定义的测试方法以test_开头,例如`def test_add(self):
  4. 测试函数的名称以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():
    pass

4.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) == expected

5.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 * b

5.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.py

7.2 HTML报告

可以使用pytest-html插件生成HTML格式的测试报告:

# 安装pytest-html插件
pip install pytest-html

# 生成HTML报告
pytest --html=report.html test_calculator.py

7.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 / b

9.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框架编写单元测试,以及单元测试的最佳实践。

« 上一篇 unittest框架 下一篇 » 单元测试