[譯]pytest文檔第5章

Chapter 5

pytest fixtures: explicit, modular, scalable

pytest fixtures: 明確、模塊化、可伸縮

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

測試夾具Fixtures的目的,是為了讓測試可以可靠地重復(fù)執(zhí)行提供一個(gè)基準(zhǔn)。pytest fixtures 在傳統(tǒng)的xUnit風(fēng)格的 setup/teardown 函數(shù)基礎(chǔ)上提出了顯著的提高:

  • fixtures 具有顯示的名字,并通過從函數(shù)、模塊、類或整改項(xiàng)目中聲明它們的使用來激活它們。
  • fixtures 以模塊化的方式實(shí)現(xiàn),因?yàn)槊總€(gè)fixture名稱都觸發(fā)一個(gè)fixture函數(shù),該函數(shù)可以使用其他fixture。
  • fixture 可用的范圍可以從簡單的單元測試到復(fù)雜的功能測試,允許根據(jù)配置和組件選項(xiàng)參數(shù)化fixtures和測試,或者跨函數(shù)、類、模塊或整個(gè)測試session范圍內(nèi)重用fixture。

此外,pytest 繼續(xù)支持傳統(tǒng)的xUnit風(fēng)格的setup,你可以混可兩種風(fēng)格,以增量的方式遷移到新的風(fēng)格,隨你喜歡。你也可以從已經(jīng)存在的unittest.TestCase風(fēng)格或nose based項(xiàng)目開始。

5.1 以函數(shù)參數(shù)形式的fixture

測試函數(shù)可以將fixture對象命名為輸入?yún)?shù)來接收他們。對于每一個(gè)參數(shù)名,具有該名稱的fixture函數(shù)提供fixture對象。fixture函數(shù)通過標(biāo)記@pytest.fixture來注冊。我們來看看一個(gè)簡單的自帶測試代碼的函數(shù)模塊,其中含有一個(gè)fixture和一個(gè)使用它的測試函數(shù):

# 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將發(fā)現(xiàn)并調(diào)用被@pytest.fixture標(biāo)記的smtp_connectionfixtrue 函數(shù)。運(yùn)行這個(gè)測試看起來像這樣:

$ 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 =========================

在故障回溯中,我們看到測試函數(shù)被smtp_connection參數(shù)調(diào)用了,smtplib.SMTP()實(shí)例被fixture函數(shù)創(chuàng)建。測試函數(shù)失敗在故意設(shè)置的assert 0上。下面是pytest調(diào)用測試函數(shù)使用的具體協(xié)議:

  1. 因?yàn)?code>test_前綴,pytest 找到test_ehlo。測試函數(shù)需要一個(gè)名為smtp_connection的函數(shù)參數(shù)。查找一個(gè)被fixtrue標(biāo)記的名為smtp_connection的函數(shù),發(fā)現(xiàn)了一個(gè)匹配的fixture函數(shù)。
  2. smtp_connection()被調(diào)用來創(chuàng)建一個(gè)實(shí)例。
  3. test_ehlo(<smtp_connection instance>)被調(diào)用并失敗在測試函數(shù)的最后一行

Note:如果你寫錯(cuò)了一個(gè)函數(shù)參數(shù)或者想使用的不可用,你將看到一個(gè)含有可用函數(shù)參數(shù)的列表的錯(cuò)誤
你通常可以執(zhí)行下列代碼查看可用的fixtures(以_開頭的fixtures只有在你加上-v選項(xiàng)的時(shí)候才會顯示)。
pytest --fixtures test_simplefactory.py

5.2 Fixtures: 一個(gè)依賴注入的典型例子

Fixtures 允許測試函數(shù)輕易地接收并處理特定的預(yù)初始化應(yīng)用程序?qū)ο?,不需要關(guān)心 import setup cleanup 的細(xì)節(jié)。這是依賴注入的一個(gè)典型例子,其中fixture函數(shù)充當(dāng)注入器的角色,而測試函數(shù)則是fixture對象的消費(fèi)者。

5.3 conftest.py共用fixture函數(shù)

如果在實(shí)現(xiàn)測試的過程中你意識到想要在多個(gè)測試文件中使用同一個(gè)fuxture函數(shù),你可以將其移動(dòng)到conftest.py文件中。你無需import這個(gè)fixture,pytest會自動(dòng)找到它。fixture函數(shù)的發(fā)現(xiàn)從測試類開始,然后是測試模塊,然后是conftest.py文件和內(nèi)置、第三方插件。
你也可以使用conftest.py文件實(shí)現(xiàn)每個(gè)目錄的本地插件。

