第 3 章 pytest夹具

既然你已经用 pytest 编写并运行了测试函数,那么让我们把注意力转移到名为 fixtures 的测试辅助函数上吧。Fixturespytest 在实际测试函数之前(有时是之后)运行的函数。夹具(fixture)中的代码可以做任何你想做的事。你可以使用夹具获取数据集,供测试使用。在运行测试之前,可以使用夹具让系统进入已知状态。夹具还可用于为多个测试准备数据。

在本章中,你将学习如何创建夹具,并学习如何使用它们。你还将学习如何构建夹具,以容纳设置和拆卸代码。你将使用作用域让夹具在多个测试中运行一次,并学习测试如何使用多个夹具。你还将学习如何通过夹具和测试代码跟踪代码执行。

但首先,在了解夹具的来龙去脉并使用它们来帮助测试 Cards 之前,让我们先来看看一个小型的夹具示例,以及夹具和测试功能是如何连接的。

夹具入门

下面是一个返回数字的简单夹具:

"""Demonstrate simple fixtures."""

import pytest


@pytest.fixture()
def some_data():
    """Return answer to ultimate question."""
    return 42
def test_some_data(some_data):
    """Use fixture return value in a test."""
    assert some_data == 42

@pytest.fixture() 装饰器用于告诉 pytest 某个函数是一个夹具。当你在测试函数的参数列表中包含夹具名称时,pytest 就会知道在运行测试之前要先运行它。夹具可以执行工作,也可以向测试函数返回数据。

pytest 使用装饰器为其他函数添加功能和特性。在本例中,pytest.fixture() 正在装饰 some_data() 函数。测试 test_some_data() 的参数是夹具名称 some_data

在编程和测试社区,甚至在 Python 社区,"fixture" 一词有多种含义。我交替使用 "fixture"、"fixture 函数" 和 "fixture 方法" 来指本章讨论的 @pytest.fixture() 装饰函数。Fixture 也可以用来指被 Fixture 函数设置的资源。夹具函数通常会设置或检索一些测试可以使用的数据。有时,这些数据也被视为夹具。例如,Django 社区经常用 fixture 来指应用程序开始时加载到数据库中的一些初始数据。

无论其他含义如何,在 pytest 和本书中,测试夹具指的是 pytest 提供的一种机制,它允许将 "准备" 和 "清理" 代码从测试函数中分离出来。

与测试函数相比,pytest 在夹具中对异常的处理是不同的。在测试代码中发生的异常(或断言失败或调用 pytest.fail())会导致 "Fail"(失败)结果。然而,在测试夹具中,测试函数报告为 "错误"。这种区别有助于调试测试未通过的原因。如果测试结果为 "Fail"(失败),那么失败就发生在测试函数的某个地方(或函数调用的某个地方)。如果测试结果是 "Error"(错误),那么故障就出现在夹具中。

pytest fixtures 是使 pytest 傲视其他测试框架的独特核心特性之一,也是许多人转用并继续使用 pytest 的原因。关于夹具,有很多特性和细微差别。一旦你对它们的工作原理有了一个良好的心理模型,它们对你来说就会显得很容易。不过,你必须玩上一段时间才能达到目的,所以接下来我们就来玩玩夹具。

使用夹具进行安装(Setup)和拆卸(Teardown)

夹具对我们测试 Cards 应用程序大有帮助。卡片 "应用程序设计有一个 API(完成大部分工作和逻辑)和一个精简的 CLI。特别是由于用户界面的逻辑非常简单,因此将大部分测试工作集中在 API 上将会给我们带来最大的效益。Cards 应用程序还使用数据库,而处理数据库则需要使用夹具。

确保 Cards 已安装

本章中的示例需要安装 Cards 应用程序。 如果您尚未安装 Cards 应用程序,请务必使用 cd 代码安装它; pip install ./cards_proj。 有关详细信息,请参阅安装示例应用程序章节。

让我们首先为支持计数功能的 count() 方法编写一些测试。 提醒一下,让我们在命令行上玩一下 count

$ cards count
0
$ cards add first item
$ cards add second item
$ cards count
2

检查计数是否从零开始的初始测试可能如下所示:

ch3/test_count_initial.py
from pathlib import Path
from tempfile import TemporaryDirectory
import cards


