美文网首页测试平台
使用 Pytest 进行有效的 Python 测试

使用 Pytest 进行有效的 Python 测试

作者: ADADACHAN | 来源:发表于2021-08-17 17:29 被阅读0次

    使用 Pytest 进行有效的 Python 测试

    测试您的代码会带来各种各样的好处。它增加了您对代码按预期运行的信心,并确保对代码的更改不会导致回归。编写和维护测试是一项艰巨的工作,因此您应该利用可用的所有工具使其尽可能轻松。 pytest 是您可以用来提高测试效率的最佳工具之一。

    1.如何安装pytest

    要跟随本教程中的一些示例,您需要安装 pytest。与大多数 Python 包一样,您可以使用 pip 在 PyPI 的虚拟环境中安装 pytest:

    pip install pytest 或者 python -m pip install pytest

    2.是什么让 pytest 如此有用

    如果您之前为 Python 代码编写过单元测试,那么您可能已经使用过 Python 的内置单元测试模块。 unittest 为构建测试套件提供了坚实的基础,但它也有一些缺点。

    许多第三方测试框架试图解决 unittest 的一些问题,而 pytest 已被证明是最受欢迎的框架之一。 pytest 是一个功能丰富、基于插件的生态系统,用于测试 Python 代码。

    如果您还没有使用 pytest 的乐趣,那么您将获得一种享受!它的理念和功能将使您的测试体验更加高效和愉快。使用pytest,普通任务需要更少的代码,高级任务可以通过各种省时的命令和插件来实现。它甚至可以开箱即用地运行您现有的测试,包括那些用 unittest 编写的测试。

    与大多数框架一样,一些在您第一次开始使用 pytest 时有意义的开发模式可能会随着您的测试套件的增长而开始造成痛苦。本教程将帮助您了解 pytest 提供的一些工具,即使它在扩展时也能保持测试的效率和效果

    3.样板

    想象一下,您想编写一个测试套件来确保 unittest 在您的项目中正常工作。你可能想编写一个总是通过的测试,一个总是失败的测试:

    from unittestimport TestCase

    class TryTesting(TestCase):

        def test_always_passes(self):

            self.assertTrue(True)

        def test_always_fails(self):

            self.assertTrue(False)

    D:\Users\dev\PycharmProjects\djangod\test>python -m unittest discover

    F.

    ======================================================================

    FAIL: test_always_fails (test_with_unittest.TryTesting)

    ----------------------------------------------------------------------

    Traceback (most recent call last):

      File "D:\Users\dev\PycharmProjects\djangod\test\test_with_unittest.py", line 8, in test_always_fails

        self.assertTrue(False)

    AssertionError: False is not true

    ----------------------------------------------------------------------

    Ran 2 tests in 0.001s

    FAILED (failures=1)

    正如预期的那样,一项测试通过,一项失败。你已经证明了 unittest 是有效的,但看看你必须做什么:

    3.1基本思路:

    1)从 unittest 导入 TestCase 类

    2)创建 TryTesting,TestCase 的子类

    3)在 TryTesting 中为每个测试编写一个方法

    4)使用 unittest.TestCase 中的 self.assert* 方法之一进行断言

    3.2 备注

    1)点 (.) 表示测试通过。

    2)F 表示测试失败。

    3)E 表示测试引发了意外异常。

    pytest 的学习曲线比 unittest 的要浅,因为您不需要为大多数测试学习新的结构。此外,您之前可能在实现代码中使用过的断言的使用使您的测试更易于理解

    3.3 状态和依赖管理

    您的测试通常取决于代码中某些对象的数据片段或测试替身。在 unittest 中,您可以将这些依赖项提取到 setUp() 和 tearDown() 方法中,以便类中的每个测试都可以使用它们。但这样做时,您可能会不经意间使测试对特定数据或对象的依赖完全隐含。

    随着时间的推移,隐式依赖会导致复杂的代码混乱,您必须展开这些代码才能使测试有意义。测试应该可以帮助您使代码更易于理解。如果测试本身很难理解,那么你可能有麻烦了!

    pytest 采用不同的方法。由于夹具的可用性,它会引导您实现仍然可重用的显式依赖项声明。 pytest fixtures 是为测试套件创建数据或测试替身或初始化某些系统状态的函数。任何想要使用夹具的测试都必须明确接受它作为参数,因此依赖项总是预先声明。

    Fixtures 也可以使用其他 Fixtures,同样通过将它们显式声明为依赖项。这意味着,随着时间的推移,您的装置会变得笨重和模块化。尽管将固定装置插入其他固定装置的能力提供了极大的灵活性,但随着测试套件的增长,它也会使管理依赖项变得更具挑战性。在本教程的稍后部分,您将了解有关装置的更多信息,并尝试使用一些技巧来应对这些挑战。

    3.4 测试过滤

    随着您的测试套件的增长,您可能会发现您只想对某个功能运行几个测试,然后保存完整的套件以备后用。 pytest 提供了几种方法来做到这一点:

    1)基于名称的过滤:您可以将 pytest 限制为仅运行那些完全限定名称与特定表达式匹配的测试。您可以使用 -k 参数执行此操作。

    2)目录范围:默认情况下,pytest 将仅运行当前目录中或当前目录下的那些测试。

    3)测试分类:pytest 可以包含或排除您定义的特定类别中的测试。您可以使用 -m 参数执行此操作。

    3.4 测试参数化

    当您测试处理数据或执行通用转换的函数时,您会发现自己编写了许多类似的测试。它们可能仅在被测试代码的输入或输出上有所不同。这需要复制测试代码,这样做有时会掩盖您尝试测试的行为。

    unittest 提供了一种将多个测试收集到一个中的方法,但它们不会在结果报告中显示为单个测试。如果一项测试失败而其余测试通过,则整个组仍将返回一个失败结果。 pytest 提供了自己的解决方案,其中每个测试都可以独立通过或失败。在本教程后面,您将看到如何使用 pytest 参数化测试。

    3.5 基于插件的架构

    pytest 最漂亮的特性之一是它对定制和新特性的开放性。几乎程序的每一部分都可以被破解和更改。因此,pytest 用户开发了一个丰富的有用插件生态系统。

    尽管一些 pytest 插件专注于 Django 等特定框架,但其他插件适用于大多数测试套件。您将在本教程后面看到一些特定插件的详细信息

    4.Fixtures:管理状态和依赖关系

    pytest fixtures 是一种为测试提供数据、测试替身或状态设置的方式。 Fixtures 是可以返回大范围值的函数。每个依赖于夹具的测试都必须明确接受该夹具作为参数。

    4.1 何时创建夹具

    假设您正在编写一个函数 format_data_for_display() 来处理 API 端点返回的数据。数据代表一个人的列表,每个人都有一个给定的名字、姓氏和职位。该函数应该输出一个字符串列表,其中包括每个人的全名(他们的 given_name 后跟他们的 family_name)、一个冒号和他们的头衔。要对此进行测试,您可以编写以下代码:现在,每个测试都明显缩短了,但仍然有一条清晰的路径返回它所依赖的数据。请务必为您的灯具命名特定的名称。这样,您可以在将来编写新测试时快速确定是否要使用它!

    import pytest

    @pytest.fixture

    def format_data_for_display(people):

    return "abcdedf "

        ...# Implement this!

    def test_format_data_for_display():

    people = [

    {

    "given_name":"Alfonsa",

                "family_name":"Ruiz",

                "title":"Senior Software Engineer",

            },

            {

    "given_name":"Sayid",

                "family_name":"Khan",

                "title":"Project Manager",

            },

        ]

    assert format_data_for_display(people) == [

    "Alfonsa Ruiz: Senior Software Engineer",

            "Sayid Khan: Project Manager",

        ]

    5.标记:分类测试

    在任何大型测试套件中,某些测试不可避免地会很慢。例如,他们可能会测试超时行为,或者他们可能会练习广泛的代码区域。不管是什么原因,当您尝试快速迭代新功能时,最好避免运行所有缓慢的测试。

    pytest 使您能够为测试定义类别,并提供在运行套件时包括或排除类别的选项。您可以使用任意数量的类别标记测试。

    标记测试对于按子系统或依赖项对测试进行分类非常有用。例如,如果您的某些测试需要访问数据库,那么您可以为它们创建一个 @pytest.mark.database_access 标记

    当需要运行测试时,您仍然可以默认使用 pytest 命令运行它们。如果您只想运行那些需要访问数据库的测试,那么您可以使用 pytest -m database_access。要运行除需要访问数据库的测试之外的所有测试,您可以使用 pytest -m "not database_access"。您甚至可以使用自动使用装置来限制对那些标有 database_access 的测试的数据库访问。

    一些插件通过保护对资源的访问来扩展标记的功能。 pytest-django 插件提供了一个 django_db 标记。任何没有此标记的尝试访问数据库的测试都将失败。尝试访问数据库的第一个测试将触发 Django 测试数据库的创建。

    添加 django_db 标记的要求会促使您明确声明您的依赖项。毕竟,这就是 pytest 的哲学!这也意味着您可以更快地运行不依赖数据库的测试,因为 pytest -m "not django_db" 将阻止测试触发数据库创建。节省的时间确实加起来,特别是如果您勤于频繁地运行测试。

    5.1 pytest 提供了一些开箱即用的标记

    开箱即用的标记

    1)skip 无条件跳过测试。

    2)如果传递给它的表达式的计算结果为 True,skipif 将跳过测试。

    3)xfail 表示测试预计会失败,因此如果测试确实失败,整个套件仍会导致通过状态。

    4)parametrize(注意拼写)以不同的值作为参数创建测试的多个变体。您很快就会了解有关此标记的更多信息。

    6.参数化

    6.1组合测试

    如何使用 pytest 固定装置通过提取公共依赖项来减少代码重复。当你有几个输入和预期输出略有不同的测试时,在这些情况下,您可以参数化单个测试定义,pytest 将使用您指定的参数为您创建测试的变化。假设您编写了一个函数来判断字符串是否为回文。一组初始测试可能如下所示:

    用这种参数的方法:把参数进行字典的方式传入

    from unittestimport TestCase

    import pytest

    def is_palindrome(palindrome):

    return palindrome

    class Testing2(TestCase):

    @pytest.mark.parametrize("palindrome", [

    "",

            "a",

            "Bob",

            "Never odd or even",

            "Do geese see God?",

        ])

    def test_is_palindrome(palindrome):

    assert is_palindrome(palindrome)

    @pytest.mark.parametrize("non_palindrome", [

    "abc",

            "abab",

        ])

    def test_is_palindrome_not_palindrome(non_palindrome):

    assert not is_palindrome(non_palindrome)

    6.2 运行效果

    test2.py .F [100%]

    =====FAILURES_ Testing2.test_is_palindrome_not_palindrome____________

    non_palindrome = <test.test2.Testing2 testMethod=test_is_palindrome_not_palindrome>

        @pytest.mark.parametrize("non_palindrome", [

            "abc",

            "abab",

        ])

        def test_is_palindrome_not_palindrome(non_palindrome):

    >      assert not is_palindrome(non_palindrome)

    E      AssertionError

    test2.py:26: AssertionError

    short test summary info FAILED test2.py::Testing2::test_is_palindrome_not_palindrome - AssertionError

    =====1 failed, 1 passed in 0.17s  =====

    parametrize() 的第一个参数是以逗号分隔的参数名称字符串。第二个参数是表示参数值的元组或单个值的列表。可以进一步进行参数化,将所有测试合并为一个,代码更加清晰好看。

    7.有用的 pytest 插件

    7.1  pytest-randomly

    pytest-randomly 做了一些看似简单但具有重要作用的事情:它强制您的测试以随机顺序运行。 pytest 总是在运行它们之前收集它可以找到的所有测试,因此 pytest-randomly 在执行之前打乱该测试列表。

    这是发现依赖于以特定顺序运行的测试的好方法,这意味着它们对某些其他测试有状态依赖。如果您在 pytest 中从头开始构建测试套件,那么这不太可能。它更有可能发生在您迁移到 pytest 的测试套件中。

    该插件将在配置描述中打印一个种子值。您可以使用该值以与尝试修复问题相同的顺序运行测试

    可以这么用:pytest --randomly-seed=1234 或者是:pytest --randomly-seed=1234  如果测试由于排序或随机创建的数据而失败,您可以按照建议使用该标志使用该种子重新启动它们

    7.2 pytest-django

    pytest-django 提供了一些有用的装置和标记来处理 Django 测试。您在本教程前面看到了 django_db 标记,并且 rf 夹具提供了对 Django 的 RequestFactory 实例的直接访问。设置夹具提供了一种快速设置或覆盖 Django 设置的方法。这极大地提高了您的 Django 测试效率!

    如果您有兴趣了解有关在 Django 中使用 pytest 的更多信息,请查看如何在 Pytest 中为 Django 模型提供测试装置。

    相关文章

      网友评论

        本文标题:使用 Pytest 进行有效的 Python 测试

        本文链接:https://www.haomeiwen.com/subject/xldibltx.html