【pytest官方文檔】解讀- 插件開發之hooks 函數(鉤子)

上一節講到如何安裝和使用第三方插件,用法很簡單。接下來解讀下如何自己開發pytest插件。

但是,由於一個插件包含一個或多個鉤子函數開發而來,所以在具體開發插件之前還需要先學習hooks函數

一、什麼是 hooks 函數

簡單來說,在 pytest 的代碼中,預留出了一些函數供我們修改,以便來改變pytest工作方式,這些函數就是hooks函數,我們可以直接重寫函數裏的內容。

比如,在 pytest代碼路徑\Lib\site-packages\_pytest\hookspec.py中,可以看到 pytest 定義好的 hook 規範,方便我們在開發插件的時候參考規範來調用對應的hooks函數。

二、hooks 函數的分類

hooks函數的職責分類來看,大概如下幾類:

  • Bootstrapping hooks:引導類鉤子,用來調用已經早就註冊好的內部插件第三方插件
  • Collection hooks:集合類鉤子,pytest 調用集合鉤子來收集文件和目錄。
  • Test running (runtest) hooks:測試運行相關的鉤子,所有與測試運行相關的鉤子都接收一個pytest.Item對象。
  • Reporting hooks:與Session 會話相關的鉤子。
  • Debugging/Interaction hooks:調試/交互鉤子,少有的可以用於特殊的報告或與異常交互的鉤子函數。

可供調用的鉤子函數有很多,功能也是各式各樣的,有興趣的童鞋可以進一步細看官方文檔裏的介紹。我們就是要通過不同鉤子函數具備的功能,來實現我們自定義的需求。

三、編寫 hooks 函數開發本地插件

寫一個插件示例。

比如我們平時執行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

解析

1. 重寫鉤子函數

首先,關於pathlib模塊就是用來做一些路徑操作的庫,因爲我要在本地路徑中進行文件相關操作。

def pytest_sessionstart()中做的事情就是先看下本地是否存在這個名字叫failures.txt的文件,有的話就刪除,沒有就新建。

爲啥用pytest_sessionstart這個hook函數,因爲通過查看官方API文檔裏的介紹,發現這個鉤子函數是在創建Session對象之後,且在執行收集和進入運行測試循環之前調用,所以很適合用在這裏。

所以直接重寫這個hook函數來實現我們定義的功能。

2. hook函數中的 firstresult

示例中使用hook函數pytest_runtest_makereport,同樣通過查看官方API介紹,它的作用是爲測試用例的每個setup運行tearDown階段創建TestReport。而插件要做的事情,就是要在
用例執行後獲取到狀態,若是失敗就存放到本地txt文件。

當查看hook規範時候,發現一個裝飾器參數firstresult=True

由於在大多數情況下,調用hook函數可能還會觸發調用多個hook,所以最後的結果會是包含所調用鉤子函數的非none結果

firstresult=True時,調用鉤子函數時只要有第一個返回非none結果,就會將該結果作爲整個鉤子調用的結果。在這種情況下,將不會調用其餘鉤子函數。

3. hook函數中的 hookwrapper

回到插件代碼本身,也用到了一個參數hookwrapper=True

默認情況下,我們之間重寫hook函數來徹底改變它要做的事情,就像插件代碼裏第一個hook函數pytest_sessionstart一樣。

hookwrapper=True時,等於是我們實現了一個hook函數的包裝器。鉤子包裝器是一個生成器函數,它只產生一次。

當 pytest 調用鉤子時,首先執行鉤子包裝器,並像常規鉤子一樣傳遞相同的參數。

yield關鍵字大家都熟悉了,當代碼執行到這裏的時候會暫停一下,繼續執行下一個鉤子,並且會把所有的結果或者異常封裝成一個result對象返回到yield這裏。

鉤子包裝器本身並不返回結果,只是在實際的鉤子實現的外面做一些其他的事情。

我們的插件功能其實也並不是要修改這個鉤子本身測試報告的內容,所以就直接通過hookwrapper=True將我們的pytest_runtest_makereport寫成一個包裝好的鉤子。

接下來就是具體功能的代碼,判斷當用例測試結果是fail,就寫到本地文件中。

運行
運行一下測試用例,看下我們插件的執行情況。

查看下failures.txt內容,結果正確。

四、鉤子函數排序/調用示例

存在這樣的情況,對於同一個鉤子規範,可能會存在多個實現。這種情況下可以使用參數tryfirsttrylast來影響鉤子的調用順序。

# 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
    # 在執行所有非鉤子包裝器之後執行

具體執行順序如下:

  1. Plugin3pytest_collection_modifyitems一直調用到yield,因爲它是一個鉤子包裝器。
  2. Plugin1pytest_collection_modifyitems被調用,因爲它被標記爲tryfirst=True
  3. Plugin2pytest_collection_modifyitems被調用,因爲它被標記爲trylast=True(但即使沒有這個標記,它也會在Plugin1之後)。
  4. Plugin3pytest_collection_modifyitems繼續在yield執行代碼,yield接收一個Result實例。

關於hook本篇先到此,剩下的內容另起篇幅了。

最後,聞道有先後,文章有遺漏,歡迎交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章