Pytest夹具:显式、模块化、可扩展

这个 purpose of test fixtures 是提供一个固定的基线,在此基础上测试可以可靠地重复执行。Pytest夹具比传统的XUnit设置/拆卸功能提供了显著的改进:

  • 装置有明确的名称,通过声明它们在测试函数、模块、类或整个项目中的使用来激活。

  • 夹具以模块化的方式实现,因为每个夹具名称触发 夹具功能 可以使用其他固定装置。

  • 夹具管理从简单的单元扩展到复杂的功能测试,允许根据配置和组件选项参数化夹具和测试,或者跨功能、类、模块或整个测试会话范围重复使用夹具。

此外,pytest继续支持 经典的Xunit风格设置 . 您可以混合这两种样式,根据您的喜好,逐步从经典样式移动到新样式。你也可以从现有的 unittest.TestCase stylenose based 项目。

fixtures作为函数参数

测试函数可以通过将fixture对象命名为输入参数来接收它们。对于每个参数名,具有该名称的fixture函数提供fixture对象。通过标记fixture函数来注册fixture函数 @pytest.fixture . 让我们来看一个简单的独立测试模块,它包含一个fixture和一个使用fixture的测试函数:

# content of ./test_smtpsimple.py
import pytest

@pytest.fixture
def smtp_connection():
    import smtplib
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

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

这里, test_ehlo 需要 smtp_connection 夹具值。Pytest将发现并调用 @pytest.fixture 标记 smtp_connection 夹具功能。运行测试的方式如下:

$ pytest test_smtpsimple.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 1 item

