上一節講到如何安裝和使用第三方外掛,用法很簡單。接下來解讀下如何自己開發pytest
外掛。
但是,由於一個外掛包含一個或多個勾點函數開發而來,所以在具體開發外掛之前還需要先學習hooks函數。
簡單來說,在 pytest 的程式碼中,預留出了一些函數供我們修改,以便來改變pytest工作方式,這些函數就是hooks函數
,我們可以直接重寫函數裡的內容。
比如,在 pytest程式碼路徑\Lib\site-packages\_pytest\hookspec.py
中,可以看到 pytest 定義好的 hook 規範,方便我們在開發外掛的時候參考規範來呼叫對應的hooks
函數。
從hooks
函數的職責分類來看,大概如下幾類:
pytest.Item
物件。可供呼叫的勾點函數有很多,功能也是各式各樣的,有興趣的童鞋可以進一步細看官方檔案裡的介紹。我們就是要通過不同勾點函數具備的功能,來實現我們自定義的需求。
寫一個外掛範例。
比如我們平時執行case的時候,一通跑完可能會出現不少失敗的case,那通常我可能就會翻控制檯的輸出來找出哪些case失敗了。
但是控制檯裡輸出的資訊有很多,於是乎我想直接把測試失敗的case資訊存到一個本地檔案裡,我直接開啟就可以看到所有失敗的case。
先寫一個case檔案裡的建議測試用例:
# content of mytest/tests.py
def test_failed():
assert False
def test_passed():
assert True
def test_failed2():
assert False
然後再同級目錄下建立一個conftest
檔案,之前聊fixture時候就說過,conftest裡的內容就是本地外掛了。
先直接放上外掛程式碼:
# content of mytest/conftest.py
import pytest
from pathlib import Path
from _pytest.main import Session
from _pytest.nodes import Item
from _pytest.runner import CallInfo
from _pytest.terminal import TerminalReporter
FAILURES_FILE = Path() / "failures.txt"
@pytest.hookimpl()
def pytest_sessionstart(session: Session):
print("Hello 把蘋果咬哭")
if FAILURES_FILE.exists():
FAILURES_FILE.unlink()
FAILURES_FILE.touch()
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item: Item, call: CallInfo):
outcome = yield
result = outcome.get_result()
if result.when == "call" and result.failed:
try:
with open(str(FAILURES_FILE), "a") as f:
f.write(result.nodeid + "\n")
except Exception as e:
print("ERROR", e)
pass
解析
首先,關於pathlib
模組就是用來做一些路徑操作的庫,因為我要在本地路徑中進行檔案相關操作。
def pytest_sessionstart()
中做的事情就是先看下本地是否存在這個名字叫failures.txt
的檔案,有的話就刪除,沒有就新建。
為啥用pytest_sessionstart
這個hook函數,因為通過檢視官方API檔案裡的介紹,發現這個勾點函數是在建立Session物件之後,且在執行收集和進入執行測試迴圈之前呼叫,所以很適合用在這裡。
所以直接重寫這個hook函數來實現我們定義的功能。
範例中使用hook函數pytest_runtest_makereport
,同樣通過檢視官方API介紹,它的作用是為測試用例的每個setup
、執行
和tearDown
階段建立TestReport
。而外掛要做的事情,就是要在
用例執行後獲取到狀態,若是失敗就存放到本地txt
檔案。
當檢視hook規範
時候,發現一個裝飾器引數firstresult=True
。
由於在大多數情況下,呼叫hook函數可能還會觸發呼叫多個hook,所以最後的結果會是包含所呼叫勾點函數的非none結果
。
當firstresult=True
時,呼叫勾點函數時只要有第一個返回非none結果,就會將該結果作為整個勾點呼叫的結果。在這種情況下,將不會呼叫其餘勾點函數。
回到外掛程式碼本身,也用到了一個引數hookwrapper=True
。
預設情況下,我們之間重寫hook函數來徹底改變它要做的事情,就像外掛程式碼裡第一個hook函數pytest_sessionstart
一樣。
當hookwrapper=True
時,等於是我們實現了一個hook函數的包裝器。勾點包裝器是一個生成器函數,它只產生一次。
當 pytest 呼叫勾點時,首先執行勾點包裝器,並像常規勾點一樣傳遞相同的引數。
yield
關鍵字大家都熟悉了,當程式碼執行到這裡的時候會暫停一下,繼續執行下一個勾點,並且會把所有的結果或者異常封裝成一個result
物件返回到yield
這裡。
勾點包裝器本身並不返回結果,只是在實際的勾點實現的外面做一些其他的事情。
我們的外掛功能其實也並不是要修改這個勾點本身測試報告的內容,所以就直接通過hookwrapper=True
將我們的pytest_runtest_makereport
寫成一個包裝好的勾點。
接下來就是具體功能的程式碼,判斷當用例測試結果是fail
,就寫到本地檔案中。
執行
執行一下測試用例,看下我們外掛的執行情況。
檢視下failures.txt
內容,結果正確。
存在這樣的情況,對於同一個勾點規範,可能會存在多個實現。這種情況下可以使用引數tryfirst
和trylast
來影響勾點的呼叫順序。
# Plugin 1
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items):
# 儘可能早的執行
...
# Plugin 2
@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(items):
# 儘可能晚的執行
...
# Plugin 3
@pytest.hookimpl(hookwrapper=True)
def pytest_collection_modifyitems(items):
# 會在上面的 tryfirst 之前執行
outcome = yield
# 在執行所有非勾點包裝器之後執行
具體執行順序如下:
Plugin3
的pytest_collection_modifyitems
一直呼叫到yield
,因為它是一個勾點包裝器。Plugin1
的pytest_collection_modifyitems
被呼叫,因為它被標記為tryfirst=True
。Plugin2
的pytest_collection_modifyitems
被呼叫,因為它被標記為trylast=True
(但即使沒有這個標記,它也會在Plugin1之後)。Plugin3
的pytest_collection_modifyitems
繼續在yield
執行程式碼,yield
接收一個Result
範例。關於hook本篇先到此,剩下的內容另起篇幅了。
最後,聞道有先後,文章有遺漏,歡迎交流。