def test_empty():
    with TemporaryDirectory() as db_dir:
        db_path = Path(db_dir)
        db = cards.CardsDB(db_path)

        count = db.count()
        db.close()

        assert count == 0

为了调用 count(),我们需要一个数据库对象,该对象是通过调用 cards.CardsDB(db_path) 获得的。cards.CardsDB() 函数是一个构造函数; 它返回一个 CardsDB 对象。 db_path 参数需要是指向数据库目录的 pathlib.Path 对象。 Pathlib 模块是在 Python 3.4 中引入的,pathlib.Path 对象是表示文件系统路径的标准方式。 为了进行测试,我们从 tempfile.TemporaryDirectory() 获得了一个临时目录。 还有其他方法可以完成所有这一切,但目前这种方法有效。

这个测试功能确实不太痛苦。 只需几行代码。 无论如何,让我们看看问题所在。 在调用 count() 之前有一些代码用于设置数据库,但这并不是我们真正想要测试的。 在断言语句之前有对 db.close() 的调用。 把它放在函数的末尾似乎更好,但我们必须在断言之前调用它,因为如果断言语句失败,它就不会被调用。

这些问题可以通过 pytest 夹具解决:

ch3/test_count.py
import pytest


@pytest.fixture()
def cards_db():
    with TemporaryDirectory() as db_dir:
        db_path = Path(db_dir)
        db = cards.CardsDB(db_path)
        yield db
        db.close()


def test_empty(cards_db):
    assert cards_db.count() == 0

我们立即可以看到测试函数本身更容易阅读,因为我们已将所有数据库初始化推送到名为 cards_db 的夹具中。

cards_db 装置正在通过准备好数据库来 “设置” 测试。 然后它生成数据库对象。 那就是测试开始运行的时候。 然后在测试运行后,它关闭数据库。

夹具函数在使用它们的测试之前运行。 如果函数中有一个 yield,它会在那里停止,将控制传递给测试,并在测试完成后继续下一行。 yield 上方的代码是 “setup”,yield 之后的代码是 “teardown”。 无论测试期间发生什么,yield 后的代码(拆卸)都保证运行。

在我们的示例中,yield 发生在带有临时目录块的上下文管理器中。 在使用装置和运行测试时,该目录会保留下来。 测试完成后,控制权传回夹具,db.close() 可以运行,然后 with 块可以完成并清理目录。

请记住:pytest 查看测试参数的具体名称,然后查找具有相同名称的夹具。 我们从不直接调用 fixture 函数。 pytest 为我们做到了这一点。

您可以在多个测试中使用夹具。这是另一个:

ch3/test_count.py
def test_two(cards_db):
    cards_db.add_card(cards.Card("first"))
    cards_db.add_card(cards.Card("second"))
    assert cards_db.count() == 2

test_two() 使用相同的 cards_db 夹具。 这次,我们使用空数据库并添加两张卡片,然后检查计数。 我们现在可以使用 cards_db 进行任何需要配置数据库才能运行的测试。 单独的测试,例如 test_empty()test_two() 可以保持较小,并专注于我们正在测试的内容,而不是设置和拆卸位。

夹具和测试函数是独立的函数。 仔细命名您的装置以反映装置中正在完成的工作或从装置返回的对象或两者,将有助于提高可读性。

在编写和调试测试函数时,可视化夹具的设置和拆卸部分相对于使用它们的测试的运行时间通常很有帮助。下一节将介绍 --setup-show 以帮助实现此可视化。

使用 –setup-show 跟踪夹具执行

现在我们有两个使用相同夹具的测试,准确地知道所有内容被调用的顺序会很有趣。

幸运的是,pytest 提供了命令行标志 --setup-show,它向我们展示了测试和装置的操作顺序,包括装置的设置和拆卸阶段:

$ cd /path/to/code/ch3
$ pytest --setup-show test_count.py
======================== test session starts =========================
collected 2 items
test_count.py
SETUP F cards_db
ch3/test_count.py::test_empty (fixtures used: cards_db).
TEARDOWN F cards_db
SETUP F cards_db
ch3/test_count.py::test_two (fixtures used: cards_db).
TEARDOWN F cards_db
========================= 2 passed in 0.02s ==========================

我们可以看到我们的测试正在运行,周围是 cards_db 夹具的 SETUPTEARDOWN 部分。 夹具名称前面的 F 表示该夹具正在使用函数作用域,这意味着该夹具在每个使用它的测试函数之前被调用,并在每个使用它的函数之后被拆除。 接下来我们看一下范围作用域。

