pytest 中的装置:显式、模块化、可扩展

目录

什么是测试装置

回到测试装置

“请求”装置

测试装置可以请求其他装置

测试装置是可重用的

一个测试用例/装置一次可以请求多个装置

每次测试可以多次请求相同的测试装置(返回值将被缓存)

自动使用装置(您不必主动请求的装置)

范围:跨类、模块、包或会话共享测试装置

测试装置的范围

动态范围

装置错误

TearDown/CleanUp(装置的清理操作)

1.yield fixtures(推荐)

2. 直接添加finalizers

安全的TearDown

安全的装置结构

装置可用性

conftest.py:跨多个文件共享装置

来自第三方插件的装置

共享测试数据 

装置实例化顺序

首先执行范围更大的装置

执行相同顺序的装置需要参考依赖关系

autouse装置在其范围内首先执行

安全地运行多个断言语句

Fixtures 可以内省请求的测试上下文

使用标记将数据传递给装置

工厂即装置

参数化装置

使用带有参数化装置的标记

模块化:使用装置功能中的装置

按装置实例自动分组测试

在带有 usefixtures 的类和模块中使用装置

覆盖各个级别的装置

覆盖文件夹(conftest)级别的装置

覆盖测试模块级别的装置

使用直接测试参数化覆盖装置

用非参数化装置覆盖参数化装置,反之亦然

使用其他项目的装置


软件测试装置初始化测试功能。它们提供了一个固定的基线,以便可靠地执行测试并产生一致、可重复的结果。初始化可以设置服务、状态或其他操作环境。这些由测试函数通过参数访问;对于测试函数使用的每个装置,测试函数的定义中通常都有一个参数(以装置命名)。

pytest fixtures 提供了对经典 xUnit 风格的setup/teardown功能的显着改进:

  • 测试装置具有明确的名称,并通过在测试功能、模块、类或整个项目中声明其使用来激活。
  • 测试装置以模块化方式实现,因为每个测试装置名称触发一个测试装置函数,该函数本身可以使用其他测试装置。
  • 测试装置管理从简单的单元扩展到复杂的功能测试,允许根据配置和组件选项对测试装置和测试用例进行参数化,或者跨功能、类、模块或整个测试会话范围重用测试装置。
  • teardowm逻辑可以轻松、安全地管理,无论使用多少测试装置,无需手动小心处理错误或微观管理添加清理步骤的顺序。

此外,pytest 继续支持经典的 xunit 样式设置。您可以根据自己的喜好混合两种风格,从经典风格逐渐过渡到新风格。您还可以从现有的 unittest.TestCase 样式或基于Nose的项目开始。

测试装置使用 @pytest.fixture 装饰器定义,如下所述。 Pytest 有有用的内置装置,这里列出以供参考:

capfd

Capture, as text, output to file descriptors 1 and 2.

capfdbinary

Capture, as bytes, output to file descriptors 1 and 2.

caplog

Control logging and access log entries.

capsys

Capture, as text, output to sys.stdout and sys.stderr.

capsysbinary

Capture, as bytes, output to sys.stdout and sys.stderr.

cache

Store and retrieve values across pytest runs.

doctest_namespace

Provide a dict injected into the docstests namespace.

monkeypatch

Temporarily modify classes, functions, dictionaries, os.environ, and other objects.

pytestconfig

Access to configuration values, pluginmanager and plugin hooks.

record_property

Add extra properties to the test.

record_testsuite_property

Add extra properties to the test suite.

recwarn

Record warnings emitted by test functions.

request

Provide information on the executing test function.

testdir

Provide a temporary test directory to aid in running, and testing, pytest plugins.

tmp_path

Provide a pathlib.Path object to a temporary directory which is unique to each test function.

tmp_path_factory

Make session-scoped temporary directories and return pathlib.Path objects.

tmpdir

Provide a py.path.local object to a temporary directory which is unique to each test function; replaced by tmp_path.

tmpdir_factory

Make session-scoped temporary directories and return py.path.local objects; replaced by tmp_path_factory.

什么是测试装置

在我们深入研究测试装置是什么之前,让我们先看看什么是测试用例。

简单来说,测试用例旨在查看特定行为的结果,并确保该结果与您的预期一致。行为不是可以凭经验衡量的,这就是编写测试用例具有挑战性的原因。

您可以将测试分为四个步骤:

  1. Arrange:准备测试数据
  2. Act:执行测试
  3. Assert:断言
  4. CleanUp:清理测试数据

Arrange是我们为执行测试完成准备工作的地方。这几乎包括了除了“Act”之外的所有内容。它正在排列多米诺骨牌,以便Act可以在一个改变状态的步骤中完成它的事情。这可能意味着准备对象、启动/终止服务、将记录输入数据库,甚至是定义要查询的 URL、为尚不存在的用户生成一些凭据,或者只是等待某个过程完成等操作。

Act是启动我们想要测试的行为的单一的、改变状态的Behavior。这种Behavior是执行被测系统 (SUT) 状态更改的原因,我们可以查看由此产生的更改状态来对行为进行判断。这通常采用函数/方法调用的形式。

Assert是我们查看结果状态的地方,并检查它是否在测试步骤执行后看起来像我们预期的那样。这是我们收集证据表明行为符合或不符合我们期望的地方。我们测试中的断言是我们进行测量/观察并将我们的判断应用于它的地方。如果某些东西应该是绿色的,我们会说 assert thing == "green"。

CleanUp是测试在其自身之后进行的地方,因此其他测试不会意外受到它的影响。

测试的核心是Act和Assert步骤,Arrange步骤仅提供上下文。Behavior存在于Act和Assert之间。

回到测试装置

“Fixtures”,在字面意义上,是每一个Arrange步骤和数据。它们是测试用例完成其任务所需的前置条件。

在基本层面上,测试函数通过将它们声明为参数来请求测试装置,就像在前面的例子中的 test_ehlo(smtp_connection): 一样。

在 pytest 中,“fixtures”是您定义的用于此目的的函数。但它们不必仅限于Arrange步骤。它们也可以提供Act步骤,这对于设计更复杂的测试来说是一种强大的技术,特别是考虑到 pytest 的测试装置系统是如何工作的。但我们会在后面进行深入研究。

我们可以通过用 @pytest.fixture 装饰它来告诉 pytest 这个特定的函数是一个测试装置。这是一个简单的示例,说明 pytest 中的测试装置可能是什么样子:

import pytest


class Fruit:
    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return self.name == other.name


@pytest.fixture
def my_fruit():
    return Fruit('Apple')


@pytest.fixture
def fruit_basket(my_fruit):
    return [my_fruit,Fruit('Banana')] 


def test_my_fruit_in_basket(my_fruit, fruit_basket):
    assert my_fruit in fruit_basket


测试也不必限于单个装置。它们可以依赖于您想要的任意数量的装置,并且装置也可以使用其他装置。这就是 pytest 的装置系统真正闪耀的地方。

如果它使事情变得更干净,不要害怕打破它。

“请求”装置

所以fixtures是我们准备测试的方式,但是我们如何告诉pytest哪些测试和装置需要哪些fixtures?

在基本层面上,测试函数通过将它们声明为参数来请求测试装置,如在前面的示例中的 test_my_fruit_in_basket(my_fruit,fruit_basket):

在基本层面上,pytest 依赖于一个测试来告诉它它需要哪些装置,所以我们必须将这些信息构建到测试本身中。我们必须让测试“请求”它所依赖的装置,为此,我们必须将这些装置列为测试函数“签名”中的参数(即 def test_something(blah, stuff, more): line )。

当 pytest 开始运行测试时,它会查看该测试函数签名中的参数,然后搜索与这些参数同名的装置。一旦 pytest 找到它们,它就会运行这些装置,捕获它们返回的内容(如果有的话),并将这些对象作为参数传递给测试函数。

import pytest


class Fruit:
    def __init__(self, name):
        self.name = name
        self.cubed = False

    def cube(self):
        self.cubed = True


