pytest学习3: pytest fixtures explicit, modular, scalable

Pytest fixtures: explicit, modular, scalable

Pytest 固件:显式、模块化、可扩展

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

  • 固件有明确的名称,通过声明它们在测试函数、模块、类或整个项目中的使用来激活。
  • 固件以模块化的方式实现,因为每个固件名称触发一个 固件功能 , 可以使用其他固件。
  • 固件管理规模从简单的单元扩展到复杂的功能测试,允许根据配置和组件选项参数化固件和测试,或者跨功能、类、模块或整个测试会话范围重复使用固件。

此外,pytest继续支持 经典的Xunit-style setup. 你可以混合这两种样式,根据喜好,逐步从经典样式转移到新样式。你也可以从现有的 unittest.TestCase style 或 nose based 项目开始。

1. Fixtures as function arguments

fixtures作为函数参数

测试函数可以通过将fixture对象命名为输入参数来接收它们。对于每个参数名,具有该名称的fixture函数提供fixture对象。通过用@pytest.fixture标记fixture函数来注册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-4.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 参数、 fixture函数创建的实例smtplib.SMTP()来调用的。测试功能失败是故意使用了 assert 0 . pytest 以这种方式来调用测试函数:

  1. pytest找到这个 test_ehlo 因为 test_ 前缀。这个测试函数需要一个名为 smtp_connection . 通过查找名为 smtp_connection 标记的固件函数,找到一个匹配的固件函数.
  2. smtp_connection() 通过创建实例来调用。
  3. test_ehlo(<smtp_connection instance>) 在测试函数的最后一行调用并失败。

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

注解

你可以随时发布:

pytest --fixtures test_simplefactory.py

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

2. fixtures:a prime example of dependency injection

固件:依赖注入的主要示例

fixture允许测试函数轻松地接收和处理特定的预初始化应用程序对象,而不必关心import/setup/teardown细节。这是一个dependency injection的主要例子,其中fixture函数扮演的是injector(注入), 测试函数是固件对象的consumers(消费者) 。

3. conftest.py :sharing fixture functions

共享固件功能

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

你也可以使用 conftest.py 文件去实现 local per-directory plugins.

4. Sharing test data

共享测试数据

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

另一个好方法是将数据文件添加到 tests 文件夹中. 这里也有可用的插件社区可以用来帮助管理这方面的testing e.g. pytest-datadir 和 pytest-datafiles.

5. Scope: sharing a fixture instance across tests in a class, module or session

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

依赖于连接性的需要访问网络的fixtures,通常创建成本很高。扩展前面的示例,我们可以给 @pytest.fixture 调用添加一个 scope="module" 参数,引起被装饰的 smtp_connection 固件函数在每个测试模块中只调用一次(默认情况下,每个测试调用一次 function)因此,一个测试模块中的多个测试功能将接收相同的 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-4.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 失败,更重要的是,你也可以看到相同的(module-scoped模块范围) 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 作用域将在每个测试class中调用fixture一次 .

注解

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

package scope(experimental)

3.7 新版功能.

在Pytest 3.7中, package 范围已引入。当最后一次package测试结束时,Package-scoped fixtures 完成。

警告

考虑到该功能时 实验性的 ,如果在将来的版本中发现隐藏的角落情况或此功能的严重问题,则可能将其删除。

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

6. Higher-scoped fixtures are instantiated first

首先实例化更大范围的固件

在特性的功能请求中,更高范围的固件(例如 session )先实例化,然后再实例化范围较低的固件(例如 functionclass )。相同范围内固件的相对顺序遵循测试函数中声明的顺序,并尊重固件之间的依赖关系。

考虑下面的代码:

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

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

@pytest.fixture
def f1(tmpdir):
    pass

@pytest.fixture
def f2():
    pass

def test_foo(f1, m1, f2, s1):
    ...

