用Pytest进行的Python测试的完整教程

368 阅读6分钟

Effective Python Testing With Pytest

目录

立即观看本教程有一个由Real Python团队制作的相关视频课程。将它与书面教程一起观看,以加深你的理解。 用pytest测试你的代码

测试你的代码会带来各种各样的好处。它增加了你的信心,使代码的行为符合你的期望,并确保对你的代码的修改不会导致退步。编写和维护测试是一项艰巨的工作,所以你应该利用你所掌握的所有工具,使其尽可能不受影响。 pytest是你可以用来提高你的测试效率的最好的工具之一。

在本教程中,您将了解到:

  • pytest 有哪些好处
  • 如何确保你的测试是无状态
  • 如何使重复的测试更容易被理解
  • 如何按名称或自定义组运行测试子集
  • 如何创建和维护可重复使用的测试工具

如何安装pytest

为了跟上本教程中的一些例子,你需要安装pytest 。和大多数Python 包一样,pytestPyPI 上提供。你可以在虚拟环境中安装它,使用 pip:

PS> python -m venv venv
PS> .\venv\Scripts\activate
(venv) PS> python -m pip install pytest
$ python -m venv venv
$ source venv/bin/activate
(venv) $ python -m pip install pytest

pytest 命令现在将在你的安装环境中可用。

是什么让pytest 如此有用?

如果你以前为你的Python代码编写过单元测试,那么你可能使用过Python的内置模块。 **unittest**模块。unittest 提供了一个坚实的基础来建立你的测试套件,但它有一些不足之处。

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

如果你还没有有幸使用过pytest ,那么你将会是一种享受!它的理念和功能将使你的测试经验更富有成效和愉快。使用pytest ,普通的任务需要更少的代码,高级的任务可以通过各种节省时间的命令和插件来实现。它甚至可以开箱即用地运行你现有的测试,包括那些用unittest

与大多数框架一样,当你刚开始使用pytest ,一些开发模式是有意义的,但随着你的测试套件的增长,会开始造成痛苦。本教程将帮助你了解一些pytest 提供的工具,以保持你的测试的效率和效果,即使它的规模扩大。

更少的模板

大多数功能测试遵循安排-行动-暂停的模式。

  1. 安排,或设置测试的条件
  2. 通过调用一些函数或方法进行操作
  3. 断言某些结束条件为真

测试框架通常与测试的断言挂钩,以便在断言失败时提供信息。unittest举例来说,Amazon的测试框架提供了许多有用的断言工具,而且开箱即用。然而,即使是一个小的测试集也需要相当数量的模板代码

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

# test_with_unittest.py

from unittest import TestCase

class TryTesting(TestCase):
    def test_always_passes(self):
        self.assertTrue(True)

    def test_always_fails(self):
        self.assertTrue(False)

然后你可以使用unittestdiscover 选项从命令行运行这些测试。

(venv) $ python -m unittest discover
F.
======================================================================
FAIL: test_always_fails (test_with_unittest.TryTesting)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "...\effective-python-testing-with-pytest\test_with_unittest.py",
  line 10, in test_always_fails
    self.assertTrue(False)
AssertionError: False is not true

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

Ran 2 tests in 0.006s

FAILED (failures=1)

正如所料,一个测试通过,一个失败。你已经证明了unittest 是有效的,但看看你必须做什么。

  1. TestCase 类中导入unittest
  2. 创建TryTesting ,是 的一个子类TestCase
  3. TryTesting 中为每个测试写一个方法
  4. 使用unittest.TestCase 中的一个self.assert* 方法来进行断言。

这是一个相当大的代码量,而且因为这是你对任何测试的最低要求,你最终会重复写同样的代码。pytest 允许你直接使用普通函数和 Python 的assert 关键字,从而简化了这个工作流程。

# test_with_pytest.py

def test_always_passes():
    assert True

def test_always_fails():
    assert False

