掌握pytest:Python测试框架的简明指南

本文详细介绍了如何使用pytest进行Python测试,包括安装、编写测试用例、运行测试、解析结果、异常处理以及高级功能如标记、夹具、参数化和插件等,帮助开发者提升代码质量和测试效率。

如何使用pytest:Python测试的简明指南

随着AI的快速发展,像ChatGPT这样的工具使得开发过程更快、更易访问。开发者现在可以通过一些清晰表达的提示和仔细的代码审查来编写代码和构建Web应用。虽然这提高了生产力,但也带来了越来越多的缺点。AI生成的代码容易出错、出现意外错误或与代码的其他部分集成不良。由于这些风险,建立健壮的测试实践以确保代码高质量和正常运行比以往任何时候都更加重要。各种测试工具可用于解决这些挑战,而pytest在Python生态系统中因其简单性、灵活性和强大功能而脱颖而出。

在本文中,我们将探讨以下主题:

目录

  • 为什么使用pytest?
  • 如何用pytest编写你的第一个测试
  • 如何运行pytest测试
  • 如何解释pytest结果
  • 如何在pytest中处理异常
  • 高级pytest功能
    1. pytest标记
    2. pytest夹具
    3. 参数化
    4. pytest插件
  • 结论

通过本文,你将全面了解pytest,并能在Python开发过程中使用它。

先决条件

  • 必须安装Python
  • 理解Python编程语言

为什么使用pytest?

pytest是一个流行的Python测试框架,使得编写和运行测试变得容易。与unittest和其他Python测试框架不同,pytest的简单语法允许开发者直接将测试编写为函数或在类中。这让你可以编写干净、可读的代码,而无需复杂化。

pytest还支持流行的Python框架,如Flask、Django等。结合其他丰富功能,pytest为你提供了在当今AI驱动时代交付可靠软件所需的工具。

pytest作为首选测试工具的关键特性包括:

  • 灵活性:通过支持函数、类和模块的测试,在测试结构上提供灵活性。
  • 详细的测试输出:提供详细且可读的测试输出,便于理解测试失败和错误。
  • 自动测试发现:通过查找以"test_“开头或以”_test.py"结尾的文件自动发现测试。这消除了手动指定测试文件的需要。
  • 参数化:支持参数化测试,允许你用多组输入运行单个测试函数。
  • 夹具:夹具提供setup和tearDown方法,有助于防止代码重复。这使你能为测试设置基线条件,并在每个测试后删除它们。
  • 插件和扩展:拥有丰富的插件和扩展生态系统,添加额外功能,如详细测试报告,以及与其他工具和Python框架(如Django和Flask)的集成。
  • 兼容性:与其他测试框架(如unittest)兼容,允许你从不同测试框架迁移测试并在其上无缝运行。

如何用pytest编写你的第一个测试

本节将指导你使用pytest框架编写第一组测试。

pytest是一个Python包,使用前需要安装。你可以使用以下命令安装:

1
pip install pytest

注意:遵循Python最佳实践,建议在虚拟环境中安装pytest。这里有一个指南帮助你设置。

接下来,创建一个Python文件,在其中编写测试并导入pytest:

1
import pytest

pytest有2种基本的编写测试方法,包括:

  • 基于函数的方法:这种方法直接编写测试,因为你在单独的函数中编写测试。 注意:每个函数名必须以test_为前缀,以便pytest自动发现和运行这些测试。 以下是一个基于函数的测试示例:

    1
    2
    
    def test_addition():
        assert 1 + 1 == 2
    

    注意:在上面的代码中,pytest中使用的assert语句是Python的内置“assert”。它更方便,不需要像unittest中常见的assertEqual和assertTrue这样的特定方法。使用assert语句的另一个优点是,当断言失败时,它提供更详细的错误消息。

  • 基于类的方法:这种方法类似于在unittest中编写测试的方式,除了你的测试类不继承任何方法。示例如下:

    1
    2
    3
    
    class TestMathOperations:
        def test_addition(self):
            assert 1 + 1 == 2
    

    当你想将相关测试分组在一起时,这种在pytest中编写测试的方法很有用。

如何运行pytest测试

运行pytest与运行常规Python脚本的常规方法略有不同。