test_foo 所请求的固件将按以下顺序实例化:

  1. s1 :是范围最高的固件 (session
  2. m1 :是第二高范围固件 (module
  3. tmpdir 是一个 function 范围固件, f1需要 :因为它是f1的一个依赖项,此时它需要实例化 .
  4. f1 是第一个在 test_foo 参数列表内的 function -范围固件。
  5. f2 是最后一个在 test_foo 参数列表内的 function -范围固件。

7. Fixture finalization / executing teardown code

固件定型/执行拆卸代码

当fixture超出范围时,pytest支持fixture特定的定稿代码的执行。通过使用 yield 语句而不是 returnyield语句之后的所有代码当做是teardown代码:

# 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'来声明固件函数, 然后在每个测试周围进行fixture setup和clearup。无论哪种情况,测试模块本身都不需要更改或了解这些固件setup的细节。

请注意,我们还可以无缝地使用 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_connection 对象会自动关闭当 with 语句结束时。

请注意,如果一个异常在 setup 代码(在 yield 关键字前)中发生,那么 teardown 代码(在 yield 后)不会被调用。

执行 teardown 代码的替代选项是利用 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

相似地, yieldaddfinalizer 方法都是通过在测试结束后调用它们的代码来工作,但是 addfinalizer 有两个关键差异点区别于 yield

  1. 可以注册多个终结器函数。

  2. 无论fixture的 setup 代码是否引发异常,finalizers终结器都会被调用。这对于正确关闭由固件创建的所有资源非常方便,即使其中一个资源未能创建/获取:

    @pytest.fixture
    def equipments(request):
        r = []
        for port in ('C1', 'C3', 'C28'):
            equip = connect(port)
            request.addfinalizer(equip.disconnect)
            r.append(equip)
        return r
    

    在上面的示例中,如果 "C28" 异常失败, "C1""C3" 仍将正确关闭。当然,如果在注册finalize函数之前发生异常,则不会执行它。

8. Fixtures can introspect the requesting test context

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

固件函数可以接受 request 对象内省“requesting”测试函数、类或模块上下文。进一步扩展前一个 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函数从模块名称空间中获取邮件服务器名称。

9. Factories as fixtures

工厂作为固件

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

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

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

10. Parametrizing fixtures

参数化固件

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 实例。还要注意的是, 在test_ehlomail.python.org连接第二次测试失败,因为预期的服务器字符串与实际获取的字符串不同。

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

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

# 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-4.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 =======================

11. Using marks with parametrized fixtures

对参数化固件使用标记

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-4.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 ====================

12. Modularity: using fixtures from a fixture function

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

不仅可以在测试函数中使用fixture,fixture函数还可以使用其他fixture本身。这有助于固件fixtures的模块化设计,并允许在许多项目中重用特定框架的固件。作为一个简单的示例,我们可以扩展前面的示例并实例化一个对象 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-4.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,但不能使用另一种方式:会话session范围的fixture不能以有意义的方式使用模块module范围的fixture。

13. Automatic grouping of tests by fixture instances

按fixture实例自动分组测试

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

下面的示例使用两个参数化的fixture,其中一个在每个模块的基础上确定范围,所有功能都执行 print 调用以显示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 %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-4.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 资源setup前被执行。

特别要注意,test_0是完全独立的,最先完成。然后是使用 mod1 的test_1执行,接着是使用 mod1的test_2 ,然后是用 mod2的test_1, 最后是用 mod2 的test_2.

这个 otherarg 参数化资源(具有函数范围)在使用它的每个测试之前setup,然后在使用它的每个测试之后teardown。

14. Using fixtures from classes, modules or projects

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

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

# content of conftest.py

import pytest
import tempfile
import os

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

并通过一个 usefixtures 标记,在测试module中声明它的使用:

# 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”函数参数一样。让我们运行它来验证fixture是否激活,测试是否通过:

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

可以这样指定多个fixtures:

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

可以使用标记机制的通用特性,在测试模块级别指定fixture使用:

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

注意分配的变量 必须 被称为 pytestmark ,例如 foomark 不会激活fixtures。

也可以将项目中所有测试所需的fixtures放入一个ini文件中:

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

警告

注意这个标记在 fixture functions里没有作用 . 例如,这个 无法按预期工作

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

目前,这不会产生任何错误或警告.

15. Autouse fixtures (xUnit setup on steroids)

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

有时,你可能希望在不显式声明函数参数或 usefixtures装饰器的情况下自动调用fixtures。作为一个实际的例子,假设我们有一个数据库设备,它有一个begin/rollback/commit体系结构,并且我们希望通过一个transaction和一个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,而不需要在测试函数签名或类级别的usefixture装饰器中声明它。

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

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

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

  • autouse fixtures遵守 scope= 关键字参数:如果一个autouse fixture具有 scope='session' ,无论它在何处被定义,它将只运行一次。而 scope='class' 意味着它将每个类运行一次,等等。
  • 如果在一个测试模块中定义了一个autouse fixture,那么它的所有测试函数都会自动使用它。
  • 如果在conftest.py文件中定义了一个autouse fixture,那么其目录下的所有测试模块中的所有测试都将调用该fixture。
  • 最后, 请小心使用 :如果你在一个插件中定义了一个autouse fixture,它将对安装该插件的所有项目中的所有测试进行调用。如果一个fixture只在某些设置(例如在ini文件中)下工作,这是有用的。这样一个全局fixture应该总是快速确定它是否应该做任何工作,并避免其他耗费的导入或计算。

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

# 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):
        ...

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

16. Overriding fixtures on various levels

覆盖不同级别的设备

在相对较大的测试套中,你很可能需要 override 一个 globalroot fixture与 locally 定义一个,用来保持测试代码的可读性和可维护性。

(1)Override a fixture on a folder (conftest) level

覆盖文件夹(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 fixture可从 在上面的例子中很容易使用的overriding fixture中获取。

(2)Override a fixture on a test module level

在测试模块级别上覆盖fixture

假设测试文件结构为:

tests/
    __init__.py

    conftest.py
        # content of tests/conftest.py
        @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可以为某些测试模块重写覆盖。

(3)Override a fixture with direct test parametrization

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

假设测试文件结构为:

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值被测试参数值覆盖。注意,即使测试没有直接使用fixture的值(在函数原型中没有提到),也可以用这种方式覆盖fixture的值。

(4)Override a parametrized fixture with non-parametrized one and vice versa

用非参数化固件替代参数化固件,反之亦然。

假设测试文件结构为:

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'

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值