Pytest權威教程05-Pytest fixtures:清晰 模塊化 易擴展


返回: Pytest權威教程

Pytest fixtures:清晰 模塊化 易擴展

2.0/2.3/2.4版本新函數
text fixtures的目的是為測試的重復執行提供一個可靠的固定基線。 pytest fixture比經典的xUnit setUp/tearDown方法有着顯着的改進:

  • fixtures具有明確的名稱,在測試用例/類/模塊或整個項目中通過聲明使用的fixtures名稱來使用。
  • fixtures以模塊化方式實現,因為每個fixture名稱都會觸發調用fixture函數,該fixture函數本身可以使用其它的fixtures。
  • 從簡單的單元測試到復雜的函數測試,fixtures的管理允許根據配置和組件選項對fixtures和測試用例進行參數化,或者在測試用例/類/模塊或整個測試會話范圍內重復使用該fixture。

此外,pytest繼續支持經典的xUnit風格的setup方法。 你可以根據需要混合使用兩種樣式,逐步從經典樣式移動到新樣式。 你也可以從現有的unittest.TestCase樣式或基於nose的項目開始。

Fixtures作為函數參數使用

測試用例可以通過在其參數中使用fixtures名稱來接收fixture對象。 每個fixture參數名稱所對應的函數,可以通過使用@pytest.fixture注冊成為一個fixture函數,來為測試用例提供一個fixture對象。 讓我們看一個只包含一個fixture和一個使用它的測試用例的簡單獨立測試模塊:

# ./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來提供fixture對象。pytest將發現並調用帶@pytest.fixture裝飾器的smtp_connection fixture函數。 運行測試如下所示:

$ pytest test_smtpsimple.py
=========================== test session starts ============================
platform linux -- Python 3.x.y,pytest-3.x.y,py-1.x.y,pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR,inifile:
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用這種方式調用測試用例使用的確切協議:

Fixtures: 依賴注入的主要例子

Fixtures允許測試用例能輕松引入預先定義好的初始化准備函數,而無需關心導入/設置/清理方法的細節。 這是依賴注入的一個主要示例,其中fixture函數的函數扮演”注入器“的角色,測試用例來“消費”這些fixture對象。

conftest.py: 共享fixture函數

如果在測試中需要使用多個測試文件中的fixture函數,則可以將其移動到conftest.py文件中,所需的fixture對象會自動被Pytest發現,而不需要再每次導入。 fixture函數的發現順序從測試類開始,然后是測試模塊,然后是conftest.py文件,最后是內置和第三方插件。

你還可以使用conftest.py文件來實現本地每個目錄的插件。

共享測試數據

如果要使用數據文件中的測試數據,最好的方法是將這些數據加載到fixture函數中以供測試用例注入使用。這利用到了pytest的自動緩存機制。

另一個好方法是在tests文件夾中添加數據文件。 還有社區插件可用於幫助處理這方面的測試,例如:pytest-datadirpytest-datafiles

生效范圍:在測試類/測試模塊/測試會話中共享fixture對象

由於fixtures對象需要連接形成依賴網,而通常創建時間比較長。 擴展前面的示例,我們可以在@pytest.fixture調用中添加scope ="module"參數,以使每個測試模塊只調用一次修飾的smtp_connection fixture函數(默認情況下,每個測試函數調用一次)。 因此,測試模塊中的多個測試用例將各自注入相同的smtp_connectionfixture對象,從而節省時間。scope參數的可選值包括:function(函數),class(類),module(模塊),package(包)及 session(會話)。

下一個示例將fixture函數放入單獨的conftest.py文件中,以便來自目錄中多個測試模塊的測試可以訪問fixture函數:

# conftest.py文件內容
import pytest
import smtplib

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

fixture對象的名稱依然是smtp_connection,你可以通過在任何測試用例或fixture函數(在conftest.py所在的目錄中或下面)使用參數smtp_connection作為輸入參數來訪問其結果:

# 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,inifile:
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的兩個測試用例運行速度與單個函數一樣快,因為它們重用了相同的fixture對象。

如果你決定要使用session(會話,一次運行算一次會話)范圍的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實例。 這意味着當使用參數化fixture時,pytest可能會在給定范圍內多次調用fixture函數。

package(包)范圍的fixture(實驗性函數)
3.7版本新函數
在pytest 3.7中,引入了包范圍。 當包的最后一次測試結束時,最終確定包范圍的fixture函數。

警告:
此函數是實驗性的,如果在獲得更多使用后發現隱藏的角落情況或此函數的嚴重問題,可能會在將來的版本中刪除。