class FruitSalad:
    def __init__(self, *fruit_bowl):
        self.fruit = fruit_bowl
        self._cube_fruit()

    def _cube_fruit(self):
        for fruit in self.fruit:
            fruit.cube()


# Arrange
@pytest.fixture
def fruit_bowl():
    return [Fruit("apple"), Fruit("banana")]


def test_fruit_salad(fruit_bowl):
    # Act
    fruit_salad = FruitSalad(*fruit_bowl)

    # Assert
    assert all(fruit.cubed for fruit in fruit_salad.fruit)

在这个例子中,test_fruit_salad “请求”fruit_bowl(即 def test_fruit_salad(fruit_bowl):),当 pytest 看到这一点时,它会执行 Fruit_bowl 装置函数并将它返回的对象作为 Fruit_bowl 参数传递给 test_fruit_salad。

如果我们不借助pytest完成,大致会发生以下情况:

def fruit_bowl():
    return [Fruit("apple"), Fruit("banana")]


def test_fruit_salad(fruit_bowl):
    # Act
    fruit_salad = FruitSalad(*fruit_bowl)

    # Assert
    assert all(fruit.cubed for fruit in fruit_salad.fruit)


# Arrange
bowl = fruit_bowl()
test_fruit_salad(fruit_bowl=bowl)

测试装置可以请求其他装置

pytest 的最大优势之一是其极其灵活的装置系统。它允许我们将复杂的测试需求归结为更简单和有组织的功能,我们只需要让每个功能描述它们所依赖的东西。我们将进一步深入了解这一点,但现在,这里有一个快速示例来演示测试装置如何使用其他测试装置:

# contents of test_append.py
import pytest


# Arrange
@pytest.fixture
def first_entry():
    return "a"


# Arrange
@pytest.fixture
def order(first_entry):
    return [first_entry]


def test_string(order):
    # Act
    order.append("b")

    # Assert
    assert order == ["a", "b"]

请注意,这与上面的示例几乎相同。 pytest 中的测试装置请求测试装置就像测试用例请求测试装置一样。

所有相同的请求规则都适用于用于测试的装置。如果我们不使用pytest完成,这个例子将如何工作:

def first_entry():
    return "a"


def order(first_entry):
    return [first_entry]


def test_string(order):
    # Act
    order.append("b")

    # Assert
    assert order == ["a", "b"]


entry = first_entry()
the_list = order(first_entry=entry)
test_string(order=the_list)

测试装置是可重用的

使 pytest 的装置系统如此强大的原因之一是,它使我们能够定义一个通用的设置步骤,可以反复重用,就像使用普通函数一样。两个不同的测试可以请求相同的测试装置,并让 pytest 从该装置中为每个测试提供自己的结果。

这对于确保测试不会相互影响非常有用。我们可以使用这个系统来确保每个测试都获得自己的独立的一批测试数据,并且从一个干净的状态开始,这样它就可以提供一致、可重复的结果。

这是一个如何派上用场的示例:

# contents of test_append.py
import pytest


# Arrange
@pytest.fixture
def first_entry():
    return "a"


# Arrange
@pytest.fixture
def order(first_entry):
    return [first_entry]


def test_string(order):
    # Act
    order.append("b")

    # Assert
    assert order == ["a", "b"]


def test_int(order):
    # Act
    order.append(2)

    # Assert
    assert order == ["a", 2]

这里的每个测试都被赋予了它自己的列表对象副本,这意味着order装置被执行两次(对于 first_entry 装置也是如此).

如果我们不借助pytest执行此操作,它将看起来像这样:

def first_entry():
    return "a"


def order(first_entry):
    return [first_entry]


def test_string(order):
    # Act
    order.append("b")

    # Assert
    assert order == ["a", "b"]


def test_int(order):
    # Act
    order.append(2)

    # Assert
    assert order == ["a", 2]


entry = first_entry()
the_list = order(first_entry=entry)
test_string(order=the_list)

entry = first_entry()
the_list = order(first_entry=entry)
test_int(order=the_list)

一个测试用例/装置一次可以请求多个装置

测试用例和装置不限于一次请求一个装置。他们可以要求尽可能多的装置。这是另一个演示的快速示例:

# contents of test_append.py
import pytest


# Arrange
@pytest.fixture
def first_entry():
    return "a"


# Arrange
@pytest.fixture
def second_entry():
    return 2


# Arrange
@pytest.fixture
def order(first_entry, second_entry):
    return [first_entry, second_entry]


# Arrange
@pytest.fixture
def expected_list():
    return ["a", 2, 3.0]


def test_string(order, expected_list):
    # Act
    order.append(3.0)

    # Assert
    assert order == expected_list

每次测试可以多次请求相同的测试装置(返回值将被缓存)

在同一个测试用例中也可以多次请求 相同的测试装置,并且 pytest 不会为该测试再次执行它们。这意味着我们可以在依赖于它们的多个测试装置中请求同一个测试装置(甚至在测试本身中也是如此),而这些测试装置不会被多次执行。

# contents of test_append.py
import pytest


# Arrange
@pytest.fixture
def first_entry():
    return "a"


# Arrange
@pytest.fixture
def order():
    return []


# Act
@pytest.fixture
def append_first(order, first_entry):
    return order.append(first_entry)


def test_string_only(append_first, order, first_entry):
    # Assert
    assert order == [first_entry]

如果在测试期间每次请求请求都执行一次请求的装置,则此测试将失败,因为 append_first 和 test_string_only 都会将 order 视为空列表(即 []),但是由于 order 的返回值在第一次被调用后被缓存(以及执行它可能有的任何副作用),所以 test 和 append_first 都引用了同一个对象,并且在test中看到了 append_first 对该对象的影响。

自动使用装置(您不必主动请求的装置)

有时,您可能希望拥有一个(或什至几个)您知道所有测试都将依赖的测试装置。“autouse”装置是一种让所有测试自动请求它们的便捷方式。这可以减少大量冗余请求,甚至可以提供更高级的装置使用(后面会有更多进一步的内容)

我们可以通过将 autouse=True 传递给装置的装饰器来使装置成为自动使用装置。这是一个如何使用它们的简单示例:

# contents of test_append.py
import pytest


@pytest.fixture
def first_entry():
    return "a"


@pytest.fixture
def order(first_entry):
    return []


@pytest.fixture(autouse=True)
def append_first(order, first_entry):
    return order.append(first_entry)


def test_string_only(order, first_entry):
    assert order == [first_entry]


def test_string_and_int(order, first_entry):
    order.append(2)
    assert order == [first_entry, 2]

在这个例子中, append_first 装置是一个auto的装置。因为它是自动发生的,所以两个测试都会受到它的影响,即使两个测试都没有请求它。但这并不意味着他们不能被请求;只是没有必要。

范围:跨类、模块、包或会话共享测试装置

需要网络访问的装置取决于连接性,并且通常创建起来很费时间。扩展前面的例子,我们可以在@pytest.fixture 调用中添加一个 scope="module" 参数来产生一个 smtp_connection 装置函数,他负责创建到预先存在的 SMTP 服务器的连接,每个测试模块只调用一次(默认是每个测试函数调用一次),因此,测试模块中的多个测试功能将各自接收相同的 smtp_connection 装置实例,从而节省时间。scope的可能值包括:function、class、module、package或session。

# content of conftest.py
import pytest
import smtplib


@pytest.fixture(scope="module")
def smtp_connection():
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
# content of test_module.py


def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250
    assert b"smtp.gmail.com" in msg
    assert 0  # for demo purposes


def test_noop(smtp_connection):
    response, msg = smtp_connection.noop()
    assert response == 250
    assert 0  # for demo purposes

在这里, test_ehlo 需要 smtp_connection 装置值。 pytest 会发现并调用@pytest.fixture 标记的 smtp_connection 装置函数。运行测试如下所示:

$ pytest test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 2 items

test_module.py FF                                                    [100%]