就是这样。你不需要处理任何导入或类。你所需要做的就是包含一个带有test_ 前缀的函数。因为你可以使用assert 关键字,你也不需要学习或记住unittest 中所有不同的self.assert* 方法。如果你可以写一个表达式,你期望它的值是True ,然后pytest ,就可以为你测试。

pytest 不仅消除了大量的模板,而且还为你提供了更加详细和容易阅读的输出。

更漂亮的输出

你可以在你项目的顶层文件夹中使用pytest 命令来运行你的测试套件。

(venv) $ pytest
============================= test session starts =============================
platform win32 -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: ...\effective-python-testing-with-pytest
collected 4 items

test_with_pytest.py .F                                                   [ 50%]
test_with_unittest.py F.                                                 [100%]

================================== FAILURES ===================================
______________________________ test_always_fails ______________________________

    def test_always_fails():
>       assert False
E       assert False

test_with_pytest.py:7: AssertionError
________________________ TryTesting.test_always_fails _________________________

self = <test_with_unittest.TryTesting testMethod=test_always_fails>

    def test_always_fails(self):
>       self.assertTrue(False)
E       AssertionError: False is not true

test_with_unittest.py:10: AssertionError
=========================== short test summary info ===========================
FAILED test_with_pytest.py::test_always_fails - assert False
FAILED test_with_unittest.py::TryTesting::test_always_fails - AssertionError:...

========================= 2 failed, 2 passed in 0.20s =========================

pytest 呈现测试结果的方式与 不同,而且 文件也被自动包括在内。该报告显示。unittest test_with_unittest.py

  1. 系统状态,包括你已经安装了哪些版本的 Python、pytest ,以及任何插件
  2. rootdir ,或在该目录下搜索配置和测试
  3. 运行器发现的测试的数量

这些项目显示在输出的第一部分。

============================= test session starts =============================
platform win32 -- Python 3.10.5, pytest-7.1.2, pluggy-1.0.0
rootdir: ...\effective-python-testing-with-pytest
collected 4 items

然后输出显示每个测试的状态,使用类似于unittest 的语法。

  • **一个点 (.)**表示测试通过。
  • 一个F,意味着测试失败。
  • **一个E**意味着该测试引发了一个意外的异常。

特殊字符显示在名称旁边,右边显示测试套件的整体进度。

test_with_pytest.py .F                                                   [ 50%]
test_with_unittest.py F.                                                 [100%]

对于失败的测试,报告给出了失败的详细情况。在这个例子中,测试失败是因为assert False 总是失败。

================================== FAILURES ===================================
______________________________ test_always_fails ______________________________

    def test_always_fails():
>       assert False
E       assert False

test_with_pytest.py:7: AssertionError
________________________ TryTesting.test_always_fails _________________________

self = <test_with_unittest.TryTesting testMethod=test_always_fails>

    def test_always_fails(self):
>       self.assertTrue(False)
E       AssertionError: False is not true

test_with_unittest.py:10: AssertionError

这种额外的输出在调试时非常方便。最后,报告给出了测试套件的整体状态报告。

=========================== short test summary info ===========================
FAILED test_with_pytest.py::test_always_fails - assert False
FAILED test_with_unittest.py::TryTesting::test_always_fails - AssertionError:...

========================= 2 failed, 2 passed in 0.20s =========================

与 unittest 相比,pytest 的输出信息量大得多,也更容易阅读。

在下一节,你会仔细看看pytest 是如何利用现有的assert 关键字的。

更少需要学习

能够使用 assert关键字也很强大。如果你以前使用过它,那么就没有什么新的东西需要学习。这里有几个断言的例子,这样你就可以了解到你可以做的测试类型了。

# test_assert_examples.py

def test_uppercase():
    assert "loud noises".upper() == "LOUD NOISES"

def test_reversed():
    assert list(reversed([1, 2, 3, 4])) == [4, 3, 2, 1]

