Pytest官方教程-05-Pytest fixtures:清晰 模塊化 易擴展

目錄:

  1. 安裝及入門
  2. 使用和調用方法
  3. 原有TestSuite使用方法
  4. 斷言的編寫和報告
  5. Pytest fixtures:清晰 模塊化 易擴展
  6. 使用Marks標記測試用例
  7. Monkeypatching/對模塊和環境進行Mock
  8. 使用tmp目錄和文件
  9. 捕獲stdout及stderr輸出
  10. 捕獲警告信息
  11. 模塊及測試文件中集成doctest測試
  12. skip及xfail: 處理不能成功的測試用例
  13. Fixture方法及測試用例的參數化
  14. 緩存: 使用跨執行狀態
  15. unittest.TestCase支持
  16. 運行Nose用例
  17. 經典xUnit風格的setup/teardown
  18. 安裝和使用插件
  19. 插件編寫
  20. 編寫鉤子(hook)方法
  21. 運行日志
  22. API參考
    1. 方法(Functions)
    2. 標記(Marks)
    3. 鉤子(Hooks)
    4. 裝置(Fixtures)
    5. 對象(Objects)
    6. 特殊變量(Special Variables)
    7. 環境變量(Environment Variables)
    8. 配置選項(Configuration Options)
  23. 優質集成實踐
  24. 片狀測試
  25. Pytest導入機制及sys.path/PYTHONPATH
  26. 配置選項
  27. 示例及自定義技巧
  28. Bash自動補全設置

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中使用測試上下文的內省信息

Fixtures工廠方法

Fixtures參數化

使用參數化fixtures標記

模塊化:在fixture函數中使用fixtures功能

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

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

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

不同級別的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方法可以通過子文件夾下的con
ftest.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方法。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容