[译]pytest文档第5章

Chapter 5

pytest fixtures: explicit, modular, scalable

pytest fixtures: 明确、模块化、可伸缩

翻译水平低,只是方便自己以后查看

测试夹具Fixtures的目的,是为了让测试可以可靠地重复执行提供一个基准。pytest fixtures 在传统的xUnit风格的 setup/teardown 函数基础上提出了显著的提高:

  • fixtures 具有显示的名字,并通过从函数、模块、类或整改项目中声明它们的使用来激活它们。
  • fixtures 以模块化的方式实现,因为每个fixture名称都触发一个fixture函数,该函数可以使用其他fixture。
  • fixture 可用的范围可以从简单的单元测试到复杂的功能测试,允许根据配置和组件选项参数化fixtures和测试,或者跨函数、类、模块或整个测试session范围内重用fixture。

此外,pytest 继续支持传统的xUnit风格的setup,你可以混可两种风格,以增量的方式迁移到新的风格,随你喜欢。你也可以从已经存在的unittest.TestCase风格或nose based项目开始。

5.1 以函数参数形式的fixture

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

# content of ./test_smtpsimple.py
import pytest
import smtplib


@pytest.fixture
def smtp_connection():
  return smtplib.SMTP('smtp.qq.com', 587, timout=5)


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

这里test_ehlo需要smtp_connectionfixture的值。pytest将发现并调用被@pytest.fixture标记的smtp_connectionfixtrue 函数。运行这个测试看起来像这样:

$ 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参数调用了,smtplib.SMTP()实例被fixture函数创建。测试函数失败在故意设置的assert 0上。下面是pytest调用测试函数使用的具体协议:

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

Note:如果你写错了一个函数参数或者想使用的不可用,你将看到一个含有可用函数参数的列表的错误
你通常可以执行下列代码查看可用的fixtures(以_开头的fixtures只有在你加上-v选项的时候才会显示)。
pytest --fixtures test_simplefactory.py

5.2 Fixtures: 一个依赖注入的典型例子

Fixtures 允许测试函数轻易地接收并处理特定的预初始化应用程序对象,不需要关心 import setup cleanup 的细节。这是依赖注入的一个典型例子,其中fixture函数充当注入器的角色,而测试函数则是fixture对象的消费者。

5.3 conftest.py共用fixture函数

如果在实现测试的过程中你意识到想要在多个测试文件中使用同一个fuxture函数,你可以将其移动到conftest.py文件中。你无需import这个fixture,pytest会自动找到它。fixture函数的发现从测试类开始,然后是测试模块,然后是conftest.py文件和内置、第三方插件。
你也可以使用conftest.py文件实现每个目录的本地插件。

5.4 共用测试数据

如果你想让你的测试可以使用文件中的测试数据,一个好方式是,通过加载这些数据到一个被测试使用的fixture里。这利用到了pytest的自动缓存机制。

5.5 Scope:在类、模块或会话中共用一个fixture实例

需要网络访问的fixtures依赖于连接性,通常创建这些非常耗时。扩展一下前面的实例,我们可以向@pytest.fixture添加一个scope='module'参数,以使每个测试模块只调用一次被@pytest.fixture修饰的smtp_connectionfixture函数。在一个测试模块里得多个测试函数将接受相同的smtp_connectionfixture实例,从而节省时间。scope参数接受的值可以是:function class module package or session. function 是缺省值。
下面的例子是将fixture函数放在了一个单独的conftest.py文件中,所以在同一个路径下的测试模块都可以访问这个fixture函数:

# content of conftest.py
import pytest
import smtplib


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

这个fixture的名称也是smtp_connection,你可以在任何conftest.py所在目录或目录下的测试或fixture中通过列出名称smtp_connection作为入参的形式来访问它的结果。

# content of test_module.py


def test_ehlo(smtp_connection):
  response, msg = smtp_connection.ehclo()
  assert response == 250
  assert b'smtp.qq.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
  ...

最终,这个类的作用域的每个测试类将调用这个fixture一次。

Note: pytest一次只缓存一个fixture实例。这意味着当使用一个参数化的fixture时,pytest可以在指定的作用域内多次调用一个fixture。

5.5.1 package作用域(实验中)

在pytest 3.7中,引入了package作用域。包作用域的fixture在一个包的测试都结束后完成。

警告:该功能还在实验中,如果在更多的使用中发现了严重的问题,可能在未来的版本中会被删除。请谨慎使用此功能,并请务必向我们报告您发现的任何问题。

5.6 更高作用域的fixture会被首先实例化