5.4 共用測試數(shù)據(jù)

如果你想讓你的測試可以使用文件中的測試數(shù)據(jù),一個(gè)好方式是,通過加載這些數(shù)據(jù)到一個(gè)被測試使用的fixture里。這利用到了pytest的自動(dòng)緩存機(jī)制。

5.5 Scope:在類、模塊或會話中共用一個(gè)fixture實(shí)例

需要網(wǎng)絡(luò)訪問的fixtures依賴于連接性,通常創(chuàng)建這些非常耗時(shí)。擴(kuò)展一下前面的實(shí)例,我們可以向@pytest.fixture添加一個(gè)scope='module'參數(shù),以使每個(gè)測試模塊只調(diào)用一次被@pytest.fixture修飾的smtp_connectionfixture函數(shù)。在一個(gè)測試模塊里得多個(gè)測試函數(shù)將接受相同的smtp_connectionfixture實(shí)例,從而節(jié)省時(shí)間。scope參數(shù)接受的值可以是:function class module package or session. function 是缺省值。
下面的例子是將fixture函數(shù)放在了一個(gè)單獨(dú)的conftest.py文件中,所以在同一個(gè)路徑下的測試模塊都可以訪問這個(gè)fixture函數(shù):

# content of conftest.py
import pytest
import smtplib


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

這個(gè)fixture的名稱也是smtp_connection,你可以在任何conftest.py所在目錄或目錄下的測試或fixture中通過列出名稱smtp_connection作為入?yún)⒌男问絹碓L問它的結(jié)果。

# 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

我們故意插入導(dǎo)致失敗的assert 0語句,以便檢查發(fā)生了什么,現(xiàn)在運(yùn)行這個(gè)測試:

$ 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 =========================

你可以看見兩個(gè)assert 0的失敗,但是更重要的是你可以看到同樣的(module-scoped)smtp_connection對象傳入了兩個(gè)測試函數(shù)里,pytest顯示傳入?yún)?shù)中的值。因此,由于使用了同一個(gè)smtp_connection實(shí)例,兩個(gè)測試函數(shù)運(yùn)行起來與一個(gè)測試函數(shù)一樣快。
如果你決定你更想要個(gè)作用域是會話的smtp_connection實(shí)例,你可以簡單地聲明它:

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

最終,這個(gè)類的作用域的每個(gè)測試類將調(diào)用這個(gè)fixture一次。

Note: pytest一次只緩存一個(gè)fixture實(shí)例。這意味著當(dāng)使用一個(gè)參數(shù)化的fixture時(shí),pytest可以在指定的作用域內(nèi)多次調(diào)用一個(gè)fixture。

5.5.1 package作用域(實(shí)驗(yàn)中)

在pytest 3.7中,引入了package作用域。包作用域的fixture在一個(gè)包的測試都結(jié)束后完成。

警告:該功能還在實(shí)驗(yàn)中,如果在更多的使用中發(fā)現(xiàn)了嚴(yán)重的問題,可能在未來的版本中會被刪除。請謹(jǐn)慎使用此功能,并請務(wù)必向我們報(bào)告您發(fā)現(xiàn)的任何問題。

5.6 更高作用域的fixture會被首先實(shí)例化

在功能請求的特性中,更高作用域的fixture(例如session)比低作用域的fixture(例如functionclass)更先實(shí)例化。同一作用域的fixture的相對順序,依照在測試函數(shù)中的聲明順序和fixtures之間的依賴關(guān)系。
參看如下代碼:

@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依照以下順序?qū)嵗?/p>

  1. s1:是最高作用域的fixture(session)
  2. m1:是第二高作用域的fixture(module)
  3. tmpdir:是一個(gè)function作用域的fixture,被f1請求,需要被實(shí)例化,因?yàn)樗莊1的依賴項(xiàng)
  4. f1:是test_foo參數(shù)列表中的第一個(gè)function作用域的fixture
  5. f2:是test_foo參數(shù)列表中的最后一個(gè)function作用域的fixture

5.7 fixture結(jié)束/執(zhí)行teardown代碼

pytest支持fixture運(yùn)行到作用域外的時(shí)候執(zhí)行特殊的結(jié)束代碼。通過使用一個(gè)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()

不管測試的結(jié)果如何,printsmtp.close()聲明將在模塊的最后一個(gè)測試運(yùn)行結(jié)束后運(yùn)行。讓我們來運(yùn)行它:

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

2 failed in 0.12 seconds

我們看見smtp_connection實(shí)例在兩個(gè)測試運(yùn)行完成的時(shí)候結(jié)束了。