================================= FAILURES =================================
________________________________ test_ehlo _________________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef>

    def test_ehlo(smtp_connection):
        response, msg = smtp_connection.ehlo()
        assert response == 250
        assert b"smtp.gmail.com" in msg
>       assert 0  # for demo purposes
E       assert 0

test_module.py:7: AssertionError
________________________________ test_noop _________________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef>

    def test_noop(smtp_connection):
        response, msg = smtp_connection.noop()
        assert response == 250
>       assert 0  # for demo purposes
E       assert 0

test_module.py:13: AssertionError
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo - assert 0
FAILED test_module.py::test_noop - assert 0
============================ 2 failed in 0.12s =============================

您会看到两个断言 0 失败,更重要的是,您还可以看到完全相同的 smtp_connection 对象被传递到两个测试函数中,因为 pytest 在跟踪信息中显示传入的参数值。因此,使用 smtp_connection 的两个测试函数的运行速度与单个测试函数一样快,因为它们重用了相同的实例。

如果您决定想要拥有一个会话范围的 smtp_connection 实例,您可以简单地声明它:

@pytest.fixture(scope="session")
def smtp_connection():
    # the returned fixture value will be shared for
    # all tests requesting it
    ...

测试装置的范围

Fixtures 在第一次被测试请求时创建,并根据它们的作用域销毁:

  • function:默认范围,在测试结束时销毁fixture
  • class:在类中的最后一个测试用例的teardown期间销毁fixture.
  • module:在模块中的最后一个测试用例的teardown期间销毁fixture.
  • package:在包中的最后一个测试用例的teardown期间销毁fixture.
  • session: 在测试会话结束时销毁fixture

Note:Pytest 一次只缓存一个fixture 实例,这意味着当使用参数化fixture 时,pytest 可能会在给定范围内多次调用fixture。

动态范围

5.2 版中的新功能。

在某些情况下,您可能希望在不更改代码的情况下更改装置的范围。为此,请将可调用对象传递给scope。callable 必须返回一个具有有效范围的字符串,并且只会执行一次 - 在装置定义期间。该方法使用两个关键字参数 - 作为字符串的 fixture_name 和configuration对象的 config 。

这在处理需要大量时间进行测试装置的设置时特别有用,例如生成 docker 容器。您可以使用命令行参数来控制为不同环境生成的容器的范围。请参阅下面的示例。

def determine_scope(fixture_name, config):
    if config.getoption("--keep-containers", None):
        return "session"
    return "function"


@pytest.fixture(scope=determine_scope)
def docker_container():
    yield spawn_container()

装置错误

pytest 尽最大努力将给定测试的所有装置按线性顺序排列,以便它可以看到哪个装置首先发生,第二个,第三个,依此类推。但是,如果较早发生的装置出现问题并引发异常,pytest 将停止执行该测试的装置并将该测试标记为有错误。

但是,当测试被标记为有错误时,并不意味着测试失败。这只是意味着测试无法被执行,因为它所依赖的其中一项存在问题。

这就是为什么最好为给定的测试删除尽可能多的不必要的依赖项的原因之一。这样,不相关的问题不会导致我们对不确定的问题产生误区。

这是一个帮助解释的快速示例:

import pytest


@pytest.fixture
def order():
    return []


@pytest.fixture
def append_first(order):
    order.append(1)


@pytest.fixture
def append_second(order, append_first):
    order.extend([2])


@pytest.fixture(autouse=True)
def append_third(order, append_second):
    order += [3]


def test_order(order):
    assert order == [1, 2, 3]

无论出于何种原因,如果 order.append(1) 有错误并引发异常,我们将无法知道 order.extend([2]) 或 order += [3] 是否也有问题。在 append_first 抛出异常后,pytest 将不再为 test_order 运行任何装置,它甚至不会尝试运行 test_order 本身。唯一会运行的是 order 和 append_first。

TearDown/CleanUp(装置的清理操作)

当我们执行完测试时,我们希望确保它们自己清理干净,这样它们就不会干扰任何其他测试(并且我们也不会留下堆积如山的测试数据来使系统膨胀)。pytest 中的 Fixtures 提供了一个非常有用的TearDown系统,它允许我们定义每个 Fixtures 在其自身之后进行清理所需的特定步骤。

可以通过两种方式利用该系统.

1.yield fixtures(推荐)

“Yield”装置产生而不是返回结果。使用这些装置,我们可以运行一些代码并将对象传递回请求装置/测试,就像使用其他装置一样。唯一的区别是:

  1. return 被替换为yield
  2. yield后面的代码部分是该装置的清理部分

一旦 pytest 计算出测试装置的线性顺序,它将运行每个装置直到return或yield语句,然后继续执行列表中的下一个装置以执行相同的操作。

测试完成后,pytest 将返回到fixtures 列表中,但以相反的顺序,获取每一个yield,并在其中运行yield 语句之后的代码。

作为一个简单的例子,假设我们要测试从一个用户向另一个用户发送电子邮件。

我们必须首先创建每个用户,然后从一个用户向另一个用户发送电子邮件,最后断言另一个用户在他们的收件箱中收到了该邮件。如果我们想在测试运行后进行清理,在删除该用户之前,我们可能必须确保该用户的邮箱已被清空,否则系统可能会报错。

这可能是这样的:

import pytest

from emaillib import Email, MailAdminClient


@pytest.fixture
def mail_admin():
    return MailAdminClient()


@pytest.fixture
def sending_user(mail_admin):
    user = mail_admin.create_user()
    yield user
    admin_client.delete_user(user)


@pytest.fixture
def receiving_user(mail_admin):
    user = mail_admin.create_user()
    yield user
    admin_client.delete_user(user)


def test_email_received(receiving_user, email):
    email = Email(subject="Hey!", body="How's it going?")
    sending_user.send_email(_email, receiving_user)
    assert email in receiving_user.inbox

因为receipt_user 是在setup 期间最后运行的fixture,所以它是teardown 期间第一个运行的fixture。有一种风险,即使在清理的时候有正确的顺序也不能保证安全清理。Safe teardowns中更详细地介绍了这一点。

处理yield装置的错误

如果一个 yield 装置在 yield 之前抛出异常,pytest 将不会尝试在该 yield 装置的 yield 语句之后运行teardown代码。但是,对于已经为该测试成功运行的每个装置,pytest 仍会像往常一样尝试执行teardown代码。

2. 直接添加finalizers

虽然 yield 装置被认为是更干净、更直接的选项,但还有另一种选择,那就是将“终结器”函数直接添加到测试的request上下文对象中。它带来了与 yield 装置类似的结果,但需要更多的代码。

为了使用这种方法,我们必须在需要为其添加teardown代码的装置中请求请求上下文对象(就像我们请求另一个装置一样),然后将包含该拆卸代码的可调用对象传递给其 addfinalizer 方法。

但是我们必须小心,因为 pytest 将在添加终结器后运行该终结器,即使该装置在添加终结器后引发异常。所以为了确保我们不会在不需要的时候运行终结器代码,我们只会在装置完成一些我们需要teardown的事情时添加终结器。

以下是使用 addfinalizer 方法的前一个示例的外观:

import pytest

from emaillib import Email, MailAdminClient


@pytest.fixture
def mail_admin():
    return MailAdminClient()


@pytest.fixture
def sending_user(mail_admin):
    user = mail_admin.create_user()
    yield user
    admin_client.delete_user(user)


@pytest.fixture
def receiving_user(mail_admin, request):
    user = mail_admin.create_user()

    def delete_user():
        admin_client.delete_user(user)

    request.addfinalizer(delete_user)
    return user


@pytest.fixture
def email(sending_user, receiving_user, request):
    _email = Email(subject="Hey!", body="How's it going?")
    sending_user.send_email(_email, receiving_user)

    def empty_mailbox():
        receiving_user.delete_email(_email)

    request.addfinalizer(empty_mailbox)
    return _email


