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

目錄:

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

Pytest fixtures:清晰 模塊化 易擴展

2.0/2.3/2.4版本新功能
text fixtures的目的是為測試的重復執(zhí)行提供一個可靠的固定基線。 pytest fixture比經(jīng)典的xUnit setUp/tearDown方法有著顯著的改進:

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

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

Fixtures作為函數(shù)參數(shù)使用

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

# ./test_smtpsimple.py內(nèi)容
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將發(fā)現(xiàn)并調(diào)用帶@pytest.fixture裝飾器的smtp_connection fixture函數(shù)。 運行測試如下所示:

$ 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參數(shù)調(diào)用的,即由fixture函數(shù)創(chuàng)建的smtplib.SMTP()實例。測試用例在我們故意的assert 0上失敗。以下是pytest用這種方式調(diào)用測試方法使用的確切協(xié)議:

Fixtures: 依賴注入的主要例子

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

conftest.py: 共享fixture函數(shù)

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

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

共享測試數(shù)據(jù)

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

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

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

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

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

# conftest.py文件內(nèi)容
import pytest
import smtplib

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

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

# test_module.py文件內(nèi)容

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語句,以便檢查發(fā)生了什么,運行測試并查看結(jié)果:

$ 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在回溯信息中顯示傳入的參數(shù)值。 因此,使用smtp_connection的兩個測試方法運行速度與單個函數(shù)一樣快,因為它們重用了相同的fixture對象。

如果您決定要使用session(會話,一次運行算一次會話)范圍的smtp_connection對象,則只需如下聲明:

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

最后,class(類)范圍將為每個測試類調(diào)用一次fixture對象。

注意:
Pytest一次只會緩存一個fixture實例。 這意味著當使用參數(shù)化fixture時,pytest可能會在給定范圍內(nèi)多次調(diào)用fixture函數(shù)。

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

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

謹慎使用此新功能,請務(wù)必報告您發(fā)現(xiàn)的任何問題。

高范圍的fixture函數(shù)優(yōu)先實例化

3.5版本新功能
在測試函數(shù)的fixture對象請求中,較高范圍的fixture(例如session會話級)較低范圍的fixture(例如function函數(shù)級或class類級優(yōu)先執(zhí)行。相同范圍的fixture對象的按引入的順序及fixtures之間的依賴關(guān)系按順序調(diào)用。

請考慮以下代碼:

@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將按以下順序執(zhí)行:

  1. s1:是最高范圍的fixture(會話級)
  2. m1:是第二高的fixture(模塊級)
  3. tmpdir:是一個函數(shù)級的fixture,f1依賴它,因此它需要在f1前調(diào)用
  4. f1:是test_foo參數(shù)列表中第一個函數(shù)范圍的fixture。
  5. f2:是test_foo參數(shù)列表中最后一個函數(shù)范圍的fixture。

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

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

# conftest.py文件內(nèi)容

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

無論測試的異常狀態(tài)如何,printsmtp.close()語句將在模塊中的最后一個測試完成執(zhí)行時執(zhí)行。

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

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

2 failed in 0.12 seconds

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

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

# test_yield2.py文件內(nèi)容

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

測試結(jié)束后, smtp_connection連接將關(guān)閉,因為當with語句結(jié)束時,smtp_connection對象會自動關(guān)閉。

請注意,如果在設(shè)置代碼期間(yield關(guān)鍵字之前)發(fā)生異常,則不會調(diào)用teardown代碼(在yield之后)。
執(zhí)行teardown代碼的另一種選擇是利用請求上下文對象的addfinalizer方法來注冊teardown函數(shù)。
以下是smtp_connectionfixture函數(shù)更改為使用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方法在測試結(jié)束后調(diào)用它們的代碼時的工作方式類似,但addfinalizer相比yield有兩個主要區(qū)別:

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

Fixtures中使用測試上下文的內(nèi)省信息

Fixtures工廠方法

Fixtures參數(shù)化

使用參數(shù)化fixtures標記

模塊化:在fixture函數(shù)中使用fixtures功能

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

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

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

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

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

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

假設(shè)用例目錄結(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'

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

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

假設(shè)用例文件結(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方法會覆蓋文件夾conftest.py中同名的fixtures方法

在直接參數(shù)化方法中覆蓋fixtures方法

假設(shè)用例文件結(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'

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

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

假設(shè)用例結(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ù)化fixture方法,以及使用參數(shù)化fixture覆蓋非參數(shù)化fixture以用于特定測試模塊。 這同樣適用于文件夾級別的fixtures方法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內(nèi)容