指定夹具范围

每个夹具都有一个特定的范围,它定义了相对于使用该夹具运行所有测试函数的设置和拆卸运行的顺序。 范围决定了当多个测试函数使用设置和拆卸时运行的频率。

夹具的默认作用域是函数作用域。 这意味着夹具的设置部分将在需要运行的每个测试之前运行。 同样,对于每个测试,拆卸部分在测试完成后运行。

然而,有时您可能不希望这种情况发生。 也许设置和连接到数据库非常耗时,或者您正在生成大量数据,或者您正在从服务器或慢速设备检索数据。 事实上,你可以在一场比赛中做任何你想做的事情,其中一些可能会很慢。

我可以向您展示一个示例,当我们连接到数据库以模拟缓慢的资源时,我在夹具中放置了 time.sleep(1) 语句,但我认为您想象它就足够了。 因此,如果我们想在示例中避免两次缓慢的连接,或者想象 100 秒进行一百次测试,我们可以更改范围,使慢速部分在多次测试中发生一次。

让我们更改夹具的范围,以便数据库仅打开一次,然后讨论不同的范围。

这是一行更改,将 scope = “module” 添加到 fixture 装饰器:

ch3/test_mod_scope.py
@pytest.fixture(scope="module")
def cards_db():
    with TemporaryDirectory() as db_dir:
        db_path = Path(db_dir)
        db = cards.CardsDB(db_path)
        yield db
        db.close()

现在让我们再次运行它:

$ pytest --setup-show test_mod_scope.py
========================== test session starts ==========================
collected 2 items
test_mod_scope.py
SETUP M cards_db
ch3/test_mod_scope.py::test_empty (fixtures used: cards_db).
ch3/test_mod_scope.py::test_two (fixtures used: cards_db).
TEARDOWN M cards_db
=========================== 2 passed in 0.03s ===========================

哇!我们为第二个测试函数节省了假想的一秒设置时间。对模块范围的更改允许此模块中使用 cards_db 夹具的任何测试共享它的相同实例,并且不会产生额外的设置/拆卸时间。

固定装饰器范围参数允许的不仅仅是函数(function)和模块(module)。还有类(class)、包(package)和会话(session)。默认范围是函数(function)。

以下是每个范围(scope)值的概要:

scope='function'

每个测试函数运行一次。 设置部分在使用夹具的每次测试之前运行。 每次使用夹具进行测试后都会运行拆卸部分。 这是未指定范围参数时使用的默认范围。

scope='class'

每个测试类运行一次,无论类中有多少测试方法。

scope='module'

每个模块运行一次,无论模块中有多少测试函数或方法或其他装置使用它。

scope='package'

每个包或测试目录运行一次,无论包中有多少测试函数或方法或其他装置使用它。

scope='session'

每个会话运行一次。 使用会话范围夹具的所有测试方法和函数共享一个设置和拆卸调用。

范围由夹具定义。 我知道这从代码中显而易见,但确保您完全理解这一点很重要。 范围是在夹具的定义处设置的,而不是在调用它的地方设置的。 使用夹具的测试函数不控制夹具设置和拆除的频率。 通过在测试模块中定义的夹具,会话和包范围的作用就像模块范围一样。 为了使用这些其他范围,我们需要将它们放入 conftest.py 文件中。

通过 conftest.py 共享 Fixture

您可以将夹具放入单独的测试文件中,但要在多个测试文件之间共享夹具,您需要在与使用该文件的测试文件位于同一目录中或在某个父目录中使用 conftest.py 文件。 conftest.py 文件也是可选的。 它被 pytest 视为 “本地插件”,可以包含钩子函数和夹具。

首先,我们将 cards_db 夹具从 test_count.py 移出并移至同一目录中的 conftest.py 文件中:

ch3/a/conftest.py
from pathlib import Path
from tempfile import TemporaryDirectory
import cards
import pytest


@pytest.fixture(scope="session")
def cards_db():
    """CardsDB object connected to a temporary database"""
    with TemporaryDirectory() as db_dir:
        db_path = Path(db_dir)
        db = cards.CardsDB(db_path)
        yield db
        db.close()
ch3/a/test_count.py
import cards