def test_email_received(receiving_user, email):
    assert email in receiving_user.inbox

它比yield fixtures 长一点,也更复杂一点,但是当你处于紧要关头时,它确实提供了一些细微差别。

安全的TearDown

pytest的fixture系统非常强大,但它仍然由计算机运行,因此无法弄清楚如何安全地TearDown我们扔给它的所有东西。如果我们不小心,错误位置的异常可能会将测试中断,这可能会很快导致进一步的问题。

例如,考虑以下测试(基于上面的邮件示例):

import pytest

from emaillib import Email, MailAdminClient


@pytest.fixture
def setup():
    mail_admin = MailAdminClient()
    sending_user = mail_admin.create_user()
    receiving_user = mail_admin.create_user()
    email = Email(subject="Hey!", body="How's it going?")
    sending_user.send_emai(email, receiving_user)
    yield receiving_user, email
    receiving_user.delete_email(email)
    admin_client.delete_user(sending_user)
    admin_client.delete_user(receiving_user)


def test_email_received(setup):
    receiving_user, email = setup
    assert email in receiving_user.inbox

这个版本更紧凑,但也更难阅读,没有一个非常具有描述性的装置名称,并且没有一个装置可以轻松重用。

还有一个更严重的问题,即如果设置中的任何步骤抛出异常,则任何teardown代码都不会运行。

一种选择可能是使用 addfinalizer 方法而不是 yield 装置,但这可能会变得非常复杂且难以维护(并且它不再紧凑)。

安全的装置结构

最安全和最简单的装置结构要求将装置限制为每个装置只能进行一个状态更改操作,然后将它们与其TearDown代码捆绑在一起,如上面的电子邮件示例所示。

状态更改操作可能失败但仍然修改状态的可能性可以忽略不计,因为这些操作中的大多数往往是基于事务的(至少在可能会留下状态的测试级别)。因此,如果我们通过将任何成功的状态更改操作移至单独的装置方法并将其与其他可能失败的状态更改操作分开来确保将其拆除,那么我们的测试将有最好的机会按照他们发现的方式离开测试环境。

举个例子,假设我们有一个带有登录页面的网站,我们可以访问一个管理 API,我们可以在其中生成用户。对于我们的测试,我们希望:

  1. Create a user through that admin API 通过这个api创建用户

  2. Launch a browser using Selenium 使用selenium加载浏览器

  3. Go to the login page of our site 访问登录页面

  4. Log in as the user we created 用创建的账号登录

  5. Assert that their name is in the header of the landing page 验证用户的名字在登录页的标题

我们不想让那个用户留在系统中,也不想让浏览器会话继续运行,所以我们要确保创建这些东西的设备在自己执行完成之后清理干净。

这可能是这样的:

注意 :对于此示例,某些装置(即 base_url 和 admin_credentials)会存在于其他地方。所以现在,让我们假设它们存在,我们不关注它们。

from uuid import uuid4
from urllib.parse import urljoin

from selenium.webdriver import Chrome
import pytest

from src.utils.pages import LoginPage, LandingPage
from src.utils import AdminApiClient
from src.utils.data_types import User


@pytest.fixture
def admin_client(base_url, admin_credentials):
    return AdminApiClient(base_url, **admin_credentials)


@pytest.fixture
def user(admin_client):
    _user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word")
    admin_client.create_user(_user)
    yield _user
    admin_client.delete_user(_user)


@pytest.fixture
def driver():
    _driver = Chrome()
    yield _driver
    _driver.quit()


@pytest.fixture
def login(driver, base_url, user):
    driver.get(urljoin(base_url, "/login"))
    page = LoginPage(driver)
    page.login(user)


@pytest.fixture
def landing_page(driver, login):
    return LandingPage(driver)


def test_name_on_landing_page_after_login(landing_page, user):
    assert landing_page.header == f"Welcome, {user.name}!"

依赖关系的布局方式意味着不清楚user装置是否会在driver装置之前执行。但这没关系,因为这些是原子操作,所以先运行哪个并不重要,因为测试的事件序列仍然是可线性化的。但重要的是,无论哪一个先运行,如果一个引发异常而另一个没有,那么两者都不会留下任何东西。如果driver在user之前执行,并且user引发异常,driver仍然会退出,并且用户永远不会被创建。如果driver是引发异常的人,那么driver永远不会被启动,用户也永远不会被创建。

装置可用性

装置可用性是从测试用例的角度确定的。装置仅可用于测试请求,前提是它们在装置定义的范围内。如果在一个类中定义了一个fixture,它只能被该类中的测试请求。但是如果在模块的全局范围内定义了一个fixture,那么该模块中的每个测试,即使它是在一个类中定义的,都可以请求它。

类似地,如果该测试与 autouse 装置定义在同一范围内,则测试也只能受autouse装置影响。一个fixture 也可以请求任何其他fixture,无论它在哪里定义,只要请求它们的测试可以看到所有涉及的fixture。例如,这是一个带有装置(outer)的测试文件,它从未定义的范围中请求装置(inner):

import pytest


@pytest.fixture
def order():
    return []


@pytest.fixture
def outer(order, inner):
    order.append("outer")


class TestOne:
    @pytest.fixture
    def inner(self, order):
        order.append("one")

    def test_order(self, order, outer):
        assert order == ["one", "outer"]


class TestTwo:
    @pytest.fixture
    def inner(self, order):
        order.append("two")

    def test_order(self, order, outer):
        assert order == ["two", "outer"]

从测试用例的角度来看,他们可以毫无问题地看到他们所依赖的每个装置:

 因此,当它们运行时,outer 可以毫无问题地找到inner,因为 pytest 从测试用例的角度进行搜索。

注意 定义测试装置的范围与实例化的顺序无关:顺序由此处描述的逻辑强制执行。

conftest.py:跨多个文件共享装置

conftest.py 文件用作为整个目录提供装置的一种方式。在 conftest.py 中定义的 Fixture 可以被该包中的任何测试用例使用,而无需导入它们(pytest 会自动发现它们)。

你可以有多个嵌套的目录/包来存放你的测试,每个目录可以有自己的 conftest.py 和自己的装置,并添加到父目录中的 conftest.py 文件提供的装置。

例如,给定这样的测试文件结构:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture
        def order():
            return []

        @pytest.fixture
        def top(order, innermost):
            order.append("top")

    test_top.py
        # content of tests/test_top.py
        import pytest

        @pytest.fixture
        def innermost(order):
            order.append("innermost top")

        def test_order(order, top):
            assert order == ["innermost top", "top"]

    subpackage/
        __init__.py

        conftest.py
            # content of tests/subpackage/conftest.py
            import pytest

            @pytest.fixture
            def mid(order):
                order.append("mid subpackage")

        test_subpackage.py
            # content of tests/subpackage/test_subpackage.py
            import pytest

            @pytest.fixture
            def innermost(order, mid):
                order.append("innermost subpackage")

            def test_order(order, top):
                assert order == ["mid subpackage", "innermost subpackage", "top"]

范围的边界的可视化展示如下:


 这些目录成为它们自己的一种作用域,在该目录中的 conftest.py 文件中定义的装置可用于整个作用域。测试可以向上搜索(跨出一个圆圈)寻找固定装置,但永远不能向下(跨入一个圆圈)继续他们的搜索。因此,tests/subpackage/test_subpackage.py::test_order 将能够找到定义在tests/subpackage/test_subpackage.py 中的最里面的fixture,但是在 tests/test_top.py 中定义的那个对它来说是不可用的,因为它必须降低一个级别(进入一个圆圈)才能找到它。

测试用例找到的第一个装置是将使用的装置,因此如果您需要更改或扩展某个特定范围的功能,则可以覆盖测试装置。

您还可以使用 conftest.py 文件来实现本地每个目录的插件。

来自第三方插件的装置

不过,不仅在此结构中定义装置即可用于测试。它们也可以由安装的第三方插件提供,这就是大多数pytest 插件的做法。只要安装了这些插件,就可以从测试套件的任何地方请求它们提供的装置。