Note:如果我們用scope='function'裝飾fixture函數(shù),fixture的setup和cleanup會在每一個(gè)測試執(zhí)行。測試模塊的任一個(gè)用例都不用修改或者知道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鏈接將會在測試結(jié)束運(yùn)行后被關(guān)閉,因?yàn)?code>smtp_connection對象在with結(jié)束后自動(dòng)關(guān)閉。

Note:如果在setup代碼期間(yield關(guān)鍵字之前的代碼)發(fā)生了異常,teardown代碼(yield關(guān)鍵字之后的代碼)將不會被調(diào)用。
一個(gè)可供替代的運(yùn)行teardown代碼選項(xiàng)是使用request-context對象的addfinalizer方法來注冊結(jié)束函數(shù)。
這里是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方法工作方式都類似,都是在測試結(jié)束后調(diào)用他們的代碼,但是addfinalzer相較于yield有兩個(gè)不同的點(diǎn):

  1. 可能有多個(gè)結(jié)束方法。
  2. 如果fixturesetup代碼發(fā)生了異常,結(jié)束代碼依然會被調(diào)用。即使setup代碼發(fā)生了再多的創(chuàng)建失敗/獲取失敗,也能適時(shí)地關(guān)閉fixture創(chuàng)建的所有資源:
@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發(fā)生異常失敗了,C1C3將適時(shí)地關(guān)閉。當(dāng)然,如果在注冊結(jié)束函數(shù)前發(fā)生異常,它就不會運(yùn)行。

5.8 fixture 可以對請求它的測試內(nèi)容進(jìn)行自?。ǚ聪颢@取測試函數(shù)的環(huán)境)

fixture函數(shù)可以接受request對象,用來對“requesting”測試函數(shù)、類或者模塊內(nèi)容進(jìn)行自省。進(jìn)一步擴(kuò)展前面的smtp_connectionfixture例子,讓我們從一個(gè)使用我們的fixture的測試模塊讀取一個(gè)可選的服務(wù)器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屬性可選擇地從測試模塊獲取一個(gè)smtpserver屬性。如果我們只是再一次運(yùn)行,不會有什么變化:

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

2 failed in 0.12 seconds

讓我們快速地創(chuàng)建另一個(gè)測試模塊,實(shí)際地設(shè)置服務(wù)器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()

運(yùn)行它:

$ 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)

瞧!這個(gè)smtp_connection fixture函數(shù)從模塊的命名空間里獲取了我們的郵箱服務(wù)器名字。

5.9 將fixture作為工廠

factory as fixture模式,可以在“單個(gè)測試中多次需要fixture”的情況下提供幫助。fixture沒有直接返回?cái)?shù)據(jù),而是返回一個(gè)生成數(shù)據(jù)的函數(shù)。這個(gè)函數(shù)可以被測試多次調(diào)用。
工廠可以有所需的參數(shù):

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

如果工廠創(chuàng)建的數(shù)據(jù)需要管理,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 參數(shù)化fixtures

fixture函數(shù)可以在被參數(shù)化,參數(shù)化后被多次調(diào)用,每次執(zhí)行依賴測試集合,相當(dāng)于這些測試依賴于這個(gè)fixture。測試函數(shù)通常不會需要知道他們的重新運(yùn)行。fuxture參數(shù)有助于為組件編寫詳細(xì)的功能測試,組件本身可以通過多種方式配置。
擴(kuò)展之前的例子,我們可以標(biāo)記fixture來創(chuàng)建兩個(gè)smtp_connectionfixture實(shí)例,這將導(dǎo)致所有的測試使用rixture運(yùn)行兩次。fixture函數(shù)通過特殊的request對象訪問每個(gè)參數(shù):

# 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函數(shù)每次運(yùn)行會通過request.param訪問其中一個(gè)值的。不需要修改測試函數(shù)的代碼。那么我們跑一下:

$ 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

我們看見我們的兩個(gè)測試函數(shù)都跑了兩次,使用了不同的smtp_connection實(shí)例。

Note: 對于mail.python.orgtest_ehlo的第二次失敗是因?yàn)轭A(yù)期的服務(wù)器字符串與最終的不同。

pytest將構(gòu)建一個(gè)字符串,這個(gè)字符串是參數(shù)化fixture中的每個(gè)值得測試ID,比如在上面例子中的test_ehlo[smtp.gmail.com]test_ehlo[mail.python.org] 。這些ID可以和-k一起使用,以旋轉(zhuǎn)要運(yùn)行的特定cases,當(dāng)一個(gè)case失敗時(shí),他們還將標(biāo)記特定的cases。使用--collect-only運(yùn)行pytest將顯示生成的ID。
Numbers, strings, booleans and None將在測試ID中顯示他們通常的字符串表示形式。對于其他的對象,pytest將做一個(gè)基于參數(shù)名字的字符串??梢允褂?code>ids關(guān)鍵字參數(shù)為某個(gè)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