def test_empty(cards_db):
    assert cards_db.count() == 0


def test_two(cards_db):
    cards_db.add_card(cards.Card("first"))
    cards_db.add_card(cards.Card("second"))
    assert cards_db.count() == 2

是的,它仍然有效:

$ cd /path/to/code/ch3/a/
$ pytest --setup-show test_count.py
========================== test session starts ==========================
collected 2 items

test_count.py
SETUP S cards_db
ch3/a/test_count.py::test_empty (fixtures used: cards_db).
ch3/a/test_count.py::test_two (fixtures used: cards_db).
TEARDOWN S cards_db
=========================== 2 passed in 0.01s ===========================

夹具只能依赖于相同作用域或范围更广的其他夹具。因此,一个函数作用域的夹具可以依赖于其他函数作用域的夹具(默认情况,目前已在 Cards 项目中使用)。函数作用域的夹具也可以依赖于类、模块和会话作用域的夹具,但不能按相反的顺序依赖。

不用导入 conftest.py

尽管 conftest.py 是一个 Python 模块,但它不应该由测试文件导入。 conftest.py 文件会被 pytest 自动读取,因此您无需在任何地方导入 conftest

查找定义装置的位置

我们已将一个夹具从测试模块中移出并移入 conftest.py 文件中。 我们可以在测试目录的每个级别都有 conftest.py 文件。 测试可以使用与测试函数位于同一测试模块中的任何夹具,或者同一目录中的 conftest.py 文件中的任何夹具,或者位于测试根目录的任何级别的父目录中的夹具。

如果我们不记得特定设备的位置并且想要查看源代码,就会出现问题。 当然,pytest 是我们的后盾。 只需使用 --fixtures 就可以了。

我们先来尝试一下:

$ cd /path/to/code/ch3/a/
$ pytest --fixtures -v
...
-------------------- fixtures defined from conftest ---------------------
cards_db [session scope] -- conftest.py:7
CardsDB object connected to a temporary database
...

pytest 向我们展示了我们的测试可以使用的所有可用夹具的列表。 该列表包括我们将在下一章中介绍的一系列内置夹具,以及插件提供的夹具。 conftest.py 文件中找到的夹具位于底部。 如果您提供一个目录,pytest 将列出该目录中可用于测试的夹具。 如果您提供测试文件名,pytest 也将包含测试模块中定义的文件名。

pytest 还包括夹具中文档字符串的第一行(如果您已定义),以及定义夹具的文件和行号。 如果该路径不在当前目录中,它还将包含该路径。

添加 -v 将包含整个文档字符串。 请注意,对于 pytest 6.x,我们必须使用 -v 来获取路径和行号。 对于 pytest 7,这些已添加到 --fixturues 中,没有详细信息。

您还可以使用 --fixtures-per-test 来查看每个测试使用哪些装置以及定义装置的位置:

$ pytest --fixtures-per-test test_count.py::test_empty
=========================== test session starts ===========================
collected 1 item
----------------------- fixtures used by test_empty -----------------------
---------------------------- (test_count.py:4) ----------------------------
cards_db -- conftest.py:7
CardsDB object connected to a temporary database
========================== no tests ran in 0.00s ==========================

在此示例中,我们指定了一个单独的测试 test_count.py::test_empty。 但是,该标志也适用于文件或目录。 有了 --fixtures--fixtures-per-test 的帮助,您将永远不会再想知道夹具是在哪里定义的。

使用多个夹具级别

现在我们的测试代码有一点问题。 问题是测试都依赖于数据库为空开始,但它们在模块范围和会话范围版本中使用相同的数据库实例。

如果我们添加第三个测试,问题就会变得非常清楚:

ch3/a/test_three.py
import cards


def test_three(cards_db):
    cards_db.add_card(cards.Card("first"))
    cards_db.add_card(cards.Card("second"))
    cards_db.add_card(cards.Card("third"))
    assert cards_db.count() == 3

它本身工作得很好,但在 test_count.py::test_two 之后运行时就不行了:

$ pytest -v test_three.py
========================== test session starts ==========================
collected 1 item
test_three.py::test_three PASSED [100%]
=========================== 1 passed in 0.01s ===========================