def test_some_primes():
    assert 37 in {
        num
        for num in range(2, 50)
        if not any(num % div == 0 for div in range(2, num))
    }

它们看起来非常像普通的 Python 函数。所有这些使得pytest 的学习曲线比unittest 要浅,因为你不需要学习新的结构体就可以开始。

请注意,每个测试都相当小,而且自成一体。这是很常见的,你会看到长的函数名称,而且在一个函数中没有很多事情。这主要是为了使你的测试相互隔离,所以如果有什么问题,你可以清楚地知道问题出在哪里。一个很好的副作用是,在输出中的标签要好得多。

要看一个与主项目一起创建测试套件的例子,请看用TDD在Python中建立一个哈希表的教程。此外,你可以在准备下一次面试解析CSV文件时,自己做Python练习题来尝试测试驱动开发。

在下一节中,你将研究夹具,这是一个伟大的pytest功能,可以帮助你管理测试输入值。

更容易管理状态和依赖关系

你的测试经常会依赖于数据类型或测试替身,模拟你的代码可能会遇到的对象,如字典JSON文件。

通过unittest ,你可以将这些依赖关系提取到.setUp().tearDown() 方法中,这样类中的每个测试都可以利用它们。使用这些特殊的方法是很好的,但随着你的测试类越来越大,你可能无意中使测试的依赖性完全隐含。换句话说,通过孤立地看许多测试中的一个,你可能不会立即看到它依赖于其他东西。

随着时间的推移,隐含的依赖关系可能会导致复杂的代码纠结,你必须解开这些代码才能理解你的测试。测试应该有助于使你的代码更容易理解。如果测试本身难以理解,那么你可能就有麻烦了!

pytest 指南针采取了一种不同的方法。它引导你走向明确的依赖性声明,由于固定装置的存在,这些依赖性声明仍然是可重用的。 固定装置是可以为测试套件创建数据、测试替身或初始化系统状态的函数。任何想要使用夹具的测试必须明确地使用这个夹具函数作为测试函数的参数,所以依赖关系总是在前面说明。pytest

# fixture_demo.py

import pytest

@pytest.fixture
def example_fixture():
    return 1

def test_with_fixture(example_fixture):
    assert example_fixture == 1

看一下测试函数,你可以立即知道它依赖于一个夹具,而不需要检查整个文件中的夹具定义。

注意:你通常想把你的测试放到项目根层的自己的文件夹中,称为tests

关于结构化Python应用程序的更多信息,请查看关于这个主题的视频课程

固定程序也可以使用其他的固定程序,同样通过明确声明它们的依赖关系。这意味着,随着时间的推移,你的固定程序可以变得庞大和模块化。尽管将固定程序插入其他固定程序的能力提供了巨大的灵活性,但随着测试套件的增长,它也会使管理依赖关系变得更具挑战性。

在本教程的后面,你将学习更多关于固定装置的知识,并尝试一些处理这些挑战的技术。

易于过滤测试

随着测试套件的增长,你可能会发现你只想在一个功能上运行几个测试,而把整个套件保存起来备用。pytest 提供了一些方法。

  • 基于名字的过滤。你可以限制pytest 只运行那些完全合格的名称符合特定表达式的测试。你可以用-k 参数来做这个。
  • 目录范围。默认情况下,pytest 将只运行那些在当前目录下的测试。
  • 测试分类pytest 可以包括或排除来自你定义的特定类别的测试。你可以用-m 参数来做。

测试分类尤其是一个微妙的强大工具。pytest ,使你能够为任何你喜欢的测试创建标记,或自定义标签。一个测试可以有多个标签,你可以用它们来细化控制哪些测试要运行。在本教程的后面,你将看到一个关于 pytest 标记如何工作的例子,并学习如何在大型测试套件中使用它们。

允许测试参数化