运行pytest测试的一般方法是在终端中运行pytest命令。pytest将自动查找并运行当前目录和子目录中所有形式为test_.py或_test.py的文件。但虽然这可能是运行测试的好方法,pytest提供了比这种一般方法更多的灵活性。

根据偏好,你可能希望基于以下方式运行测试文件:

  • 运行特定测试文件:要运行特定文件中的测试,使用pytest命令后跟文件名。例如:pytest test_example.py
  • 运行目录中的测试:假设你有一个名为Tests的目录,其中包含一些测试文件。要运行该目录中的所有测试,使用pytest命令后跟目录和正斜杠。例如:pytest Tests/
  • 使用特定关键字运行测试:要基于某个关键字运行测试,使用命令pytest -k "keyword"。Pytest将自动查找并运行当前目录和子目录中匹配该关键字的函数名、类名或文件名。但要在特定文件中运行匹配某个关键字的测试,你必须在pytest命令后指定文件名。例如:pytest test_example.py -k "keyword"
  • 运行测试文件中的特定测试:要仅运行测试文件中的特定测试,使用命令pytest test_example.py::test_addition。这将仅运行test_example.py模块中的test_addition测试函数。
  • 运行特定类中的所有测试方法:要运行特定类中的所有测试,使用pytest test_example.py::TestClass。此命令将运行test_example.py模块中TestClass类中的所有测试方法。
  • 运行特定类中的特定测试方法:要运行特定类中的特定测试,使用pytest test_example.py::TestClass::test_addition。此命令将运行test_example.py模块中TestClass类中的特定test_addition方法。

如何解释pytest结果

pytest优于其他Python测试框架的一个主要优点是它提供的丰富输出,这给出了关于测试状态的非常详细的信息。

让我们使用一个基本测试来理解如何解释pytest的输出:

1
2
3
4
import pytest

def test_addition():
    assert 1 + 1 == 3

运行此测试,我们得到类似于以下的输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
============================== test session starts ====================================
platform win32 -- Python 3.10.5, pytest-8.4.1, pluggy-1.6.0
rootdir: C:\\Users\\hp\\Desktop\\Pytest
collected 1 items

                                                                                  [ 50%]
test_example.py F                                                                 [100%]

===================================== FAILURES =========================================
____________________________________test_addition ______________________________________

    def test_addition():
>       assert 1 + 1 == 3
E       assert (1 + 1) == 3

test_example.py:4: AssertionError
============================== short test summary info =================================
FAILED test_example.py::test_addition - assert (1 + 1) == 3
========================= 1 failed, 1 passed in 0.13s ==================================

上述输出分为几个部分。以下是每个部分的含义:

  • 测试会话信息:

    1
    2
    3
    4
    
    =============================== test session starts ===============================
    platform win32 -- Python 3.10.5, pytest-8.4.1, pluggy-1.6.0
    rootdir: C:\\Users\\hp\\Desktop\\TDD pytest
    collected 1 item
    

    此部分显示测试环境的摘要。它以指示测试会话开始的线标记开头。 标记下方,pytest显示有关操作系统以及安装的Python、pytest和pluggy版本的信息。(Pluggy是用于管理插件的pytest依赖项。) 下一行指示运行测试的根目录。 此部分的最后一行显示在此目录中找到的测试数量。

  • 测试状态:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    test_example.py F                                                              [100%]
    
    ================================== FAILURES =========================================
    ________________________________ test_addition ______________________________________
    
        def test_addition():
    >       assert 1 + 1 == 3
    E       assert (1 + 1) == 3
    
    test_example.py:4: AssertionError
    

    此部分显示有关测试状态的信息。 此部分的第一行指定正在运行的测试文件,后跟状态(在此情况下为F,表示测试失败)。 下一组行给出有关失败测试的特定信息。这包括发生失败的函数(test_addition),以及导致错误的代码确切行。 最后一行给出此部分的简明摘要。它指示错误发生在test_example.py的第4行,并且是一个AssertionError。

  • 测试摘要:

    1
    2
    3
    
    ============================= short test summary info =============================
    FAILED test_example.py::test_addition - assert (1 + 1) == 3
    ================================ 1 failed in 0.13s ================================
    

    此部分提供测试的总体摘要。 它指示失败测试发生在test_example.py文件的test_addition函数中,因为不正确的断言(1 + 1) == 3不成立。