test_smtpsimple.py F                                                 [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 0 # for demo purposes
E       assert 0

test_smtpsimple.py:11: AssertionError
========================= 1 failed in 0.12 seconds =========================

在失败的回溯中,我们看到测试函数是用 smtp_connection 论点 smtplib.SMTP() fixture函数创建的实例。测试功能失败 assert 0 . 这是 pytest 以这种方式调用测试函数:

  1. 脓包 finds 这个 test_ehlo 因为 test_ 前缀。测试函数需要一个名为 smtp_connection . 通过查找名为 smtp_connection .

  2. smtp_connection() 调用以创建实例。

  3. test_ehlo(<smtp_connection instance>) 在测试函数的最后一行调用并失败。

请注意,如果您拼错了一个函数参数,或者希望使用一个不可用的参数,您将看到一个错误,其中包含一个可用函数参数列表。

注解

您可以随时发布:

pytest --fixtures test_simplefactory.py

查看可用的固定装置(带引线的固定装置 _ 仅当添加 -v 选择权。

fixtures:依赖注入的主要示例

fixture允许测试函数轻松地接收和处理特定的预初始化应用程序对象,而不必关心导入/设置/清理细节。这是一个主要的例子 dependency injection 其中fixture函数起到 injector 测试功能是 consumers fixture对象的。

conftest.py :共享夹具功能

如果在实现测试的过程中,您意识到要使用来自多个测试文件的fixture函数,可以将其移动到 conftest.py 文件。您不需要导入要在测试中使用的设备,Pytest会自动发现它。fixture函数的发现从测试类开始,然后是测试模块,然后 conftest.py 文件,最后是内置插件和第三方插件。

您也可以使用 conftest.py 要实现的文件 local per-directory plugins .

共享测试数据

如果您想让来自文件的测试数据对您的测试可用,一个很好的方法是将这些数据加载到一个夹具中,供测试使用。这利用了pytest的自动缓存机制。

另一个好方法是将数据文件添加到 tests folder. There are also community plugins available to help managing this aspect of testing, e.g. pytest-datadirpytest-datafiles .

范围:在类、模块或会话中跨测试共享一个fixture实例

需要网络访问的设备依赖于连接性,通常创建成本很高。扩展前面的示例,我们可以添加 scope="module" 参数 @pytest.fixture 引起装饰的调用 smtp_connection 每个测试只调用一次fixture函数 模块 (默认情况下,每个测试调用一次 功能 )因此,一个测试模块中的多个测试功能将接收相同的 smtp_connection 夹具实例,节省时间。的可能值 scope 是: functionclassmodulepackagesession .

下一个示例将fixture函数放入单独的 conftest.py 文件,以便目录中多个测试模块的测试可以访问fixture函数:

# content of conftest.py
import pytest
import smtplib

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

夹具的名称是 smtp_connection 您可以通过列出名称来访问它的结果 smtp_connection 作为任何测试或fixture函数的输入参数(位于 conftest.py 位于)::

# 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

我们故意插入失败 assert 0 语句以检查正在进行的操作,现在可以运行测试:

$ pytest test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-5.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:6: 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:11: AssertionError
========================= 2 failed in 0.12 seconds =========================

你看这两个 assert 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 needing it
    ...

最后, class 作用域将在每次测试中调用fixture一次 .

注解

pytest一次只缓存一个fixture实例。这意味着当使用参数化夹具时,pytest可以在给定的范围内多次调用夹具。

package 范围(实验)

在Pytest 3.7中, package 范围已引入。当最后一次测试 包裹 完成。

警告

考虑到该功能 实验的 如果在将来的版本中发现隐藏的角落情况或此功能的严重问题,则可以将其删除,因为它在野外得到了更多的使用。

请谨慎使用此新功能,并确保报告您发现的任何问题。

顺序:首先实例化更高范围的设备

在特性的功能请求中,更高范围的夹具(例如 session )先实例化,然后再实例化范围较低的设备(例如 functionclass )。相同范围内的固定装置的相对顺序遵循测试功能中声明的顺序,并尊重固定装置之间的依赖关系。在显式使用fixture之前,将实例化autouse fixture。

考虑下面的代码:

import pytest

# fixtures documentation order example
order = []


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


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


@pytest.fixture
def f1(f3):
    order.append("f1")


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


@pytest.fixture(autouse=True)
def a1():
    order.append("a1")


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


def test_order(f1, m1, f2, s1):
    assert order == ["s1", "m1", "a1", "f3", "f1", "f2"]

所要求的固定装置 test_foo 将按以下顺序实例化:

  1. s1 :是范围最高的夹具 (session

  2. m1 :是第二高范围夹具 (module )。因为它依赖于 f1 .

  3. a1 是一个 function -范围 autouse fixture:它将在同一范围内的其他fixture之前实例化。

  4. f3 是一个 function -范围夹具,要求 f1 :此时需要实例化

  5. f1 是第一个 function -范围内夹具 test_foo 参数列表。

  6. f2 是最后一个 function -范围内夹具 test_foo 参数列表。

夹具定型/执行拆卸代码

当fixture超出范围时,pytest支持fixture特定的定稿代码的执行。通过使用 yield 语句而不是 return ,之后的所有代码 产量 语句用作拆卸代码:

# content of conftest.py

import smtplib
import pytest


@pytest.fixture(scope="module")
def smtp_connection():
    smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
    yield smtp_connection  # provide the fixture value
    print("teardown smtp")
    smtp_connection.close()

这个 printsmtp.close() 语句将在模块中的最后一个测试完成执行后执行,而不管测试的异常状态如何。

让我们执行它:

$ pytest -s -q --tb=no
FFteardown smtp

2 failed in 0.12 seconds

我们看到了 smtp_connection 实例在两个测试完成执行后完成。请注意,如果我们用 scope='function' 然后在每个测试周围进行夹具设置和清理。无论哪种情况,测试模块本身都不需要更改或了解这些夹具设置的细节。

请注意,我们还可以无缝地使用 yield 语法与 with 声明:

# content of test_yield2.py

import smtplib
import pytest


@pytest.fixture(scope="module")
def smtp_connection():
    with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
        yield smtp_connection  # provide the fixture value

这个 smtp_connection 连接将在测试完成执行后关闭,因为 smtp_connectionwith 语句结束。

无论fixture是否 设置 代码引发异常。即使其中一个无法创建/获取,也可以方便地正确关闭设备创建的所有资源:

# content of test_yield3.py

import contextlib

import pytest


@contextlib.contextmanager
def connect(port):
    ...  # create connection
    yield
    ...  # close connection


@pytest.fixture
def equipments():
    with contextlib.ExitStack() as stack:
        yield [stack.enter_context(connect(port)) for port in ("C1", "C3", "C28")]

在上面的示例中,如果 "C28" 异常失败, "C1""C3" 仍将正确关闭。

请注意,如果在 设置 代码(在 yield 关键字) 拆卸 代码(后) yield )不会被调用。

执行的替代选项 拆卸 代码是利用 addfinalizer 方法 request-context 对象以注册终结函数。

这里是 smtp_connection addfinalizer 清理:

# content of conftest.py
import smtplib
import pytest


@pytest.fixture(scope="module")
def smtp_connection(request):
    smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)

    def fin():
        print("teardown smtp_connection")
        smtp_connection.close()

    request.addfinalizer(fin)
    return smtp_connection  # provide the fixture value

这里是 equipments 夹具改为使用 addfinalizer 清理:

# content of test_yield3.py

import contextlib
import functools

import pytest


@contextlib.contextmanager
def connect(port):
    ...  # create connection
    yield
    ...  # close connection


@pytest.fixture
def equipments(request):
    r = []
    for port in ("C1", "C3", "C28"):
        cm = connect(port)
        equip = cm.__enter__()
        request.addfinalizer(functools.partial(cm.__exit__, None, None, None))
        r.append(equip)
    return r

两个 yieldaddfinalizer 方法通过在测试结束后调用它们的代码来实现类似的工作。当然,如果在注册finalize函数之前发生异常,则不会执行它。

fixtures可以反省请求的测试上下文

夹具功能可以接受 request 对象内省“请求”测试函数、类或模块上下文。进一步扩展前一个 smtp_connection fixture示例,让我们从使用fixture的测试模块中读取一个可选的服务器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 %s (%s)" % (smtp_connection, server))
    smtp_connection.close()

我们使用 request.module 属性以选择获取 smtpserver 来自测试模块的属性。如果我们再执行一次,没有什么改变:

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

2 failed in 0.12 seconds

让我们快速创建另一个测试模块,该模块在其模块命名空间中实际设置服务器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:5: 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)