当你测试处理数据或执行通用转换的函数时,你会发现自己写了很多类似的测试。他们可能只在被测试的代码的输入或输出方面有所不同。这需要重复测试代码,这样做有时会掩盖你要测试的行为。

unittest 提供了一种将几个测试集合成一个的方法,但它们在结果报告中并不显示为单独的测试。如果一个测试失败了,其他的都通过了,那么整个组仍然会返回一个失败的结果。 提供了自己的解决方案,每个测试可以独立通过或失败。在本教程的后面,你会看到pytest 如何用 来pytest 参数化测试

拥有一个基于插件的架构

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

虽然一些pytest 插件专注于特定的框架,如Django,但其他插件适用于大多数测试套件。你会在本教程的后面看到一些特定插件的细节

固定装置。管理状态和依赖关系

pytest 固定件是为你的测试提供数据、测试替身或状态设置的一种方式。固定装置是可以返回多种数值的函数。每个依赖于固定装置的测试必须明确地接受该固定装置作为参数。

何时创建固定程序

在本节中,你将模拟一个典型的测试驱动开发(TDD)工作流程。

想象一下,你正在编写一个函数,format_data_for_display() ,来处理一个API端点返回的数据。这些数据代表了一个人的列表,每个人都有一个给定的名字、姓氏和工作职位。该函数应该输出一个字符串列表,其中包括每个人的全名(他们的given_name ,后面是他们的family_name ),一个冒号,以及他们的title

# format_data.py

def format_data_for_display(people):
    ...  # Implement this!

按照良好的TDD方式,你会想首先为它写一个测试。你可以为此写下以下代码。

# test_format_data.py

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",
    ]

在写这个测试的时候,你突然想到你可能需要写另一个函数来把数据转换成逗号分隔的值,以便在Excel中使用。

# format_data.py

def format_data_for_display(people):
    ...  # Implement this!

def format_data_for_excel(people):
    ... # Implement this!

你的待办事项清单增加了!这很好!TDD的优点之一是它可以帮助你提前规划工作。format_data_for_excel() 函数的测试看起来与format_data_for_display() 函数非常相似。

# test_format_data.py

def test_format_data_for_display():
    # ...

def test_format_data_for_excel():
    people = [
        {
            "given_name": "Alfonsa",
            "family_name": "Ruiz",
            "title": "Senior Software Engineer",
        },
        {
            "given_name": "Sayid",
            "family_name": "Khan",
            "title": "Project Manager",
        },
    ]

    assert format_data_for_excel(people) == """given,family,title
Alfonsa,Ruiz,Senior Software Engineer
Sayid,Khan,Project Manager
"""

值得注意的是,这两个测试都必须重复定义people 变量,这是相当多的几行代码。

如果你发现自己写的几个测试都使用了相同的基础测试数据,那么夹具可能是你的未来。你可以把重复的数据拉到一个单一的函数中,用@pytest.fixture 来表示该函数是一个pytest fixture。

# test_format_data.py

import pytest

@pytest.fixture
def example_people_data():
    return [
        {
            "given_name": "Alfonsa",
            "family_name": "Ruiz",
            "title": "Senior Software Engineer",
        },
        {
            "given_name": "Sayid",
            "family_name": "Khan",
            "title": "Project Manager",
        },
    ]

# ...

你可以通过将函数引用作为参数添加到你的测试中来使用该夹具。注意,你不需要调用夹具函数。pytest 来处理这个问题。你将能够使用夹具函数的返回值作为夹具函数的名称。

# test_format_data.py

# ...

def test_format_data_for_display(example_people_data):
    assert format_data_for_display(example_people_data) == [
        "Alfonsa Ruiz: Senior Software Engineer",
        "Sayid Khan: Project Manager",
    ]

def test_format_data_for_excel(example_people_data):
    assert format_data_for_excel(example_people_data) == """given,family,title
Alfonsa,Ruiz,Senior Software Engineer
Sayid,Khan,Project Manager
"""