用正确的断言assert(1 + 1) == 2编辑代码并重新运行。这次,代码通过,输出不同:

1
2
3
4
5
6
7
8
=============================== test session starts ==================================
platform win32 -- Python 3.10.5, pytest-8.3.2, pluggy-1.5.0
rootdir: C:\\Users\\hp\\Desktop\\TDD pytest
collected 1 items

test_example.py .                                                               [100%]

=============================== 1 passed in 0.01s =================================

如何在pytest中处理异常

异常是在运行测试时发生的意外错误,它们阻止我们的代码按预期执行。因此,pytest提供了几种内置机制来处理这些异常(但本文仅涵盖其中之一)。

pytest.raises上下文管理器是一个工具,用于检查你的代码是否引发特定异常。如果引发指定异常,则该测试通过,确认发生了预期错误。但如果未引发指定异常,则该测试失败。

pytest.raises的使用示例:

  • 检查ValueError:在Python中,当函数接收到具有不正确值的参数时,会引发ValueError。在下面的示例中,我们可以验证在尝试计算负数的平方根时是否引发ValueError。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    import pytest
    import math
    
    def calculate_square_root(value):
        if value < 0:
            raise ValueError("Cannot calculate the square root of a negative number")
        return math.sqrt(value)
    
    def test_calculate_square_root():
        with pytest.raises(ValueError):
            calculate_square_root(-1)
    
  • 检查ZeroDivisionError:将数字除以零会引发ZeroDivisionError。在此示例中,我们检查在将数字除以零时是否引发此错误。

    1
    2
    3
    4
    5
    6
    7
    8
    
    import pytest
    
    def divide_numbers(numerator, denominator):
        return numerator / denominator
    
    def test_divide_numbers():
        with pytest.raises(ZeroDivisionError):
            divide_numbers(10, 0)
    
  • 检查TypeError:当对不适当类型的对象应用操作时,会引发TypeError。这里,我们检查在添加不兼容数据类型(如示例中给出的字符串和整数)时是否引发此错误。

    1
    2
    3
    4
    5
    6
    7
    8
    
    import pytest
    
    def add_numbers(a, b):
        return a + b
    
    def test_add_numbers():
        with pytest.raises(TypeError):
            add_numbers("10", 5)
    
  • 检查KeyError:当我们尝试访问不存在的字典键时,会引发KeyError。我们可以使用以下代码验证和处理此错误:

    1
    2
    3
    4
    5
    6
    7
    8
    
    import pytest
    
    def get_value(dictionary, key):
        return dictionary[key]
    
    def test_get_value():
        with pytest.raises(KeyError):
            get_value({"name": "Alice"}, "age")
    

高级pytest功能

作为一个健壮的测试框架,pytest提供了一些高级功能,帮助你管理复杂的测试场景。在本节中,我们将以初学者友好的水平探索其中一些高级功能,并演示如何开始在测试中应用它们。

1. pytest标记

当处理大型代码库时,有时运行每个测试可能很耗时。这就是pytest标记派上用场的地方。

标记就像一个标签,你可以将其附加到测试函数以对其进行分类。一旦测试被标记,你可以指示pytest仅运行具有某些标记的测试。例如,如果某些测试执行时间较长,你可能将它们标记为“slow”,并将它们与较快的测试分开运行。

使用标记的一个优点是,它允许你基于类别或特定参数运行特定测试,并在某些条件未满足时跳过测试。

pytest附带了一些内置标记,可能非常有用:

  • @pytest.mark.skip:此标记允许你无条件跳过测试,当你知道由于外部问题或不完整代码测试将失败时,这可能很有用。 示例:

    1
    2
    3
    
    @pytest.mark.skip(reason="Feature not yet implemented")
    def test_feature():
        pass
    
  • @pytest.mark.skipif:此标记允许你在满足某些条件时有条件地跳过测试。 示例:

    1
    2
    3
    4
    5
    6
    
    import sys
    
    @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
    class TestClass:
        def test_function(self):
            "This test will not run under 'win32' platform"
    
  • @pytest.mark.xfail:此标记附加到预期失败的测试,可能是由于错误或不完整功能。因此,当pytest运行此类测试时,不会将其计为失败。 示例:

    1
    2
    3
    
    @pytest.mark.xfail(reason="division by zero not handled yet")
    def test_divide_by_zero():
        assert divide(10, 0) == 0
    

    注意:默认情况下不显示跳过/失败测试的详细信息,以避免混乱输出。