哇!这个 smtp_connection fixture函数从模块名称空间中获取邮件服务器名称。

工厂作为固定装置

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

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

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

参数化夹具

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

扩展前面的示例,我们可以标记fixture以创建两个 smtp_connection fixture实例,它将导致使用fixture的所有测试运行两次。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 %s" % smtp_connection)
    smtp_connection.close()

主要变化是 params 具有 @pytest.fixture ,fixture函数将执行的每个值的列表,可以通过 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:6: 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:11: 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:5: 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:11: AssertionError
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef>
4 failed in 0.12 seconds

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

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

数字、字符串、布尔值和无将在测试ID中使用它们通常的字符串表示形式。对于其他对象,pytest将根据参数名生成字符串。可以通过使用 ids 关键字参数:

# 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 可以是要使用的字符串列表,也可以是将使用fixture值调用的函数,然后必须返回要使用的字符串。在后一种情况下,如果函数返回 None 然后将使用pytest的自动生成的ID。

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

$ pytest --collect-only
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-5.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]>

======================= no tests ran in 0.12 seconds =======================

对参数化夹具使用标记

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

运行此测试将 skip 调用 data_set 有价值 2

$ pytest test_fixture_marks.py -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-5.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                          [100%]

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

模块化:使用fixture函数中的fixture

不仅可以在测试函数中使用fixture,fixture函数还可以使用其他fixture本身。这有助于您的夹具的模块化设计,并允许在许多项目中重用框架特定的夹具。作为一个简单的示例,我们可以扩展前面的示例并实例化一个对象 app 我们把已经定义好的 smtp_connection 其中的资源:

# content of test_appsetup.py

import pytest

class App(object):
    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 fixture并实例化 App 对象。让我们运行它:

$ pytest -v test_appsetup.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-5.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.12 seconds =========================

由于参数化 smtp_connection ,测试将运行两次 App 实例和相应的SMTP服务器。没有必要 app 夹具应注意 smtp_connection 参数化,因为Pytest将充分分析夹具依赖关系图。

请注意 app 夹具的范围 module 并使用模块范围 smtp_connection 固定装置。如果 smtp_connection 被缓存在 session 范围:fixture可以使用“更广”范围的fixture,但不能使用另一种方式:会话范围的fixture不能以有意义的方式使用模块范围的fixture。

按设备实例自动分组测试

在测试运行期间,Pytest最小化活动设备的数量。如果您有一个参数化的fixture,那么使用它的所有测试将首先用一个实例执行,然后在创建下一个fixture实例之前调用终结器。此外,这简化了对创建和使用全局状态的应用程序的测试。

下面的示例使用两个参数化的设备,其中一个在每个模块的基础上确定范围,所有功能都执行 print 调用以显示设置/拆卸流:

# content of test_module.py
import pytest

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

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

def test_0(otherarg):
    print("  RUN test0 with otherarg %s" % otherarg)
def test_1(modarg):
    print("  RUN test1 with modarg %s" % modarg)
def test_2(otherarg, modarg):
    print("  RUN test2 with otherarg %s and modarg %s" % (otherarg, modarg))

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

$ pytest -v -s test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-5.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.12 seconds =========================

您可以看到参数化模块的作用域 modarg 资源导致测试执行的顺序,导致可能的“活动”资源最少。的终结器 mod1 参数化资源在 mod2 资源已设置。