上面的內(nèi)容顯示ids可以是要使用的字符串列表,也可以是使用fixture值調(diào)用的函數(shù),然后必須返回要使用的字符串。在稍后的情況中,如果函數(shù)返回None,則使用pytest自動(dòng)生成的ID。
在下列被使用的測試測試ID運(yùn)行上面的測試,結(jié)果如下:

$ 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 使用參數(shù)化標(biāo)記

pytest.param()可以在參數(shù)化fixtrue的值集中應(yīng)用標(biāo)記,方法與@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

運(yùn)行這個(gè)測試將會跳過值為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函數(shù)中的fixtures

你不僅僅可以在測試函數(shù)中使用fixtures,還可以使用fixture函數(shù)可以使用其他的fixture。這有助于fixture的模塊化設(shè)計(jì),并允許跨許多項(xiàng)目重用特定框架的fixture。作為一個(gè)簡單的例子,我們可以擴(kuò)展之前的例子并實(shí)例化一個(gè)app對象,我們將已經(jīng)定義好的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

這里,我們聲明了一個(gè)appfixture,它會接收之前定義的smtp_connectionfixture并為它實(shí)例化一個(gè)app對象。讓我們運(yùn)行一下:

$ 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的參數(shù)化,測試將在兩個(gè)不同的App實(shí)例和各自的smtp服務(wù)器上運(yùn)行兩次。appfixture不需要關(guān)注smtp_connection參數(shù)化,因?yàn)閜ytest將全面分析fixture依賴關(guān)系圖。

Note:appfixture的作用域是模塊,且使用了一個(gè)模塊作用域的smtp_connectionfixture。如果smtp_connection的緩存在session作用域,這個(gè)示例仍然可以正常工作:fixture可以使用“更廣泛的”作用域fixture,但是反過來不行:session作用域的fixture不能以有意義的方式使用作用域?yàn)?code>module的fixture。

5.13 按照fixture實(shí)例自動(dòng)將測試分組

pytest 最小化了再測試運(yùn)行期間活動(dòng)fixture的數(shù)量。如果你有參數(shù)化的fixture,所有使用了它的測試將首先使用一個(gè)實(shí)例執(zhí)行,然后再下一個(gè)fixture實(shí)例創(chuàng)建之前調(diào)用終結(jié)器。除此之外,這還簡化了對創(chuàng)建和使用全局狀態(tài)的應(yīng)用程序的測試。
接下來的例子,使用了兩個(gè)參數(shù)化的fixtrue,一個(gè)作用域是每個(gè)模塊,所有的函數(shù)執(zhí)行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))

讓我們運(yùn)行這些測試,使用詳細(xì)模式并關(guān)注打印輸出:

$ 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 =========================

你可以見到參數(shù)化了的作用域是模塊的modarg資源影響了測試執(zhí)行的順序,從而導(dǎo)致了盡可能少的“活動(dòng)”資源。mod1的參數(shù)化資源的終結(jié)器將在mod2資源的setup之前執(zhí)行。
需要特別注意,test_0是完全獨(dú)立的且首先完成。然后使用mod1test_1執(zhí)行,然后是使用mod1test_2執(zhí)行,然后使用mod2test_1執(zhí)行,最后是使用mod2test_2執(zhí)行。
otherarg參數(shù)化資源(擁有函數(shù)作用域)在每一個(gè)使用它的測試之前set up并在其之后tear down。

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

有時(shí),測試函數(shù)不需要直接訪問fixture對象。例如,測試可能會操作一個(gè)空的目錄作為當(dāng)前工作目錄進(jìn)行操作,同時(shí)又不關(guān)心具體是什么目錄。下面是如何使用標(biāo)準(zhǔn)的tempfile和pytest fixture來實(shí)現(xiàn)它。我們將fixture的創(chuàng)建分離到conftest.py文件中:

# content of conftest.py

import pytest
import tempfile
import os


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

并且通過usefixture標(biāo)記在測試模塊中聲明它的使用:

# 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標(biāo)記,每個(gè)測試方法的運(yùn)行都需要請求cleanfirfixture,就好像你為他們每個(gè)指定了一個(gè)cleandir函數(shù)參數(shù)一樣。讓我們運(yùn)行一下來嚴(yán)重我們的fixture是激活并測試通過了:

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

你可以像這樣指定多個(gè)fixture:

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

