通過上一篇文章,我們已經知道了pytest
中,可以使用Fixture
來完成執行測試用例之前的一些操作如連線資料庫,以及測試執行之後自動去做一些善後工作如清空髒資料、關閉資料庫連線等。
我們已經學會了fixture
函式的簡單用法,但其實fixture
還提供了兩種非常優雅高效的寫法,來完成測試執行前的處理操作與執行後的處理操作,即使用yield
或addfinalizer
來實現。
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("清空髒資料")
。
通過以上對比unittest
中setup
、teardown
以及引數的傳遞,我們就能很直觀的看出pytest
中yield
的使用方式,此處程式碼僅為示例。
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
,因為這樣程式碼更加簡潔高效,且閱讀性更強更容易維護。