现在每个测试都明显缩短了,但仍然有一个明确的路径回到它所依赖的数据。一定要给你的夹具起一个具体的名字。这样,你就可以在将来编写新的测试时,迅速确定是否要使用它。

当你第一次发现夹具的力量时,你可能很想一直使用它们,但就像所有事情一样,需要保持平衡。

什么时候应该避免使用固定装置

固定器对于提取你在多个测试中使用的数据或对象是非常好的。然而,他们并不总是适合于要求数据有轻微变化的测试。在你的测试套件中使用固定程序并不比使用普通数据或对象更好。它甚至可能更糟,因为增加了一层指示性的东西。

与大多数抽象概念一样,需要一些实践和思考来找到正确的夹具使用水平。

然而,夹具可能是你的测试套件的一个组成部分。随着项目范围的扩大,规模的挑战也开始出现。任何类型的工具所面临的挑战之一是它如何处理规模使用,幸运的是,pytest 有一堆有用的功能,可以帮助你管理随着增长而来的复杂性。

如何大规模地使用固定装置

当你从你的测试中提取更多的夹具时,你可能会发现一些夹具可以从进一步的抽象中受益。在pytest ,夹具是模块化的。模块化意味着夹具可以被导入,可以导入其他模块,它们可以依赖和导入其他夹具。所有这些都允许你为你的用例组成一个合适的灯具抽象。

例如,你可能会发现在两个独立的文件或模块中的灯具有一个共同的依赖关系。在这种情况下,你可以把测试模块中的夹具移到更一般的夹具相关模块中。这样,你就可以把它们导入需要它们的任何测试模块中。当你发现自己在整个项目中反复使用一个夹具时,这是一个好方法。

如果你想让你的整个项目都能使用一个夹具而不需要导入它,一个特殊的配置模块叫做 conftest.py将允许你这样做。

pytest 在每个目录中寻找一个 模块。如果你把你的通用夹具添加到 模块中,那么你就可以在模块的父目录和任何子目录中使用该夹具,而不需要导入它。这是一个放置你最广泛使用的灯具的好地方。conftest.py conftest.py

固定器和conftest.py 的另一个有趣的用例是保护对资源的访问。想象一下,你已经为处理API调用的代码写了一个测试套件。你想确保测试套件不会进行任何真正的网络调用,即使有人不小心写了一个这样的测试。

pytest 提供一个 monkeypatchfixture来替换值和行为,你可以用它来达到很好的效果。

# conftest.py

import pytest
import requests

@pytest.fixture(autouse=True)
def disable_network_calls(monkeypatch):
    def stunted_get():
        raise RuntimeError("Network access not allowed during testing!")
    monkeypatch.setattr(requests, "get", lambda *args, **kwargs: stunted_get())

通过将disable_network_calls() 放在conftest.py 并添加autouse=True 选项,你确保在整个套件的每个测试中禁用网络调用。任何执行调用requests.get() 的代码的测试都会引发一个RuntimeError ,表明会发生一个意外的网络调用。

你的测试套件的数量正在增长,这给你带来了很大的信心,让你有信心做出改变,不至于意外地破坏东西。也就是说,随着你的测试套件的增长,它可能开始花费很长的时间。即使它不需要那么长的时间,也许你关注的是一些核心行为,这些行为会逐渐减少,并破坏大多数测试。在这些情况下,你可能想把测试运行器限制在只有某一类测试。

对测试进行分类

在任何大型测试套件中,当你试图快速迭代一个新功能时,避免运行所有的测试会很好。除了pytest ,运行当前工作目录下的所有测试的默认行为,或过滤功能,你可以利用标记的优势。

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

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

专业提示:因为你可以给你的标记任何你想要的名字,所以很容易打错或记错标记的名字。pytest 会警告你它在测试输出中不认识的标记。