因为它们是从测试套件结构之外提供的,所以第三方插件并没有真正提供像 conftest.py 文件和测试套件中的目录那样的范围。因此,pytest 将搜索通过范围逐步退出的装置,如前所述,仅最后到达插件中定义的装置。

例如,给定以下文件结构:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture
        def order():
            return []

    subpackage/
        __init__.py

        conftest.py
            # content of tests/subpackage/conftest.py
            import pytest

            @pytest.fixture(autouse=True)
            def mid(order, b_fix):
                order.append("mid subpackage")

        test_subpackage.py
            # content of tests/subpackage/test_subpackage.py
            import pytest

            @pytest.fixture
            def inner(order, mid, a_fix):
                order.append("inner subpackage")

            def test_order(order, inner):
                assert order == ["b_fix", "mid subpackage", "a_fix", "inner subpackage"]

如果安装了 plugin_a 并提供了装置 a_fix,并且安装了 plugin_b 并提供了装置b_fix,那么这就是测试对装置的搜索的样子:

pytest 首先在 tests/ 中的范围中搜索 a_fix 和 b_fix,如果没有找到随后在插件中搜索。

共享测试数据 

如果您想让文件中的测试数据可用于您的测试,一个好的方法是将这些数据加载到一个装置中以供您的测试使用。这利用了 pytest 的自动缓存机制。另一个好方法是在tests文件夹中添加数据文件。还有社区插件可用于帮助管理测试的这方面。例如pytest-datadirpytest-datafiles

装置实例化顺序

当 pytest 想要执行测试时,一旦它知道将执行哪些装置,它就必须弄清楚它们将被执行的顺序。为此,它考虑了 3 个因素:

  1. scope

  2. dependencies

  3. autouse

装置或测试的名称、定义它们的位置、定义它们的顺序以及请求装置的顺序与执行顺序无关。虽然 pytest 会尝试确保像这样的巧合在每次运行中保持一致,但这不是应该依赖的东西。如果要控制顺序,最安全的方法是依靠这 3 件事并确保明确建立依赖关系。

首先执行范围更大的装置

在对fixtures 的函数请求中,较大范围的fixture(例如session)在较低范围的fixtures(例如function或class)之前执行。

下面是一个例子:

import pytest


@pytest.fixture(scope="session")
def order():
    return []


@pytest.fixture
def func(order):
    order.append("function")


@pytest.fixture(scope="class")
def cls(order):
    order.append("class")


@pytest.fixture(scope="module")
def mod(order):
    order.append("module")


@pytest.fixture(scope="package")
def pack(order):
    order.append("package")


@pytest.fixture(scope="session")
def sess(order):
    order.append("session")


class TestClass:
    def test_order(self, func, cls, mod, pack, sess, order):
        assert order == ["session", "package", "module", "class", "function"]

测试将通过,因为较大范围的装置会先执行

顺序分解为:

执行相同顺序的装置需要参考依赖关系

当一个装置3请求另一个装置时,被请求的装置会首先执行。所以如果装置a 请求装置b,装置 b 将首先执行,因为 a 依赖于 b,没有它就不能运行。即使 a 不需要 b 的结果,如果需要确保它在 b 之后执行,它仍然可以请求 b。

例如:

import pytest


@pytest.fixture
def order():
    return []


@pytest.fixture
def a(order):
    order.append("a")


@pytest.fixture
def b(a, order):
    order.append("b")


@pytest.fixture
def c(a, b, order):
    order.append("c")


@pytest.fixture
def d(c, b, order):
    order.append("d")


@pytest.fixture
def e(d, b, order):
    order.append("e")


@pytest.fixture
def f(e, order):
    order.append("f")


@pytest.fixture
def g(f, c, order):
    order.append("g")


def test_order(g, order):
    assert order == ["a", "b", "c", "d", "e", "f", "g"]

如果我们绘制出依赖关系图,我们会得到如下所示的内容:

这样每个装置提供的规则(关于每个固定装置的依赖关系)足够全面,可以将其扁平化为:

须通过这些请求提供足够的信息,以便 pytest 能够找出清晰的线性依赖关系链,从而确定给定测试的操作顺序。如果有任何歧义,并且操作顺序可以以多种路径进行解释,您应该假设 pytest 可以在任何时候采用这些顺序中的任何一种。 

例如,如果 d 没有请求 c,即图形将如下所示:

因为除了 g 没有任何装置请求 c,并且 g 也请求 f,所以现在不清楚 c 是否应该在 f、e 或 d 之前/之后。为 c 设置的唯一规则是它必须在 b 之后和 g 之前执行。pytest 不知道在这种情况下 c 应该什么时候执行,所以应该假设它可以在 g 和 b 之间的任何地方。

这不一定是坏事,但需要牢记这一点。如果它们执行的顺序可能影响测试所针对的行为,或者可能以其他方式影响测试的结果,那么应该以允许 pytest 线性化/“扁平化”该顺序的方式明确定义顺序。

autouse装置在其范围内首先执行

autouse装置被设定应用于可以引用它们的每个测试,因此它们在该范围内的其他装置之前执行。autouse装置请求的装置也会成为autouse装置以供真正的autouse装置适用的测试使用。

因此,如果装置 a 是autouse的,而装置b 不是,但装置 a 请求装置 b,那么装置b 也将有效地成为autouse的装置,但仅适用于 a 适用的测试。

在最后一个例子中,如果 d 没有请求 c,图表就会变得不清楚。但是如果 c 是autouse,那么 b 和 a 也将autouse,因为 c 依赖于它们。

因此,如果测试文件如下所示:

import pytest


@pytest.fixture
def order():
    return []


@pytest.fixture
def a(order):
    order.append("a")


@pytest.fixture
def b(a, order):
    order.append("b")


@pytest.fixture(autouse=True)
def c(b, order):
    order.append("c")


@pytest.fixture
def d(b, order):
    order.append("d")


@pytest.fixture
def e(d, order):
    order.append("e")


@pytest.fixture
def f(e, order):
    order.append("f")


@pytest.fixture
def g(f, c, order):
    order.append("g")


def test_order_and_g(g, order):
    assert order == ["a", "b", "c", "d", "e", "f", "g"]

该图如下所示:

因为 c 现在可以放在图中的 d 上方,所以 pytest 可以再次将图线性化为:

在这个例子中,c 使 b 和 a 也成为了有效地autouse设备。

但是要小心使用autouse,因为autouse装置会自动为每个可以到达它的测试用例执行装置,即使他们没有请求它。例如,考虑这个文件:

import pytest


@pytest.fixture(scope="class")
def order():
    return []


@pytest.fixture(scope="class", autouse=True)
def c1(order):
    order.append("c1")


@pytest.fixture(scope="class")
def c2(order):
    order.append("c2")


@pytest.fixture(scope="class")
def c3(order, c1):
    order.append("c3")


class TestClassWithC1Request:
    def test_order(self, order, c1, c3):
        assert order == ["c1", "c3"]


class TestClassWithoutC1Request:
    def test_order(self, order, c2):
        assert order == ["c1", "c2"]

 即使 TestClassWithoutC1Request 中没有任何内容请求 c1,它仍然会针对其中的测试执行:

但仅仅因为一个autouse装置请求了一个非autouse装置,这并不意味着非autouse装置成为它可以其应用的所有上下文的autouse装置。 它只会有效地成为真正的autouse装置(请求非autouse装置)可以应用的上下文的autouse装置。

例如,看看这个测试文件:

import pytest


@pytest.fixture
def order():
    return []


@pytest.fixture
def c1(order):
    order.append("c1")


@pytest.fixture
def c2(order):
    order.append("c2")


class TestClassWithAutouse:
    @pytest.fixture(autouse=True)
    def c3(self, order, c2):
        order.append("c3")

    def test_req(self, order, c1):
        assert order == ["c2", "c3", "c1"]

    def test_no_req(self, order):
        assert order == ["c2", "c3"]


class TestClassWithoutAutouse:
    def test_req(self, order, c1):
        assert order == ["c1"]

    def test_no_req(self, order):
        assert order == []

它会分解为这样的:

对于 TestClassWithAutouse 中的 test_req 和 test_no_req,c3 有效地使 c2 成为自动使用装置,这就是为什么 c2 和 c3 在两个测试中都执行,尽管没有被请求,以及为什么 c2 和 c3 在 c1 之前执行 test_req。

如果这使 c2 成为实际的 autouse 装置,那么 c2 也会为 TestClassWithoutAutouse 内的测试执行,因为如果他们愿意,它们可以引用 c2。但事实并非如此,因为从 TestClassWithoutAutouse 测试的角度来看,c2 不是auto装置,因为它们看不到 c3。 

安全地运行多个断言语句

有时您可能希望在完成所有设置后运行多个断言,这是有道理的,因为在更复杂的系统中,单个操作可以启动多个行为。pytest 有一种方便的方法来处理这个问题,它结合了我们迄今为止所讨论的一堆内容。

所需要做的就是升级到更大的范围,然后将Act步骤定义为自动使用装置,最后确保所有装置都针对更高级别的范围。

让我们从上面拉一个例子,并稍微调整一下。假设除了检查标题中的欢迎消息外,我们还想检查退出按钮和指向用户个人资料的链接。让我们来看看我们如何构建它,以便我们可以运行多个断言而不必再次重复所有这些步骤。

注意 对于此示例,某些装置(即 base_url 和 admin_credentials)假设存在于其他地方。所以现在,让我们假设它们存在,我们只是不看它们。

# contents of tests/end_to_end/test_login.py
from uuid import uuid4
from urllib.parse import urljoin

from selenium.webdriver import Chrome
import pytest

from src.utils.pages import LoginPage, LandingPage
from src.utils import AdminApiClient
from src.utils.data_types import User


@pytest.fixture(scope="class")
def admin_client(base_url, admin_credentials):
    return AdminApiClient(base_url, **admin_credentials)


@pytest.fixture(scope="class")
def user(admin_client):
    _user = User(name="Susan", username=f"testuser-{uuid4()}", password="P4$$word")
    admin_client.create_user(_user)
    yield _user
    admin_client.delete_user(_user)


@pytest.fixture(scope="class")
def driver():
    _driver = Chrome()
    yield _driver
    _driver.quit()


@pytest.fixture(scope="class")
def landing_page(driver, login):
    return LandingPage(driver)


class TestLandingPageSuccess:
    @pytest.fixture(scope="class", autouse=True)
    def login(self, driver, base_url, user):
        driver.get(urljoin(base_url, "/login"))
        page = LoginPage(driver)
        page.login(user)

    def test_name_in_header(self, landing_page, user):
        assert landing_page.header == f"Welcome, {user.name}!"

    def test_sign_out_button(self, landing_page):
        assert landing_page.sign_out_button.is_displayed()

    def test_profile_link(self, landing_page, user):
        profile_href = urljoin(base_url, f"/profile?id={user.profile_id}")
        assert landing_page.profile_link.get_attribute("href") == profile_href

请注意,这些方法仅在签名中引用 self 作为一种形式。没有状态与实际测试类相关联,因为它可能在 unittest.TestCase 框架中。一切都由 pytest 装置系统管理。

每个方法只需要请求它实际需要的装置,而不必担心顺序。这是因为 act 装置是一个autouse的z=装置,它确保所有其他装置在它之前执行。不再需要进行状态更改,因此测试可以随意进行任意数量的非状态更改查询,而不会冒踩到其他测试的脚趾的风险。

login装置也在类中定义,因为并非模块中的每个其他测试都期望成功登录,并且对于另一个测试类可能需要稍微不同地处理该行为。例如,如果我们想围绕提交错误凭据编写另一个测试场景,我们可以通过在测试文件中添加如下内容来处理它:

class TestLandingPageBadCredentials:
    @pytest.fixture(scope="class")
    def faux_user(self, user):
        _user = deepcopy(user)
        _user.password = "badpass"
        return _user

    def test_raises_bad_credentials_exception(self, login_page, faux_user):
        with pytest.raises(BadCredentialsException):
            login_page.login(faux_user)

Fixtures 可以内省请求的测试上下文

装置函数可以接受request对象来内省“请求”测试函数、类或模块上下文。进一步扩展前面的 smtp_connection 装置示例,让我们从使用我们的装置的测试模块中读取一个可选的服务器 URL:

# content of conftest.py
import pytest
import smtplib


@pytest.fixture(scope="module")
def smtp_connection(request):
    server = getattr(request.module, "smtpserver", "smtp.gmail.com")
    smtp_connection = smtplib.SMTP(server, 587, timeout=5)
    yield smtp_connection
    print("finalizing {} ({})".format(smtp_connection, server))
    smtp_connection.close()

我们使用 request.module 属性来可选地从测试模块获取 smtpserver 属性。如果我们再次执行,则没有太大变化:

$ pytest -s -q --tb=no
FFfinalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com)

========================= short test summary info ==========================
FAILED test_module.py::test_ehlo - assert 0
FAILED test_module.py::test_noop - assert 0
2 failed in 0.12s

让我们快速创建另一个测试模块,在其模块命名空间中实际设置服务器 URL:

# content of test_anothersmtp.py

smtpserver = "mail.python.org"  # will be read by smtp fixture


def test_showhelo(smtp_connection):
    assert 0, smtp_connection.helo()

运行它:

$ pytest -qq --tb=short test_anothersmtp.py
F                                                                    [100%]
================================= FAILURES =================================
______________________________ test_showhelo _______________________________
test_anothersmtp.py:6: in test_showhelo
    assert 0, smtp_connection.helo()
E   AssertionError: (250, b'mail.python.org')
E   assert 0
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef> (mail.python.org)
========================= short test summary info ==========================
FAILED test_anothersmtp.py::test_showhelo - AssertionError: (250, b'mail....

瞧! smtp_connection 装置函数从模块命名空间中获取我们的邮件服务器名称。

使用标记将数据传递给装置

使用request对象,fixture 还可以访问应用于测试函数的标记。这对于将数据从测试传递到装置非常有用:

import pytest


@pytest.fixture
def fixt(request):
    marker = request.node.get_closest_marker("fixt_data")
    if marker is None:
        # Handle missing marker in some way...
        data = None
    else:
        data = marker.args[0]

    # Do something with the data
    return data


@pytest.mark.fixt_data(42)
def test_fixt(fixt):
    assert fixt == 42

工厂即装置

“工厂即装置”模式可以帮助在单个测试中多次需要装置结果的情况。装置不是直接返回数据,而是返回一个生成数据的函数。然后可以在测试中多次调用此函数。

工厂可以根据需要设置参数:

@pytest.fixture
def make_customer_record():
    def _make_customer_record(name):
        return {"name": name, "orders": []}

    return _make_customer_record


def test_customer_records(make_customer_record):
    customer_1 = make_customer_record("Lisa")
    customer_2 = make_customer_record("Mike")
    customer_3 = make_customer_record("Meredith")

如果工厂创建的数据需要管理,装置可以处理:

@pytest.fixture
def make_customer_record():

    created_records = []

    def _make_customer_record(name):
        record = models.Customer(name=name, orders=[])
        created_records.append(record)
        return record

    yield _make_customer_record

    for record in created_records:
        record.destroy()


def test_customer_records(make_customer_record):
    customer_1 = make_customer_record("Lisa")
    customer_2 = make_customer_record("Mike")
    customer_3 = make_customer_record("Meredith")

参数化装置

装置函数可以被参数化,在这种情况下,它们将被多次调用,每次执行一组相关测试,也就是依赖于这个装置的测试。测试函数通常不需要知道它们的重新运行。装置参数化有助于为组件编写详尽的功能测试,这些组件本身可以通过多种方式进行配置。

扩展前面的示例,我们可以标记装置以创建两个 smtp_connection 装置实例,这将导致使用装置的所有测试运行两次。Fixture 函数通过特殊的request对象访问每个参数:

# content of conftest.py
import pytest
import smtplib


@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
    smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
    yield smtp_connection
    print("finalizing {}".format(smtp_connection))
    smtp_connection.close()

主要的变化是使用@pytest.fixture 声明params参数,这是一个值列表,装置函数将执行每个值,并且可以通过 request.param 访问一个值。无需更改测试功能代码。所以让我们再运行一​​次:

$ pytest -q test_module.py
FFFF                                                                 [100%]
================================= FAILURES =================================
________________________ test_ehlo[smtp.gmail.com] _________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef>

    def test_ehlo(smtp_connection):
        response, msg = smtp_connection.ehlo()
        assert response == 250
        assert b"smtp.gmail.com" in msg
>       assert 0  # for demo purposes
E       assert 0

test_module.py:7: AssertionError
________________________ test_noop[smtp.gmail.com] _________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef>

    def test_noop(smtp_connection):
        response, msg = smtp_connection.noop()
        assert response == 250
>       assert 0  # for demo purposes
E       assert 0

test_module.py:13: AssertionError
________________________ test_ehlo[mail.python.org] ________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef>

    def test_ehlo(smtp_connection):
        response, msg = smtp_connection.ehlo()
        assert response == 250
>       assert b"smtp.gmail.com" in msg
E       AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8\nCHUNKING'

test_module.py:6: AssertionError
-------------------------- Captured stdout setup ---------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef>
________________________ test_noop[mail.python.org] ________________________

smtp_connection = <smtplib.SMTP object at 0xdeadbeef>

    def test_noop(smtp_connection):
        response, msg = smtp_connection.noop()
        assert response == 250
>       assert 0  # for demo purposes
E       assert 0

test_module.py:13: AssertionError
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef>
========================= short test summary info ==========================
FAILED test_module.py::test_ehlo[smtp.gmail.com] - assert 0
FAILED test_module.py::test_noop[smtp.gmail.com] - assert 0
FAILED test_module.py::test_ehlo[mail.python.org] - AssertionError: asser...
FAILED test_module.py::test_noop[mail.python.org] - assert 0
4 failed in 0.12s

我们看到我们的两个测试函数分别针对不同的 smtp_connection 实例运行了两次。另请注意,对于 mail.python.org 连接,第二个测试在 test_ehlo 中失败,因为预期的服务器字符串与到达的字符串不同。

pytest 将构建一个字符串,该字符串是参数化装置中每个装置值的测试 ID,例如上面例子中的 test_ehlo[smtp.gmail.com] 和 test_ehlo[mail.python.org]。这些 ID 可以与 -k 一起使用来选择要运行的特定用例,并且它们还将在失败时识别特定案例。使用 --collect-only 运行 pytest 将显示生成的 ID。

数字、字符串、布尔值和None将在测试 ID 中使用它们通常的字符串表示。对于其他对象,pytest 将根据参数名称生成一个字符串。可以使用 ids 关键字参数为某个装置值自定义测试 ID 中使用的字符串:

# content of test_ids.py
import pytest


@pytest.fixture(params=[0, 1], ids=["spam", "ham"])
def a(request):
    return request.param


def test_a(a):
    pass


def idfn(fixture_value):
    if fixture_value == 0:
        return "eggs"
    else:
        return None


@pytest.fixture(params=[0, 1], ids=idfn)
def b(request):
    return request.param


def test_b(b):
    pass

上面显示了 ids 可以是要使用的字符串列表,也可以是将使用装置值调用然后必须返回要使用的字符串的函数。在后一种情况下,如果函数返回 None 则将使用 pytest 的自动生成的 ID。

运行上述测试会导致使用以下测试 ID:

$ pytest --collect-only
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 10 items

<Module test_anothersmtp.py>
  <Function test_showhelo[smtp.gmail.com]>
  <Function test_showhelo[mail.python.org]>
<Module test_ids.py>
  <Function test_a[spam]>
  <Function test_a[ham]>
  <Function test_b[eggs]>
  <Function test_b[1]>
<Module test_module.py>
  <Function test_ehlo[smtp.gmail.com]>
  <Function test_noop[smtp.gmail.com]>
  <Function test_ehlo[mail.python.org]>
  <Function test_noop[mail.python.org]>

======================= 10 tests collected in 0.12s ========================

使用带有参数化装置的标记

pytest.param() 可用于在参数化装置的值集中应用标记,其方式与可与 @pytest.mark.parametrize 一起使用的方式相同。

例如:

# content of test_fixture_marks.py
import pytest


@pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)])
def data_set(request):
    return request.param


def test_data(data_set):
    pass

运行此测试将跳过对值为 2 的 data_set 的调用:

$ pytest test_fixture_marks.py -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collecting ... collected 3 items

test_fixture_marks.py::test_data[0] PASSED                           [ 33%]
test_fixture_marks.py::test_data[1] PASSED                           [ 66%]
test_fixture_marks.py::test_data[2] SKIPPED (unconditional skip)     [100%]

======================= 2 passed, 1 skipped in 0.12s =======================

模块化:使用装置功能中的装置

除了在测试函数中使用fixtures之外,fixture函数本身也可以使用其他fixtures。这有助于装置的模块化设计,并允许在许多项目中重复使用特定于框架的装置。作为一个简单的例子,我们可以扩展前面的例子并实例化一个对象app,我们将已经定义的 smtp_connection 资源粘贴到其中:

# content of test_appsetup.py

import pytest


class App:
    def __init__(self, smtp_connection):
        self.smtp_connection = smtp_connection


@pytest.fixture(scope="module")
def app(smtp_connection):
    return App(smtp_connection)


def test_smtp_connection_exists(app):
    assert app.smtp_connection

在这里,我们声明了一个 app 装置,它接收先前定义的 smtp_connection 装置并用它实例化一个 App 对象。让我们运行它:

$ pytest -v test_appsetup.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collecting ... collected 2 items

test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%]
test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100%]

============================ 2 passed in 0.12s =============================

由于 smtp_connection 的参数化,测试将使用两个不同的 App 实例和各自的 smtp 服务器运行两次。app装置不需要知道 smtp_connection 参数化,因为 pytest 将全面分析装置依赖关系图。

请注意,app装置的范围为session并使用模块范围的 smtp_connection 装置。如果 smtp_connection 缓存在会话范围内,该示例仍然有效:装置使用“更广泛”的固定装置很好,但反过来不行:session范围的装置不能使用module范围的装置。

按装置实例自动分组测试

pytest 在测试运行期间最大限度地减少激活的装置的数量。如果你有一个参数化的装置,那么所有使用它的测试将首先用一个实例执行,然后在创建下一个装置实例之前调用终结器。除此之外,这简化了对创建和使用全局状态的应用程序的测试。

下面的例子使用了两个参数化的装置,其中一个是基于每个模块的范围,并且所有函数都执行打印调用以显示setup/teardown流程:

# content of test_module.py
import pytest


@pytest.fixture(scope="module", params=["mod1", "mod2"])
def modarg(request):
    param = request.param
    print("  SETUP modarg", param)
    yield param
    print("  TEARDOWN modarg", param)


@pytest.fixture(scope="function", params=[1, 2])
def otherarg(request):
    param = request.param
    print("  SETUP otherarg", param)
    yield param
    print("  TEARDOWN otherarg", param)


def test_0(otherarg):
    print("  RUN test0 with otherarg", otherarg)


def test_1(modarg):
    print("  RUN test1 with modarg", modarg)