在功能请求的特性中,更高作用域的fixture(例如session)比低作用域的fixture(例如functionclass)更先实例化。同一作用域的fixture的相对顺序,依照在测试函数中的声明顺序和fixtures之间的依赖关系。
参看如下代码:

@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请求的fixtures依照以下顺序实例化:

  1. s1:是最高作用域的fixture(session)
  2. m1:是第二高作用域的fixture(module)
  3. tmpdir:是一个function作用域的fixture,被f1请求,需要被实例化,因为他是f1的依赖项
  4. f1:是test_foo参数列表中的第一个function作用域的fixture
  5. f2:是test_foo参数列表中的最后一个function作用域的fixture

5.7 fixture结束/执行teardown代码

pytest支持fixture运行到作用域外的时候执行特殊的结束代码。通过使用一个yield声明代替return,所有在yield声明之后的代码将会作为teardown代码:

# content of conftest.py

import smtplib
import pytest


@pytest.fixture(scope="module")
def smtp_connection():
  smtp_connection = smtplib.SMTP("smtp.qq.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实例在两个测试运行完成的时候结束了。

Note:如果我们用scope='function'装饰fixture函数,fixture的setup和cleanup会在每一个测试执行。测试模块的任一个用例都不用修改或者知道fixture的setup。
Note:我们同样可以将yieldwith类似地使用:

# content of test_yield2.py

import smtplib
import pytest


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

smtp_connection链接将会在测试结束运行后被关闭,因为smtp_connection对象在with结束后自动关闭。

Note:如果在setup代码期间(yield关键字之前的代码)发生了异常,teardown代码(yield关键字之后的代码)将不会被调用。
一个可供替代的运行teardown代码选项是使用request-context对象的addfinalizer方法来注册结束函数。
这里是smtp_commectionfixture修改为使用addfinalizer作为cleanup:

# 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方法工作方式都类似,都是在测试结束后调用他们的代码,但是addfinalzer相较于yield有两个不同的点:

  1. 可能有多个结束方法。
  2. 如果fixturesetup代码发生了异常,结束代码依然会被调用。即使setup代码发生了再多的创建失败/获取失败,也能适时地关闭fixture创建的所有资源:
@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发生异常失败了,C1C3将适时地关闭。当然,如果在注册结束函数前发生异常,它就不会运行。

5.8 fixture 可以对请求它的测试内容进行自省(反向获取测试函数的环境)

fixture函数可以接受request对象,用来对“requesting”测试函数、类或者模块内容进行自省。进一步扩展前面的smtp_connectionfixture例子,让我们从一个使用我们的fixture的测试模块读取一个可选的服务器URL:

# content of conftest.py
import pytest
import smtplib


@pytest.fixture(scope="module")
def smtp_connection(request):
  server = getattr(request.module, "smtpserver", "smtp.qq.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.qq.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函数从模块的命名空间里获取了我们的邮箱服务器名字。

5.9 将fixture作为工厂

factory as fixture模式,可以在“单个测试中多次需要fixture”的情况下提供帮助。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")

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

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

5.10 参数化fixtures

fixture函数可以在被参数化,参数化后被多次调用,每次执行依赖测试集合,相当于这些测试依赖于这个fixture。测试函数通常不会需要知道他们的重新运行。fuxture参数有助于为组件编写详细的功能测试,组件本身可以通过多种方式配置。
扩展之前的例子,我们可以标记fixture来创建两个smtp_connectionfixture实例,这将导致所有的测试使用rixture运行两次。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()

主要的变化是带有@pytest.fixture声明的praram,它是一组值,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.qq.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.qq.com" in msg
E   AssertionError: assert b'smtp.qq.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实例。

Note: 对于mail.python.orgtest_ehlo的第二次失败是因为预期的服务器字符串与最终的不同。

pytest将构建一个字符串,这个字符串是参数化fixture中的每个值得测试ID,比如在上面例子中的test_ehlo[smtp.gmail.com]test_ehlo[mail.python.org] 。这些ID可以和-k一起使用,以旋转要运行的特定cases,当一个case失败时,他们还将标记特定的cases。使用--collect-only运行pytest将显示生成的ID。
Numbers, strings, booleans and None将在测试ID中显示他们通常的字符串表示形式。对于其他的对象,pytest将做一个基于参数名字的字符串。可以使用ids关键字参数为某个fixture值定制测试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可以是要使用的字符串列表,也可以是使用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 =======================

5.11 使用参数化标记

pytest.param()可以在参数化fixtrue的值集中应用标记,方法与@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-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 ====================

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

你不仅仅可以在测试函数中使用fixtures,还可以使用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

这里,我们声明了一个appfixture,它会接收之前定义的smtp_connectionfixture并为它实例化一个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服务器上运行两次。appfixture不需要关注smtp_connection参数化,因为pytest将全面分析fixture依赖关系图。

Note:appfixture的作用域是模块,且使用了一个模块作用域的smtp_connectionfixture。如果smtp_connection的缓存在session作用域,这个示例仍然可以正常工作:fixture可以使用“更广泛的”作用域fixture,但是反过来不行:session作用域的fixture不能以有意义的方式使用作用域为module的fixture。

5.13 按照fixture实例自动将测试分组

pytest 最小化了再测试运行期间活动fixture的数量。如果你有参数化的fixture,所有使用了它的测试将首先使用一个实例执行,然后再下一个fixture实例创建之前调用终结器。除此之外,这还简化了对创建和使用全局状态的应用程序的测试。
接下来的例子,使用了两个参数化的fixtrue,一个作用域是每个模块,所有的函数执行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_REFIX/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是完全独立的且首先完成。然后使用mod1test_1执行,然后是使用mod1test_2执行,然后使用mod2test_1执行,最后是使用mod2test_2执行。
otherarg参数化资源(拥有函数作用域)在每一个使用它的测试之前set up并在其之后tear down

5.14 从class、module或者project中引用fixture

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

# content of conftest.py

import pytest
import tempfile
import os


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

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

# 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标记,每个测试方法的运行都需要请求cleanfirfixture,就好像你为他们每个指定了一个cleandir函数参数一样。让我们运行一下来严重我们的fixture是激活并测试通过了:

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

你可以像这样指定多个fixture:

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

你可以使用mark机制的通用特性,在测试模块级别指定fixture的使用方式:

pytestmark = pytest.mark.usefixture('cleandir')

Note: 指定的变量必须名为pytestmark,例如foomark将不会激活fixture。也可以将项目中所有测试所需的fixture放入一个ini文件中:

# content of pytest.ini

[pytest]
usefixtures = cleandir

Warning:注意这里的标记对fixture函数没有效果。例如,这将不会像预期的那样工作:

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

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

5.15 自动调用fixture(xUnit setup on steroids)

有时候,你可能想在不显示声明函数参数或usefixtures装饰器的情况下自动调用fixture。作为一个世纪的例子,假设我们有一个数据库fixture,它具有一个begin/rollback/commit体系结构,并且我们希望通过事务和回滚自动地包裹每个测试方法。这里是这个想法的虚拟的自包含实现:

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

class级别的transactfixture被标记了autouse=true,意味着类中的所有测试方法都将使用这个fixture,二不需要在测试函数签名中声明它,也不需要使用类级别的usefixture装饰器。
如果我们运行它,会得到两个通过的测试:

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

这里是在其他作用域 autouse的fixture怎么工作

  • autouse fixture遵循scope=参数:如果一个autouse fixture具有scope='session',那么他讲值运行一次,无论它在哪里定义的。scope='class'表示它将在每个类中运行一次,等等。
  • 如果在测试模块中定义了一个autouse fixture,那么这个模块下的所有测试函数都会自动使用它。
  • 如果在conftest.py文件中定义了一个autouse fuxture,那么在同一个目录下所有测试模块中的所有测试都会调用该fixture
  • 最后,使用的时候请小心:如果在插件中定义了一个autouse fixture,那么它将被所有安装了插件的项目中的所有测试使用。如果fixture只是在某些(例如在ini文件中)设置存在的情况下工作,那么它将非常有用。这样的全局fixture应该总是快速递确定它是否应该执行任何工作,避免了其他昂贵的导入或计算。
    请注意,上面的transactionfixture很可能是希望你在项目中使用的fixture,而不需要它处于激活状态。规范的方法是将这个事务定义在一个conftest.py文件中,而不是使用autouse
# content of conftest.py
@pytest.fixture
def transact(request, db):
    db.begin()
    yield
    db.rollback()

并且例如,有一个TestClass使用它来声明需要使用这个fixture:

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

所有在TestClass里的测试方法将会使用transactionfixture,而模块中的其他测试类或函数将不会使用它,除非它们也添加了transact引用。

5.16 覆盖不同级别的fixture

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

5.16.1覆盖文件夹(conftest.py)级别的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可以被子文件夹中的fixture覆盖。请注意,在上面的例子中,可以从overridingfixture轻易地访问base或者superfixture。

5.16.2 在测试模块级别覆盖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可以被某些测试模块覆盖。

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

5.16.4 用非参数化的fixture覆盖参数化的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'

在上面的例子中,参数化的fixture被非参数化版本覆盖,而非参数化fixture被某些测试模块的参数化版本覆盖。显然,测试文件夹级别也是如此。
【第5章 完】

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容