你可以在pytest 命令中使用--strict-markers 标志,以确保你的测试中的所有标记都在你的pytest 配置文件中注册,pytest.ini 。它会阻止你运行你的测试,直到你注册任何未知标记。

关于注册标记的更多信息,请查看pytest 文档。

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

一些插件通过添加自己的防护措施来扩展mark的功能。例如,该 pytest-django插件,例如,提供了一个django_db 标记。任何没有这个标记的测试试图访问数据库都会失败。第一个试图访问数据库的测试将触发Django测试数据库的创建。

添加django_db 标记的要求促使你明确说明你的依赖关系。这就是pytest 的哲学,毕竟!这也意味着你可以更快速地运行不依赖数据库的测试,因为pytest -m "not django_db" 将阻止测试触发数据库的创建。节省的时间真的增加了,特别是如果你勤奋地经常运行你的测试。

pytest 提供了一些标记出来。

  • **skip**无条件地跳过一个测试。
  • **skipif**如果传递给它的表达式评估为True ,则跳过一个测试。
  • **xfail**表示一个测试预计会失败,所以如果测试真的失败了,整个套件仍然可以产生一个通过的状态。
  • **parametrize**用不同的值作为参数创建一个测试的多个变体。你很快就会了解到这个标记的更多信息。

你可以通过运行pytest --markers ,看到pytest 知道的所有标记的列表。

关于参数化的话题,接下来会讲到。

参数化 结合测试

你在本教程的前面看到了pytest fixtures如何通过提取共同的依赖关系来减少代码的重复。当你有几个输入和预期输出略有不同的测试时,固定程序就不太有用了。在这些情况下,你可以 Parametrize一个单一的测试定义,然后pytest ,用你指定的参数为你创建测试的变体。

想象一下,你写了一个函数来判断一个字符串是否是复数。一组初始的测试可以是这样的。

def test_is_palindrome_empty_string():
    assert is_palindrome("")

def test_is_palindrome_single_character():
    assert is_palindrome("a")

def test_is_palindrome_mixed_casing():
    assert is_palindrome("Bob")

def test_is_palindrome_with_spaces():
    assert is_palindrome("Never odd or even")

def test_is_palindrome_with_punctuation():
    assert is_palindrome("Do geese see God?")

def test_is_palindrome_not_palindrome():
    assert not is_palindrome("abc")

def test_is_palindrome_not_quite():
    assert not is_palindrome("abab")

除了最后两个,所有这些测试都有相同的形状。

def test_is_palindrome_<in some situation>():
    assert is_palindrome("<some string>")

这开始闻起来很像模板。到目前为止,pytest 已经帮助你摆脱了模板,现在它也不会让你失望。你可以使用@pytest.mark.parametrize() ,用不同的值来填充这个形状,大大减少你的测试代码。

@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)

parametrize() 的第一个参数是一个以逗号分隔的参数名称的字符串。你不必提供一个以上的名字,正如你在这个例子中看到的。第二个参数是一个代表参数值的图元或单值的列表。你可以把你的参数化更进一步,把所有的测试合并成一个。

@pytest.mark.parametrize("maybe_palindrome, expected_result", [
    ("", True),
    ("a", True),
    ("Bob", True),
    ("Never odd or even", True),
    ("Do geese see God?", True),
    ("abc", False),
    ("abab", False),
])
def test_is_palindrome(maybe_palindrome, expected_result):
    assert is_palindrome(maybe_palindrome) == expected_result

尽管这缩短了你的代码,但重要的是要注意,在这种情况下,你实际上失去了原始函数的一些描述性的性质。确保你没有把你的测试套件参数化到不可理解的地步。你可以使用参数化将测试数据与测试行为分开,这样就可以清楚地知道测试的内容,也可以使不同的测试案例更容易阅读和维护。

持久性报告 对抗缓慢的测试

每次你从实现代码切换到测试代码的上下文时,你都会产生一些开销。如果你的测试一开始就很慢,那么开销就会引起摩擦和挫折感。