你可以使用mark機(jī)制的通用特性,在測試模塊級別指定fixture的使用方式:

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

Note: 指定的變量必須名為pytestmark,例如foomark將不會激活fixture。也可以將項(xiàng)目中所有測試所需的fixture放入一個(gè)ini文件中:

# content of pytest.ini

[pytest]
usefixtures = cleandir

Warning:注意這里的標(biāo)記對fixture函數(shù)沒有效果。例如,這將不會像預(yù)期的那樣工作:

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

目前,這不會生產(chǎn)任何錯(cuò)誤或警告,但是這將由#3664處理。

5.15 自動(dòng)調(diào)用fixture(xUnit setup on steroids)

有時(shí)候,你可能想在不顯示聲明函數(shù)參數(shù)或usefixtures裝飾器的情況下自動(dòng)調(diào)用fixture。作為一個(gè)世紀(jì)的例子,假設(shè)我們有一個(gè)數(shù)據(jù)庫fixture,它具有一個(gè)begin/rollback/commit體系結(jié)構(gòu),并且我們希望通過事務(wù)和回滾自動(dòng)地包裹每個(gè)測試方法。這里是這個(gè)想法的虛擬的自包含實(shí)現(xiàn):

# 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被標(biāo)記了autouse=true,意味著類中的所有測試方法都將使用這個(gè)fixture,二不需要在測試函數(shù)簽名中聲明它,也不需要使用類級別的usefixture裝飾器。
如果我們運(yùn)行它,會得到兩個(gè)通過的測試:

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

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

  • autouse fixture遵循scope=參數(shù):如果一個(gè)autouse fixture具有scope='session',那么他講值運(yùn)行一次,無論它在哪里定義的。scope='class'表示它將在每個(gè)類中運(yùn)行一次,等等。
  • 如果在測試模塊中定義了一個(gè)autouse fixture,那么這個(gè)模塊下的所有測試函數(shù)都會自動(dòng)使用它。
  • 如果在conftest.py文件中定義了一個(gè)autouse fuxture,那么在同一個(gè)目錄下所有測試模塊中的所有測試都會調(diào)用該fixture
  • 最后,使用的時(shí)候請小心:如果在插件中定義了一個(gè)autouse fixture,那么它將被所有安裝了插件的項(xiàng)目中的所有測試使用。如果fixture只是在某些(例如在ini文件中)設(shè)置存在的情況下工作,那么它將非常有用。這樣的全局fixture應(yīng)該總是快速遞確定它是否應(yīng)該執(zhí)行任何工作,避免了其他昂貴的導(dǎo)入或計(jì)算。
    請注意,上面的transactionfixture很可能是希望你在項(xiàng)目中使用的fixture,而不需要它處于激活狀態(tài)。規(guī)范的方法是將這個(gè)事務(wù)定義在一個(gè)conftest.py文件中,而不是使用autouse
# content of conftest.py
@pytest.fixture
def transact(request, db):
    db.begin()
    yield
    db.rollback()

并且例如,有一個(gè)TestClass使用它來聲明需要使用這個(gè)fixture:

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

所有在TestClass里的測試方法將會使用transactionfixture,而模塊中的其他測試類或函數(shù)將不會使用它,除非它們也添加了transact引用。

5.16 覆蓋不同級別的fixture

在相對較大的測試套件中,你極大可能需要用本地定義的fixture覆蓋全局或根fixture,以保持測試代碼的可讀性和可維護(hù)性。

5.16.1覆蓋文件夾(conftest.py)級別的fixture

給定的測試文件結(jié)構(gòu)為:

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'

如你所見,一個(gè)相同名字的fixture可以被子文件夾中的fixture覆蓋。請注意,在上面的例子中,可以從overridingfixture輕易地訪問base或者superfixture。

5.16.2 在測試模塊級別覆蓋fixture

給定的測試文件結(jié)構(gòu)如下:

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 直接用測試的參數(shù)化覆蓋fixture

給定的測試文件結(jié)構(gòu)如下:

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的值被測試參數(shù)的值覆蓋了。要注意的是即使沒用直接使用fixture的值(在函數(shù)原型中沒有提到),也可以用這種方式覆蓋。

5.16.4 用非參數(shù)化的fixture覆蓋參數(shù)化的fixture,反過來也可以

給定的測試文件結(jié)構(gòu)如下:

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'

在上面的例子中,參數(shù)化的fixture被非參數(shù)化版本覆蓋,而非參數(shù)化fixture被某些測試模塊的參數(shù)化版本覆蓋。顯然,測試文件夾級別也是如此。
【第5章 完】

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容