$ pytest -v --tb=line test_count.py test_three.py
========================== test session starts ==========================
collected 3 items
test_count.py::test_empty PASSED [ 33%]
test_count.py::test_two PASSED [ 66%]
test_three.py::test_three FAILED [100%]
=============================== FAILURES ================================
/path/to/code/ch3/a/test_three.py:8: assert 5 == 3
======================== short test summary info ========================
FAILED test_three.py::test_three - assert 5 == 3
====================== 1 failed, 2 passed in 0.01s ======================

数据库中有五个元素,因为之前的测试在 test_three 运行之前添加了两个项目。 有一条历史悠久的经验法则,即测试不应依赖于运行顺序。 显然,确实如此。 如果我们单独运行 test_third,则可以正常通过,但如果在 test_two 之后运行,则失败。

如果我们仍然想尝试坚持使用一个打开的数据库,但以数据库中的零元素开始所有测试,我们可以通过添加另一个夹具来做到这一点:

ch3/b/conftest.py
from pathlib import Path
from tempfile import TemporaryDirectory
import cards
import pytest


@pytest.fixture(scope="session")
def db():
    """CardsDB object connected to a temporary database"""
    with TemporaryDirectory() as db_dir:
        db_path = Path(db_dir)
        db_ = cards.CardsDB(db_path)
        yield db_
        db_.close()


@pytest.fixture(scope="function")
def cards_db(db):
    """CardsDB object that's empty"""
    db.delete_all()
    return db

我已将旧的 cards_db 重命名为 db 并使其成为会话范围。

cards_db 夹具在其参数列表中指定了 db,这意味着它依赖于 db 夹具。 此外,cards_db 是函数作用域,它的作用域比 db 更窄。 当夹具依赖于其他夹具时,它们只能使用具有相同或更宽范围的夹具。

让我们看看它是否有效:

$ cd /path/to/code/ch3/b/
$ pytest --setup-show
========================== test session starts ==========================
collected 3 items
test_count.py
SETUP S db
SETUP F cards_db (fixtures used: db)
ch3/b/test_count.py::test_empty (fixtures used: cards_db, db).
TEARDOWN F cards_db
SETUP F cards_db (fixtures used: db)
ch3/b/test_count.py::test_two (fixtures used: cards_db, db).
TEARDOWN F cards_db
test_three.py
SETUP F cards_db (fixtures used: db)
ch3/b/test_three.py::test_three (fixtures used: cards_db, db).
TEARDOWN F cards_db
TEARDOWN S db
=========================== 3 passed in 0.01s ===========================

我们可以看到 db 的设置首先发生,并且具有会话范围(来自 S)。 cards_db 的设置接下来发生,在每个测试函数调用之前,并且具有函数范围(从 F 开始)。 此外,所有三个测试均通过。

使用这样的多级夹具可以提供一些令人难以置信的速度优势并保持测试顺序的独立性。

每个测试或夹具使用多个夹具

我们使用多个夹具的另一种方法是在一个函数或一个夹具中使用多个夹具。 例如,我们可以将一些预先固定的任务放在一起作为夹具进行测试:

ch3/c/conftest.py
@pytest.fixture(scope="session")
def some_cards():
    """List of different Card objects"""
    return [
        cards.Card("write book", "Brian", "done"),
        cards.Card("edit book", "Katie", "done"),
        cards.Card("write 2nd edition", "Brian", "todo"),
        cards.Card("edit 2nd edition", "Katie", "todo"),
    ]

然后我们可以在测试中同时使用 empty_dbsome_cards

ch3/c/test_some.py
def test_add_some(cards_db, some_cards):
    expected_count = len(some_cards)
    for c in some_cards:
        cards_db.add_card(c)
    assert cards_db.count() == expected_count

夹具还可以使用多个其他夹具:

ch3/c/conftest.py
@pytest.fixture(scope="function")
def non_empty_db(cards_db, some_cards):
    """CardsDB object that's been populated with 'some_cards'"""
    for c in some_cards:
        cards_db.add_card(c)
    return cards_db

夹具 non_empty_db 必须是函数作用域,因为它使用 cards_db,这是函数作用域。 如果您尝试使 non_empty_db 模块范围或更宽,pytest 将抛出错误。 请记住,如果您不指定作用域,您将获得函数作用域夹具。

现在,需要数据库的测试可以轻松做到这一点:

ch3/c/test_some.py
def test_non_empty(non_empty_db):
    assert non_empty_db.count() > 0