謹慎使用此新函數,請務必報告你發現的任何問題。

高范圍的fixture函數優先實例化

3.5版本新函數
在測試函數的fixture對象請求中,較高范圍的fixture(例如session會話級)較低范圍的fixture(例如function函數級或class類級優先執行。相同范圍的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(會話級)
  2. m1:是第二高的fixture(模塊級)
  3. tmpdir:是一個函數級的fixture,f1依賴它,因此它需要在f1前調用
  4. f1:是test_foo參數列表中第一個函數范圍的fixture。
  5. f2:是test_foo參數列表中最后一個函數范圍的fixture。

fixture結束/執行teardown代碼

當fixture超出范圍時,通過使用yield語句而不是return,pytest支持fixture執行特定的teardown代碼。yield語句之后的所有代碼都視為teardown代碼:

# 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()語句將在模塊中的最后一個測試完成執行時執行。

讓我們執行一下(上文的test_module.py):

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

2 failed in 0.12 seconds

我們看到smtp_connection實例在兩個測試完成執行后完成。 請注意,如果我們使用scope ='function'修飾我們的fixture函數,那么每次單個測試都會進行fixture的setup和teardown。 在任何一種情況下,測試模塊本身都不需要改變或了解fixture函數的這些細節。

請注意,我們還可以使用with語句無縫地使用yield語法:

# 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連接將關閉,因為當with語句結束時,smtp_connection對象會自動關閉。

請注意,如果在設置代碼期間(yield關鍵字之前)發生異常,則不會調用teardown代碼(在yield之后)。
執行teardown代碼的另一種選擇是利用請求上下文對象的addfinalizer方法來注冊teardown函數。
以下是smtp_connectionfixture函數更改為使用addfinalizer進行teardown:

# 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. 使用addfinalizer可以注冊多個teardown函數。
  2. 無論fixture中setup代碼是否引發異常,都將始終調用teardown代碼。 即使其中一個資源無法創建/獲取,也可以正確關閉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”因異常而失敗,則“C1”和“C3”仍將正確關閉。 當然,如果在注冊finalize函數之前發生異常,那么它將不會被執行。

Fixtures中使用測試上下文的內省信息

Fixture函數可以接受request對象來內省“請求”測試函數,類或模塊上下文。進一步擴展前一個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.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_connectionFixture方法函數從模塊命名空間拿起我們的郵件服務器名稱。

Fixture作為函數工廠(譯者注:Fixture返回一個函數,以支持根據參數得到不同的結果。)

“Fixture作為函數工廠”模式,可以支持在用例中根據不同的參數使用Fixture得到不同的結果。Fixture可以返回一個內部定義的函數,而不是直接返回數據,便可以在用例中使用該函數通過不同的參數獲取到不同的結果。

Fixtures工廠方法可根據需要提供參數生成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")

Fixtures參數化

可以對Fixture方法函數進行參數化,在這種情況下,它們將被多次調用,每次執行一組相關測試,即依賴於此Fixture方法的測試。測試函數通常不需要知道它們的重新運行。Fixture方法參數化有助於為可以以多種方式配置的組件編寫詳盡的函數測試。

擴展前面的示例,我們可以標記Fixture方法以創建兩個smtp_connectionFixture方法實例,這將導致使用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()

主要的變化是paramswith 的聲明@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方法,例如每個器材值測試ID test_ehlo[smtp.gmail.com]和 test_ehlo[mail.python.org]在上述實施例。這些ID可用於-k選擇要運行的特定案例,並且還可以在失敗時識別特定案例。運行pytest --collect-only將顯示生成的ID。

數字,字符串,布爾值和None將在測試ID中使用它們通常的字符串表示形式。對於其他對象,Pytest將根據參數名稱生成一個字符串。可以使用ids關鍵字參數自定義測試ID中用於特定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-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 =======================

使用參數化fixtures標記

pytest.param()可用於在參數化Fixture方法的值集中應用標記,其方式與@ 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

運行此測試將跳過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函數中使用fixtures函數

你不僅可以在測試函數中使用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

這里我們聲明一個appfixture,它接收先前定義的 smtp_connectionfixture並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服務器運行兩次。沒有必要為appFixture方法要意識到的smtp_connection 參數化,因為pytest將全面分析Fixture方法依賴關系圖。

請注意,appFixture方法具有范圍module並使用模塊范圍的smtp_connectionFixture方法。如果smtp_connection緩存在session范圍上,該示例仍然可以工作 :Fixture方法使用“更廣泛”的范圍Fixture方法,但不是相反的方式:會話范圍的Fixture方法不能以有意義的方式使用模塊范圍的Fixture方法。

使用fixture實例自動組織測試用例

pytest在測試運行期間最小化活動Fixture方法的數量。如果你有一個參數化Fixture方法,那么使用它的所有測試將首先用一個實例執行,然后在創建下一個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資源建立之前執行 。

特別注意test_0是完全獨立的並且首先完成。然后執行mod1test_1 mod1,然后執行test_2 ,然后執行test_1,mod2最后執行test_2 mod2。

該otherarg參數化資源(其函數范圍)是之前設置和使用它的每一個測試后撕開了下來。

在類/模塊/項目中使用fixtures

有時,測試函數不需要直接訪問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)