特别要注意,测试_0是完全独立的,首先完成。然后使用 mod1 ,然后用测试 mod1 ,然后用 mod2 最后用 mod2 .

这个 otherarg 参数化资源(具有功能范围)在使用它的每个测试之前设置,然后在使用它的每个测试之后拆下。

使用类、模块或项目中的设备

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

# content of conftest.py

import pytest
import tempfile
import os

@pytest.fixture()
def cleandir():
    newpath = tempfile.mkdtemp()
    os.chdir(newpath)

并通过一个 usefixtures 标记::

# content of test_setenv.py
import os
import pytest

@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit(object):
    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 每个测试方法的执行都需要fixture,就像您为每个方法指定了“cleandir”函数参数一样。让我们运行它来验证夹具是否激活,测试是否通过:

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

您可以这样指定多个设备:

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

您可以使用标记机制的一般特性在测试模块级别指定夹具的使用:

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

注意分配的变量 must 被称为 pytestmark ,例如 foomark 不会激活设备。

也可以将项目中所有测试所需的设备放入一个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 .

自动固定装置(Xunit类固醇装置)

有时,您可能希望在不显式声明函数参数或 usefixtures 装饰符。作为一个实际的例子,假设我们有一个数据库设备,它有一个begin/rollback/commit体系结构,并且我们希望通过一个事务和一个rollback自动包围每个测试方法。下面是这个想法的一个虚拟的独立实现:

# content of test_db_transact.py

import pytest

class DB(object):
    def __init__(self):
        self.intransaction = []
    def begin(self, name):
        self.intransaction.append(name)
    def rollback(self):
        self.intransaction.pop()

@pytest.fixture(scope="module")
def db():
    return DB()

class TestClass(object):
    @pytest.fixture(autouse=True)
    def transact(self, request, db):
        db.begin(request.function.__name__)
        yield
        db.rollback()

    def test_method1(self, db):
        assert db.intransaction == ["test_method1"]

    def test_method2(self, db):
        assert db.intransaction == ["test_method2"]

类级别 transact 夹具标有 autouse=true 这意味着类中的所有测试方法都将使用这个fixture,而不需要在测试函数签名或类级别中声明它。 usefixtures 装饰符。

如果我们运行它,我们会得到两个通过的测试:

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

以下是Autouse装置在其他范围中的工作方式:

  • 自动装置遵守 scope= 关键字参数:如果autouse fixture具有 scope='session' 它将只运行一次,无论在何处定义。 scope='class' 意味着它将每个类运行一次,等等。

  • 如果在一个测试模块中定义了一个autouse fixture,那么它的所有测试函数都会自动使用它。

  • 如果在conftest.py文件中定义了一个autouse fixture,那么其目录下所有测试模块中的所有测试都将调用该fixture。

  • 最后,以及 请小心使用 :如果您在一个插件中定义了一个autouse fixture,它将对安装该插件的所有项目中的所有测试进行调用。如果一个设备只在某些设置(例如在ini文件中)下工作,这是有用的。这样一个全局夹具应该总是快速确定它是否应该做任何工作,并避免其他昂贵的导入或计算。

注意上面 transact fixture很可能是您希望在项目中可用的一个fixture,而不需要它通常处于活动状态。实现这一点的规范方法是将Transact定义放入conftest.py文件中。 没有 使用 autouse ::

# content of conftest.py
@pytest.fixture
def transact(request, db):
    db.begin()
    yield
    db.rollback()

然后,例如,通过声明需求,让一个testclass使用它:

@pytest.mark.usefixtures("transact")
class TestClass(object):
    def test_method1(self):
        ...

此测试类中的所有测试方法都将使用事务夹具,而模块中的其他测试类或函数将不使用它,除非它们还添加了 transact 参考文献。

覆盖不同级别的设备

在相对较大的测试套件中,您很可能需要 overrideglobalroot 夹具与A locally 定义了一个,保持测试代码的可读性和可维护性。

覆盖文件夹(conftest)级别上的fixture

假设测试文件结构为:

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'

如您所见,具有相同名称的fixture可以被某些测试文件夹级别覆盖。请注意 basesuper 夹具可从 overriding 夹具很容易-在上面的例子中使用。

覆盖测试模块级别上的夹具

假设测试文件结构为:

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'

在上面的示例中,可以为某些测试模块重写具有相同名称的fixture。

通过直接测试参数化覆盖夹具

假设测试文件结构为:

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'

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

用非参数化夹具替代参数化夹具,反之亦然。

假设测试文件结构为:

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'

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