def test_2(otherarg, modarg):
    print("  RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg))

让我们以详细模式运行测试并查看打印输出:

$ pytest -v -s test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collecting ... collected 8 items

test_module.py::test_0[1]   SETUP otherarg 1
  RUN test0 with otherarg 1
PASSED  TEARDOWN otherarg 1

test_module.py::test_0[2]   SETUP otherarg 2
  RUN test0 with otherarg 2
PASSED  TEARDOWN otherarg 2

test_module.py::test_1[mod1]   SETUP modarg mod1
  RUN test1 with modarg mod1
PASSED
test_module.py::test_2[mod1-1]   SETUP otherarg 1
  RUN test2 with otherarg 1 and modarg mod1
PASSED  TEARDOWN otherarg 1

test_module.py::test_2[mod1-2]   SETUP otherarg 2
  RUN test2 with otherarg 2 and modarg mod1
PASSED  TEARDOWN otherarg 2

test_module.py::test_1[mod2]   TEARDOWN modarg mod1
  SETUP modarg mod2
  RUN test1 with modarg mod2
PASSED
test_module.py::test_2[mod2-1]   SETUP otherarg 1
  RUN test2 with otherarg 1 and modarg mod2
PASSED  TEARDOWN otherarg 1

test_module.py::test_2[mod2-2]   SETUP otherarg 2
  RUN test2 with otherarg 2 and modarg mod2
PASSED  TEARDOWN otherarg 2
  TEARDOWN modarg mod2


============================ 8 passed in 0.12s =============================

您可以看到参数化模块范围的 modarg 资源导致测试执行的顺序,从而导致尽可能少的“活动”资源。mod1 参数化资源的终结器在设置 mod2 资源之前执行。

特别注意 test_0 是完全独立的并且最先完成。然后用 mod1 执行 test_1,然后用 mod1 执行 test_2,然后用 mod2 执行 test_1,最后用 mod2 执行 test_2。

otherarg 参数化资源(具有函数作用域)在每次使用它的测试之前设置并在之后拆除。

在带有 usefixtures 的类和模块中使用装置

有时测试函数不需要直接访问装置对象。例如,测试可能需要将空目录作为当前工作目录进行操作,而不关心具体目录。以下是如何使用标准临时文件和 pytest 装置来实现它。我们将装置的创建分离到一个 conftest.py 文件中:

# content of conftest.py

import os
import shutil
import tempfile

import pytest


@pytest.fixture
def cleandir():
    old_cwd = os.getcwd()
    newpath = tempfile.mkdtemp()
    os.chdir(newpath)
    yield
    os.chdir(old_cwd)
    shutil.rmtree(newpath)

并通过 usefixtures 标记声明它在测试模块中的使用:

# content of test_setenv.py
import os
import pytest


@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit:
    def test_cwd_starts_empty(self):
        assert os.listdir(os.getcwd()) == []
        with open("myfile", "w") as f:
            f.write("hello")

    def test_cwd_again_starts_empty(self):
        assert os.listdir(os.getcwd()) == []

由于 usefixtures 标记,每个测试方法的执行都需要 cleandir 装置,就像您为每个测试方法指定了一个“cleandir”函数参数一样。让我们运行它来验证我们的装置是否被激活并且测试通过:

$ pytest -q
..                                                                   [100%]
2 passed in 0.12s

您可以像这样指定多个装置:

@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test():
    ...

您可以使用 pytestmark 在测试模块级别指定装置使用:

pytestmark = pytest.mark.usefixtures("cleandir")

也可以将项目中所有测试所需装置放入 ini 文件中:

# content of pytest.ini
[pytest]
usefixtures = cleandir

警告 请注意,此标记对装置方法没有影响。例如,这不会按预期工作:

@pytest.mark.usefixtures("my_other_fixture")
@pytest.fixture
def my_fixture_that_sadly_wont_use_my_other_fixture():
    ...

目前这不会产生任何错误或警告,但这打算由 #3664 处理。

覆盖各个级别的装置

在相对较大的测试套件中,您很可能需要使用本地定义的装置覆盖全局或根装置,以保持测试代码的可读性和可维护性。

覆盖文件夹(conftest)级别的装置

给定测试文件结构是:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture
        def username():
            return 'username'

    test_something.py
        # content of tests/test_something.py
        def test_username(username):
            assert username == 'username'

    subfolder/
        __init__.py

        conftest.py
            # content of tests/subfolder/conftest.py
            import pytest

            @pytest.fixture
            def username(username):
                return 'overridden-' + username

        test_something.py
            # content of tests/subfolder/test_something.py
            def test_username(username):
                assert username == 'overridden-username'

如您所见,对于某些测试文件夹级别,可以覆盖具有相同名称的装置。请注意,可以从覆盖的装置轻松访问base装置或super装置 - 如上面的示例中使用的方式。

覆盖测试模块级别的装置

给定测试文件结构是:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture
        def username():
            return 'username'

    test_something.py
        # content of tests/test_something.py
        import pytest

        @pytest.fixture
        def username(username):
            return 'overridden-' + username

        def test_username(username):
            assert username == 'overridden-username'

    test_something_else.py
        # content of tests/test_something_else.py
        import pytest

        @pytest.fixture
        def username(username):
            return 'overridden-else-' + username

        def test_username(username):
            assert username == 'overridden-else-username'

在上面的示例中,可以为某些测试模块覆盖具有相同名称的装置。

使用直接测试参数化覆盖装置

给定测试文件结构是:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture
        def username():
            return 'username'

        @pytest.fixture
        def other_username(username):
            return 'other-' + username

    test_something.py
        # content of tests/test_something.py
        import pytest

        @pytest.mark.parametrize('username', ['directly-overridden-username'])
        def test_username(username):
            assert username == 'directly-overridden-username'

        @pytest.mark.parametrize('username', ['directly-overridden-username-other'])
        def test_username_other(other_username):
            assert other_username == 'other-directly-overridden-username-other'

在上面的示例中,装置值被测试参数值覆盖。请注意,即使测试用例不直接使用它(在函数原型中没有提到它),也可以通过这种方式覆盖装置的值。

用非参数化装置覆盖参数化装置,反之亦然

给定测试文件结构是:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        import pytest

        @pytest.fixture(params=['one', 'two', 'three'])
        def parametrized_username(request):
            return request.param

        @pytest.fixture
        def non_parametrized_username(request):
            return 'username'

    test_something.py
        # content of tests/test_something.py
        import pytest

        @pytest.fixture
        def parametrized_username():
            return 'overridden-username'

        @pytest.fixture(params=['one', 'two', 'three'])
        def non_parametrized_username(request):
            return request.param

        def test_username(parametrized_username):
            assert parametrized_username == 'overridden-username'

        def test_parametrized_username(non_parametrized_username):
            assert non_parametrized_username in ['one', 'two', 'three']

    test_something_else.py
        # content of tests/test_something_else.py
        def test_username(parametrized_username):
            assert parametrized_username in ['one', 'two', 'three']

        def test_username(non_parametrized_username):
            assert non_parametrized_username == 'username'

在上面的示例中,参数化装置被非参数化版本覆盖,非参数化装置被某些测试模块的参数化版本覆盖。显然,这同样适用于测试文件夹级别。

使用其他项目的装置

通常提供 pytest 支持的项目将使用入口点,因此只需将这些项目安装到环境中即可使这些装置可用。

如果您想使用不使用入口点的项目中的装置,您可以在顶部 conftest.py 文件中定义 pytest_plugins 将该模块注册为插件。

假设您在 mylibrary.fixtures 中有一些装置,并且您想将它们重用到您的 app/tests 目录中。

您需要做的就是在 app/tests/conftest.py 中定义指向该模块的 pytest_plugins。

pytest_plugins = "mylibrary.fixtures"

这有效地将 mylibrary.fixtures 注册为插件,使其所有夹具和钩子可用于 app/tests 中的测试。

Note 有时用户会从其他项目中导入装置以供使用,但不建议这样做:将装置导入模块将在 pytest 中注册它们,如同在该模块中定义的那样。

这会产生轻微的后果,例如在 pytest --help 中多次出现,但不建议这样做,因为此行为可能会在未来版本中更改/停止工作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值