我们已经讨论了不同的夹具范围如何工作以及如何在不同的夹具中使用不同的范围以发挥我们的优势。 但是,有时您可能需要在运行时确定范围。 这可以通过动态作用域来实现。

动态决定夹具范围

假设我们像现在一样设置了夹具,其中 db 在会话范围内,cards_db 在函数范围内,但我们对此感到担心。 cards_db 夹具是空的,因为它调用了 delete_all()。 但是,如果我们还不完全信任这个 delete_all() 函数,并且想要采取某种方法来为每个测试函数完全设置数据库怎么办?

我们可以通过在运行时动态确定数据库夹具的范围来做到这一点。 首先,我们更改 db 的范围:

ch3/d/conftest.py
def db_scope(fixture_name, config):
    if config.getoption("--func-db", None):
        return "function"
    return "session"

我们可以通过多种方法来确定要使用的作用域,但在本例中,我选择依赖一个新的命令行标志 --func-db。 为了让 pytest 能够使用这个新标志,我们需要编写一个钩子函数(我将在 【第 15 章 “构建插件”,第 205 页】中更深入地介绍该函数):

ch3/d/conftest.py
def pytest_addoption(parser):
    parser.addoption(
        "--func-db",
        action="store_true",
        default=False,
        help="new db for each test",
    )

毕竟,默认行为与以前相同,具有会话范围 db

$ pytest --setup-show test_count.py
========================== test session starts ==========================
collected 2 items
test_count.py
SETUP S db
SETUP F cards_db (fixtures used: db)
ch3/d/test_count.py::test_empty (fixtures used: cards_db, db).
TEARDOWN F cards_db
SETUP F cards_db (fixtures used: db)
ch3/d/test_count.py::test_two (fixtures used: cards_db, db).
TEARDOWN F cards_db
TEARDOWN S db
=========================== 2 passed in 0.01s ===========================

但是当我们使用新标志时,我们得到一个函数范围的 db 夹具:

$ pytest --func-db --setup-show test_count.py
=========================== test session starts ===========================
collected 2 items
test_count.py
    SETUP F db
    SETUP F cards_db (fixtures used: db)
    ch3/d/test_count.py::test_empty (fixtures used: cards_db, db).
    TEARDOWN F cards_db
    TEARDOWN F db
    SETUP F db
    SETUP F cards_db (fixtures used: db)
    ch3/d/test_count.py::test_two (fixtures used: cards_db, db).
    TEARDOWN F cards_db
    TEARDOWN F db
============================ 2 passed in 0.01s ============================

现在,数据库在每个测试函数之前建立,并在之后拆除。

将 autouse 用于经常使用的夹具

到目前为止,在本章中,测试使用的所有装置均由测试或参数列表中的另一个装置命名。 但是,您可以使用 autouse=True 来让夹具始终运行。 这对于您想要在特定时间运行的代码非常有效,但测试并不真正依赖于任何系统状态或来自夹具的数据。

这是一个人为构造的例子:

ch3/test_autouse.py
import pytest
import time


@pytest.fixture(autouse=True, scope="session")
def footer_session_scope():
    """Report the time at the end of a session."""
    yield
    now = time.time()
    print("--")
    print(
        "finished : {}".format(
            time.strftime("%d %b %X", time.localtime(now))
        )
    )
    print("-----------------")


@pytest.fixture(autouse=True)
def footer_function_scope():
    """Report test durations after each function."""
    start = time.time()
    yield
    stop = time.time()
    delta = stop - start
    print("\ntest duration : {:0.3} seconds".format(delta))


def test_1():
    """Simulate long-ish running test."""
    time.sleep(1)


def test_2():
    """Simulate slightly longer test."""
    time.sleep(1.23)

我们希望在每次测试后添加测试时间,并在会话结束时添加日期和当前时间。下面就是这些内容的样子:

$ cd /path/to/code/ch3
$ pytest -v -s test_autouse.py
===================== test session starts ======================
collected 2 items
test_autouse.py::test_1 PASSED
test duration : 1.0 seconds
test_autouse.py::test_2 PASSED
test duration : 1.24 seconds
--
finished : 25 Jul 16:18:27
-----------------
=================== 2 passed in 2.25 seconds ===================