並通過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”函數參數一樣。讓我們運行它來驗證我們的Fixture方法是否已激活且測試通過:

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

你可以像這樣指定多個Fixture方法:

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

你可以使用標記機制的通用函數在測試模塊級別指定Fixture方法使用情況:

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

請注意,必須調用指定的變量pytestmark,分配例如 foomark不會激活Fixture方法。

也可以將項目中所有測試所需的Fixture方法放入ini文件中:

 content of pytest.ini
[pytest]
usefixtures = cleandir

警告
請注意,此標記對Fixture方法函數沒有影響。例如,這將無法按預期工作:

@pytest.mark.usefixtures("my_other_fixture")
@pytest.fixture
def my_fixture_that_sadly_wont_use_my_other_fixture():
...
目前,這不會產生任何錯誤或警告,但這應由#3664處理。

自動使用fixtures(xUnit 框架的setup固定方法)

有時,你可能希望自動調用fixture,而無需顯式聲明函數參數或使用usefixtures裝飾器。作為一個實際的例子,假設我們有一個數據庫fixture,它有一個開始/回滾/提交架構,我們希望通過事務和回滾自動包圍每個測試用例。以下是這個想法的虛擬自包含實現:

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

類級別的transactfixture用autouse = true標記, 這意味着類中的所有測試用例都將使用此fixture而無需在測試函數簽名中或使用類級usefixtures裝飾器進行陳述。

如果我們運行它,我們得到兩個通過測試:

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

以下是autouseFixture方法在其他范圍內的工作原理:

  • autouse fixtures服從scope=關鍵字參數:如果autouse fixture具有scope='session'它,它將只運行一次,無論它在何處定義。scope='class'意味着它將每班運行一次,等等。
  • 如果在測試模塊中定義了autouse fixture,則其所有測試函數都會自動使用它。
  • 如果在conftest.py文件中定義了autouse fixture,那么其目錄下所有測試模塊中的所有測試都將調用fixture。
  • 最后,請謹慎使用:如果你在插件中定義了autouse fixture,則會在安裝插件的所有項目中為所有測試調用它。如果Fixture方法僅在任何情況下在某些設置(例如ini文件中)的情況下工作,則這可能是有用的。這樣的全局Fixture方法應該總是快速確定它是否應該做任何工作並避免昂貴的進口或計算。

請注意,上述transactFixture方法很可能是你希望在項目中可用的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):
        ...

此TestClass中的所有測試用例都將使用事務Fixture方法,而模塊中的其他測試類或函數將不使用它,除非它們還添加transact引用。

不同級別的fixtures的覆蓋(優先級)

相對於在較大范圍的測試套件中的Test Fixtures方法,在較小范圍子套件你可能需要重寫和覆蓋外層的Test Fixtures方法,從而保持測試代碼的可讀性和可維護性。

在文件夾級別(通過conftest文件)重寫fixtures方法

假設用例目錄結構為:

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'

你可以看到,基礎/上級fixtures方法可以通過子文件夾下的conftest.py中同名的fixtures方法覆蓋,非常簡單,只需要按照上面的例子使用即可.

在測試模塊級別重寫fixtures方法

假設用例文件結構如下:

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方法會覆蓋文件夾conftest.py中同名的fixtures方法

在直接參數化方法中覆蓋fixtures方法

假設用例文件結構為:

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'

在上面的示例中,username fixture方法的結果值被參數化值覆蓋。 請注意,即使測試不直接使用(也未在函數原型中提及),也可以通過這種方式覆蓋fixture的值。

使用非參數化fixture方法覆蓋參數化fixtures方法,反之亦然

假設用例結構為:

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方法,以及使用參數化fixture覆蓋非參數化fixture以用於特定測試模塊。 這同樣適用於文件夾級別的fixtures方法


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM