pytest(7)-yield與終結函式

給你一頁白紙發表於2022-02-16

通過上一篇文章,我們已經知道了pytest中,可以使用Fixture來完成執行測試用例之前的一些操作如連線資料庫,以及測試執行之後自動去做一些善後工作如清空髒資料、關閉資料庫連線等。

我們已經學會了fixture函式的簡單用法,但其實fixture還提供了兩種非常優雅高效的寫法,來完成測試執行前的處理操作與執行後的處理操作,即使用yieldaddfinalizer來實現。

yield

在fixture中的關鍵字yield主要有兩個作用:

  • yield代替return進行引數的傳遞
  • 起到程式碼的分割作用,yield之前的程式碼為setup的作用,yield之後的程式碼為teardown的作用

yield 與 return

在 pytest 的fixture函式中可以使用yield代替return進行返回,示例如下:

import pytest

@pytest.fixture(autouse=True)
def fixture_one():
    print("執行fixture_one")
    yield 1
    
def test_e(fixture_one):
    print("執行test_e")
    print(fixture_one)
    assert fixture_one == 1


if __name__ == '__main__':
    pytest.main(["-s"])

執行結果如下:

test_case_4.py::test_e 
執行fixture_one
PASSED                                            [100%]執行test_e
1


============================== 1 passed in 0.12s ==============================

從執行結果我們能看到fixture_one會返回1並傳遞給test_e,與return的作用完全一致。但如果僅僅只是這樣使用的話,毫無意義,因為使用return足夠了。所以,在實際的使用過程中我們一般會在yield後面加上teardown的程式碼。

yield 與 teardown

yield不進行引數傳遞

對於不需要在前置操作中返回資料的 fixture 函式,加入yield,那麼yield之前的程式碼為用例執行之前的操作(即setup),yield之後的程式碼為用例執行之後的操作(即teardown)。示例如下:

import pytest

@pytest.fixture()
def fixture_demo():
    # setup
    print("\n連線資料庫")
    yield
    # teardown
    print("清空髒資料")

def test_case(fixture_demo):
    print("執行test_case")
    assert True


if __name__ == '__main__':
    pytest.main(["-s"])

執行結果如下:

從結果中我們可以看出來,先執行了setup部分,再執行測試用例,最後執行teardown部分。

yield進行引數傳遞

yield可以將引數傳遞給測試用例。

假設有這樣一個場景,需要用到介面1的返回引數作為介面2的請求引數,即介面2依賴介面1,我們需要寫一條測試用例對介面2進行測試,這個時候可以將介面1的請求寫在前置中,如果是unittest框架則程式碼如下:

import unittest
import requests

class TestDemo(unittest.TestCase):

    def setup(self):
        print("請求介面1")
        self.res_1 = requests.get(url=url_1, params=params_1)

    def test_api_2(self):
        print("驗證介面2")
        # 將介面1的返回值self.res_1作為請求引數,請求介面2
        res = requests.post(url=url_2, data=self.res_1)
        # 斷言
        self.assertEqual(res, "介面2預期的返回結果")

    def teardown(self):
        print("清空髒資料")

pytest框架中使用fixture+yield則可編寫如下:

@pytest.fixture()
def get_api_1_result():
    # setup
    res_1 = requests.get(url=url_1, params=params_1)
    yield res_1
    # teardown
    print("清空髒資料")
    

def test_api_2(get_api_1_result):
    print("驗證介面2")
    # 將介面1的返回值res_1作為請求引數,請求介面2
    res = requests.post(url=url_2, data=get_api_1_result)
    # 斷言
    assert res == "介面2預期的返回結果"

其中,fixture 會先通過yield返回res_1,並傳入測試用例test_api_2中,test_api_2執行完成後再去執行yield後面的程式碼,即執行print("清空髒資料")

通過以上對比unittestsetupteardown以及引數的傳遞,我們就能很直觀的看出pytestyield的使用方式,此處程式碼僅為示例。

yield 的執行順序

有時候我們會遇到一個fixture函式呼叫另一個或多個fixture函式,且這些函式中可能含有yield,我們先看示例,程式碼如下:

import pytest

@pytest.fixture
def fixture_1():
    print("\n執行fixture_1")
    yield 1
    print("\n執行fixture_1的teardown程式碼")

@pytest.fixture
def fixture_2(fixture_1):
    print("\n執行fixture_2")
    yield 2
    print("\n執行fixture_2的teardown程式碼")

@pytest.fixture
def fixture_add(fixture_1, fixture_2):
    print("\n執行fixture_add")
    result = fixture_1 + fixture_2
    yield result
    print("\n執行fixture_add的teardown程式碼")

    
def test_demo(fixture_add):
    print("\n執行測試函式test_demo")
    assert fixture_add == 3


if __name__ == '__main__':
    pytest.main(["-s"])

執行結果如下:

rootdir: E:\blog\python介面自動化\apiAutoTest, configfile: pytest.ini
plugins: html-2.1.1, metadata-1.10.0, ordering-0.6, rerunfailures-9.1.1
collecting ... collected 1 item

test_case_4.py::test_demo 
執行fixture_1

執行fixture_2

執行fixture_add
PASSED                                         [100%]
執行測試函式test_demo

執行fixture_add的teardown程式碼

執行fixture_2的teardown程式碼

執行fixture_1的teardown程式碼


============================== 1 passed in 0.12s ==============================

從結果可以看出:

test_demo 測試函式執行之前:先執行了 fixture_1,再執行fixture_2,最後執行fixture_add,注意此時都是執行yield之前的的程式碼;

test_demo 測試函式執行之後:先執行了 fixture_add,再執行fixture_2,最後執行fixture_1,注意此時都是執行yield之後的的程式碼。

因此,當一個fixture函式呼叫另一個或多個fixture函式,且fixture函式中含有yield時,被測試函式呼叫時有如下執行順序:

  • 測試函式執行之前,pytest會根據fixture函式之間的線性關係順序呼叫,即依次執行yield之前的程式碼

  • 而測試函式執行結束後,pytest會根據之前的順序反方向執行fixture函式中yield之後的程式碼

finalizer

finalizer即終結器 (終結函式),與unittest中的teardown作用一樣,測試用例執行完成後再執行終結器程式碼。

在pytest中,fixture除了使用 yield 進行 teardown 之外,還可以使用request.addfinalizer()定義finalizer來進行後置操作。

使用addfinalizer,需要在定義 fixture 函式時傳入request,並以內嵌函式的形式進行定義。終結函式可以定義一個或多個。

定義單個終結函式

示例如下:

import pytest

@pytest.fixture
def fixture_demo(request):
    print("\nsetup:每個case開始前執行一次")

    # 定義終結函式
    def finalizer_demo():
        print("\nteardown:每個case完成後執行一次")

    # 將finalizer_demo註冊為終結函式
    request.addfinalizer(finalizer_demo)


def test_2(fixture_demo):
    print("\n執行test_2")

def test_1(fixture_demo):
    print("\n執行test_1")


if __name__ == '__main__':
    pytest.main()

執行結果如下:

rootdir: E:\blog\python介面自動化\apiAutoTest, configfile: pytest.ini
plugins: html-2.1.1, metadata-1.10.0, ordering-0.6, rerunfailures-9.1.1
collecting ... collected 2 items

test_module_02\test_case_3.py::test_2 
setup:每個case開始前執行一次
PASSED                             [ 50%]
執行test_2

teardown:每個case完成後執行一次

test_module_02\test_case_3.py::test_1 
setup:每個case開始前執行一次
PASSED                             [100%]
執行test_1

teardown:每個case完成後執行一次


============================== 2 passed in 0.03s ==============================

從結果可以看出來,在測試用例執行完後會執行addfinalizer函式,效果與執行yield後的程式碼一致。

定義多個終結函式

示例如下:

import pytest

@pytest.fixture
def fixture_demo(request):
    print("\nsetup:每個case開始前執行一次")

    # 定義終結函式
    def finalizer_demo_1():
        print("\nteardown1:每個case完成後執行一次")

    def finalizer_demo_2():
        print("\nteardown2:每個case完成後執行一次")

    # 註冊為終結函式
    request.addfinalizer(finalizer_demo_1)
    request.addfinalizer(finalizer_demo_2)


def test_2(fixture_demo):
    print("\n執行test_2")

def test_1(fixture_demo):
    print("\n執行test_1")


if __name__ == '__main__':
    pytest.main()

執行結果如下:

rootdir: E:\blog\python介面自動化\apiAutoTest, configfile: pytest.ini
plugins: html-2.1.1, metadata-1.10.0, ordering-0.6, rerunfailures-9.1.1
collecting ... collected 2 items

test_module_02\test_case_3.py::test_2 
setup:每個case開始前執行一次
PASSED                             [ 50%]
執行test_2

teardown2:每個case完成後執行一次

teardown1:每個case完成後執行一次

test_module_02\test_case_3.py::test_1 
setup:每個case開始前執行一次
PASSED                             [100%]
執行test_1

teardown2:每個case完成後執行一次

teardown1:每個case完成後執行一次


============================== 2 passed in 0.02s ==============================

從結果可以看出,上面示例中測試函式執行完成後,先執行了finalizer_demo_2,後執行finalizer_demo_1

所以, 當有多個終結函式被執行時,執行順序與註冊順序是相反的

總結

實際專案中,可以視情況進行選擇,但一般情況下,推薦使用yield,因為這樣程式碼更加簡潔高效,且閱讀性更強更容易維護。

相關文章