我在这个例子中使用了 -s 标志。 这是 --capture=no 的快捷方式标志,告诉 pytest 关闭输出捕获。 我使用它是因为新的夹具具有打印功能,并且我想查看输出。 在不关闭输出捕获的情况下,pytest 仅打印失败测试的输出。

autouse 功能很好用。 但这更多的是例外而不是规则。 选择指定的夹具,除非你有充分的理由不这样做。

重命名夹具

在测试和使用它的其他夹具的参数列表中列出的夹具名称,通常与夹具的函数名称相同。不过,pytest 允许使用 @pytest.fixture()name 参数来重命名夹具:

ch3/test_rename_fixture.py
import pytest


@pytest.fixture(name="ultimate_answer")
def ultimate_answer_fixture():
    return 42


def test_everything(ultimate_answer):
    assert ultimate_answer == 42

我遇到过一些需要重命名的例子。 如本例所示,有些人喜欢使用 _fixture 后缀或 fixture_ 前缀或类似名称来命名他们的装置。

重命名很有用的一种情况是,最明显的夹具名称已经作为现有变量或函数名称存在:

ch3/test_rename_2.py
import pytest
from somewhere import app


@pytest.fixture(scope="session", name="app")
def _app():
    """The app object"""
    yield app()


def test_that_uses_app(app):
    assert app.some_property == "something"

我通常只对与使用它的测试位于同一模块中的夹具使用夹具重命名,因为重命名夹具会使找到它的定义位置变得更加困难。 但是,请记住,总会有 --fixtures,它可以帮助您找到夹具所在的位置。

回顾

在本章中,我们讨论了很多关于夹具的内容:

  • Fixtures 是 @pytest.fixture() 修饰的函数。

  • 测试函数或其他夹具通过将其名称放入其参数列表中来依赖于夹具。

  • 夹具可以使用 return 或 yield 返回数据。

  • yield 之前的代码是设置代码。yield 之后的代码是拆卸代码。

  • 夹具可设置为函数、类、模块、包或会话范围。 默认是函数作用域。 您甚至可以动态定义范围。

  • 多个测试函数可以使用同一个夹具。

  • 如果同一夹具位于 conftest.py 文件中,则多个测试模块可以使用该夹具。

  • 不同范围的多个夹具可以加速测试套件,同时保持测试隔离。

  • 测试和夹具可以使用多个夹具。

  • 自动使用夹具不必由测试函数命名。

  • 您可以使夹具名称与夹具函数名称不同。

我们还介绍了一些新的命令行标志:

  • pytest --setup-show 用于查看执行顺序。

  • pytest --fixtures 用于列出可用的夹具以及夹具所在的位置。

  • -s 和—​capture=no 即使在通过测试时也允许看到打印语句。

练习

Fixtures 通常是 pytest 中最难让人适应的部分之一。 通过以下练习将:

  • 帮助您巩固对装置如何工作的理解,

  • 允许您使用不同的装置范围,以及

  • 通过 --setup-show 的可视化输出来内部化运行顺序。

    1. 创建一个名为 test_fixtures.py 的测试文件。

    2. 编写一些数据夹具(带有 @pytest.fixture() 装饰器的函数),返回一些数据(可能是列表、字典或元组)。

    3. 对于每个夹具,至少编写一个使用它的测试函数。

    4. 编写两个使用相同夹具的测试。

    5. 运行 pytest --setup-show test_fixtures.py。 每次测试前是否运行所有夹具?

    6. 将 scope='module' 添加到练习 4 中的夹具中。

    7. 重新运行 pytest --setup-show test_fixtures.py。 发生了什么变化?

    8. 对于练习 6 中的装置,将 return <data> 更改为 yield <data>。

    9. 在 yield 前后添加打印语句。

    10. 运行 pytest -s -v test_fixtures.py。 输出有意义吗?

    11. 运行 pytest --fixtures。 您能看到列出的夹具吗?

    12. 如果您还没有包含文档字符串,请将文档字符串添加到您的其中一个设备中。 重新运行 pytest --fixtures 以查看显示的描述。

下一步

pytest 夹具实现足够灵活,可以使用像构建块这样的夹具来构建测试设置和拆卸。 因为夹具非常灵活,所以我大量使用它们来将尽可能多的测试设置推入夹具中。

在本章中,我们研究了您自己编写的 pytest 夹具,但 pytest 提供了大量有用的夹具供您开箱即用。 我们将在下一章中仔细研究一些夹具。