虽然pytest附带了一些内置标记,但你也可以创建自己的自定义标记(但本教程不涵盖)。请参阅文档以获取有关使用自定义标记的更多信息。

2. pytest夹具

在pytest中,夹具允许你创建可重用的默认数据,这些数据可以在多个测试之间共享。通过使用夹具,你可以减少代码重复,使测试更清晰、更易于维护。

在pytest中,夹具使用@pytest.fixture装饰器定义,如下例所示: 假设我们有多个测试依赖于用户数据列表。而不是在每个测试中重复相同的数据,我们可以创建一个夹具来保存这些数据,并且夹具被传递给需要它的测试。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pytest

@pytest.fixture
def user_data():
    return [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 25},
        {"name": "Charlie", "age": 35}
    ]

# 测试函数以按名称和年龄检查特定用户
def test_user_exists(user_data):
    user = {"name": "Alice", "age": 30}

    # 检查目标用户是否在列表中
    assert user in user_data

# 测试用户的平均年龄
def test_average_age(user_data):
    ages = [user["age"] for user in user_data]
    avg_age = sum(ages) / len(ages)
    assert avg_age == 30

注意:上面代码中的@pytest.fixture装饰器将user_data函数标记为pytest中的夹具。此夹具提供可重用的数据,可以在多个测试函数之间共享,允许它们共享相同的设置而无需重复代码。

3. 参数化

参数化是pytest的一个功能,允许你一次用不同的数据集运行测试函数。

例如:假设你有一个计算数字平方的函数。为了在测试时提供足够的覆盖,你希望用零、正数和负数测试该函数。

而不是为每个场景编写单独的测试函数,你可以使用参数化一次用不同的数据集运行测试函数。这种方法更简洁,并减少代码重复。

要在pytest中使用参数化,我们使用@pytest.mark.parametrize装饰器,如下例所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import pytest

# 计算数字平方的函数
def square_numbers(num):
    return num * num

# 参数化装饰器以用不同输入测试平方函数
@pytest.mark.parametrize("input_value, expected_output", [
    (2, 4),     
    (-3, 9),    
    (0, 0)    
])

def test_square(input_value, expected_output):
    assert square_numbers(input_value) == expected_output

在上面的示例中,不同的输入值和期望值列在@pytest.mark.parametrize装饰器中。我们用三个不同的输入值测试square_numbers()函数:2、-3和0。

对于每个值,pytest调用test_square()函数并将square_numbers(input_value)的结果与expected_output进行比较。

这种方法更高效,并确保函数在各种情况下按预期行为。

4. pytest插件

插件是一种扩展机制,允许你向pytest添加新功能或修改其现有行为。这些插件通过提供扩展pytest能力的额外功能工作,这可能很有用,尤其是在复杂的测试场景中。

pytest拥有庞大的插件生态系统,每个插件都设计以满足你的不同测试需求。你可以在PyPI的pytest插件列表中找到可用插件的完整列表。

要使用插件,只需用pip安装它。 例如:

1
2
pip install pytest-NAME
pip uninstall pytest-NAME

注意:上面的代码中的NAME应替换为要安装的插件名称。

安装插件后,pytest自动查找并集成它。无需任何额外配置。

在本节中,我们探索了pytest的一些高级功能。通过利用这些功能,你现在可以通过确保测试更高效、可扩展且随时间推移更易于维护来显著提高测试质量。

结论

在本文中,你学习了使用pytest进行测试的基础知识,从编写和解释测试到处理异常以及使用高级功能如夹具和参数化。

无论你的代码是手动编写还是由AI生成,学习如何编写测试使你能够早期检测错误,并构建更可靠的软件。测试充当安全网,在开发过程中增强你的信心,并确保你的代码按预期工作。

如果你准备更进一步,我写了一篇关于Python中测试驱动开发的深入文章。这是一种强大的方法,其中编写测试指导你的整个编码过程。

如果你觉得这有帮助,请告诉我,与你的网络分享,或点赞以帮助其他人也发现它。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计