你在前面读到当你运行你的套件时,使用标记来过滤掉慢的测试,但在某些时候你需要运行它们。如果你想提高你的测试速度,那么知道哪些测试可能提供最大的改进是很有用的。pytest 可以自动为你记录测试持续时间,并报告最主要的违规者。

使用pytest 命令的--durations 选项,在你的测试结果中包括一个持续时间报告。--durations 希望有一个整数值n ,并将报告最慢的n 的测试数量。一个新的部分将包括在你的测试报告中。

(venv) $ pytest --durations=5
...
============================= slowest 5 durations =============================
3.03s call     test_code.py::test_request_read_timeout
1.07s call     test_code.py::test_request_connection_timeout
0.57s call     test_code.py::test_database_read

(2 durations < 0.005s hidden.  Use -vv to show these durations.)
=========================== short test summary info ===========================
...

在持续时间报告中显示的每个测试都是一个很好的加速候选者,因为它所花费的时间超过了总测试时间的平均水平。请注意,短持续时间在默认情况下是隐藏的。正如报告中所阐述的,你可以通过将-vv--durations ,来增加报告的粗略程度并显示这些内容。

请注意,一些测试可能有一个看不见的设置开销。你在前面读到,第一个标有django_db 的测试将触发Django测试数据库的创建。durations 报告反映了在触发数据库创建的测试中设置数据库所需的时间,这可能会产生误导。

你已经在实现全面测试覆盖的路上了。接下来,你将会看一下丰富的pytest 插件生态系统中的一些插件。

有用的pytest 插件

在本教程的早期,你已经了解了一些有价值的pytest 插件。在本节中,你将更深入地探索这些和其他一些插件--从实用插件(如pytest-randomly )到特定库的插件(如用于 Django 的),无所不包。

pytest-randomly

通常情况下,测试的顺序并不重要,但随着代码库的增长,你可能会无意中引入一些副作用,如果不按顺序运行,会导致一些测试失败。

pytest-randomly强制你的测试以随机顺序运行。pytest ,在运行之前总是收集所有它能找到的测试。pytest-randomly ,只是在执行之前对测试的列表进行洗牌。

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

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

pytest-cov

如果你想测量你的测试对实现代码的覆盖程度,那么你可以使用覆盖率包。 pytest-cov集成了覆盖率,所以你可以运行pytest --cov 来查看测试覆盖率报告并在你的项目主页上夸耀它。

pytest-django

pytest-django提供了一些有用的固定装置和标记来处理Django测试。你在本教程的前面看到了django_db 标记。rf 夹具提供了直接访问Django的 RequestFactory.settings fixture提供了一种快速设置或覆盖Django设置的方法。这些插件对你的Django测试效率有很大的促进作用!

如果你有兴趣了解更多关于在Django中使用pytest ,那么请查看如何在Pytest中为Django模型提供测试夹具

pytest-bdd

pytest 可以用来运行超出传统单元测试范围的测试。行为驱动开发(BDD)鼓励编写可能的用户行为和期望的纯语言描述,然后你可以用它来决定是否实现一个特定的功能。pytest-bdd帮助你使用Gherkin为你的代码编写功能测试。

你可以通过这个广泛的第三方插件列表查看哪些其他插件可用于pytest

总结

pytest 提供了一套核心的生产力功能来过滤和优化你的测试,以及一个灵活的插件系统来进一步扩展其价值。无论你有一个巨大的传统的 套件,还是你从头开始一个新的项目, 都能为你提供一些东西。unittest pytest

在这个教程中,你学会了如何使用:

  • 用于处理测试依赖性、状态和可重用功能的固定装置
  • 对测试进行分类并限制对外部资源的访问的标记
  • 参数化,以减少测试之间的重复代码
  • 识别最慢的测试的持续时间
  • 用于与其他框架和测试工具集成的插件