前言
參考官方的這篇文章,我嘗試翻譯其中一些重點部分,並且拓展了相關的pluggy部分的知識。由於pytest是在pluggy基礎上構建的,強烈建議先閱讀一下pluggy的官方文檔,這樣理解起來更加容易一點。
正文
conftest.py可以作為最簡單的本地plugin調用一些hook函數,以此來做些強化功能。pytest整個框架通過調用如下定義良好的hooks來實現配置,收集,執行和報告這些過程:
- 內置plugins:從代碼內部的_pytest目錄加載;
- 外部插件(第三方插件):通過setuptools entry points機制發現的第三方插件模塊;
- conftest.py形式的本地插件:測試目錄下的自動模塊發現機制;
原則上,每個hook都是一個 1:N 的python函數調用, 這里的 N 是對一個給定hook的所有注冊調用數。所有的hook函數都使用pytest_xxx的命名規則,以便於查找並且同其他函數區分開來。
資源網站大全 https://55wd.com 我的007辦公資源網站 https://www.wode007.com
decorrator
pluggy里提供了兩個decorator helper類,分別是HookspecMarker和HookimplMarker,通過使用相同的project_name參數初始化得到對應的裝飾器,后續可以用這個裝飾器將函數標記為hookspec和hookimpl。
hookspec
hook specification (hookspec)用來validate每個hookimpl,保證hookimpl被正確的定義。
hookspec 通過 add_hookspecs()方法加載,一般在注冊hookimpl之前先加載;
hookimpl
hook implementation (hookimpl) 是一個被恰當標記過的回調函數。hookimpls 通過register()方法加載。
注:為了保證hookspecs在項目里可以不斷演化, hookspec里的參數對於hookimpls是可選的,即可以定義少於spec里定義數量的參數。
hookwrapper
hookimpl 里還有一個hookwrapper選項,用來表示這個函數是個hookwrapper函數。hookwrapper函數可以在普通的非wrapper的hookimpls執行的前后執行一些其他代碼, 類似於@contextlib.contextmanager,hookwrapper必須在它的主體包含單一的yield,用來實現生成器函數,例如:
import pytest @pytest.hookimpl(hookwrapper=True) def pytest_pyfunc_call(pyfuncitem): do_something_before_next_hook_executes() outcome = yield # outcome.excinfo may be None or a (cls, val, tb) tuple res = outcome.get_result() # will raise if outcome was exception post_process_result(res) outcome.force_result(new_res) # to override the return value to the plugin system
生成器發送一個 pluggy.callers._Result對象 , 這個對象可以在 yield表達式里指定並且通過 force_result()或者get_result() 方法重寫或者拿到最終結果。
注:hookwrapper不能返回結果 (跟所有的生成器函數一樣);
hookimpl的調用順序
默認情況下,hook的調用順序遵循注冊時的順序LIFO(后進先出),hookimpl允許通過tryfirst, trylast*選項調整這一項順序。
舉個例子,對於如下的代碼:
# Plugin 1 @pytest.hookimpl(tryfirst=True) def pytest_collection_modifyitems(items): # will execute as early as possible ... # Plugin 2 @pytest.hookimpl(trylast=True) def pytest_collection_modifyitems(items): # will execute as late as possible ... # Plugin 3 @pytest.hookimpl(hookwrapper=True) def pytest_collection_modifyitems(items): # will execute even before the tryfirst one above! outcome = yield # will execute after all non-hookwrappers executed
執行順序如下:
- Plugin3的pytest_collection_modifyitems先調用,直到yield點,因為這是一個hook warpper。
- Plugin1的pytest_collection_modifyitems被調用,因為有 tryfirst=True參數。
- Plugin2的pytest_collection_modifyitems被調用,因為有 trylast=True參數 (不過即使沒有這個參數也會排在tryfirst標記的plugin后面)。
- Plugin3的pytest_collection_modifyitems調用yield后面的代碼. yield接收非Wrapper的result返回. Wrapper函數不應該修改這個result。
當然也可以同時將 tryfirst 和 trylast與 hookwrapper=True 混用,這種情況下它將影響hookwrapper之間的調用順序.
hook執行結果處理和firstresult選項
默認情況下,調用一個hook會使底層的hookimpl函數在一個循環里按順序執行,並且將其非空的執行結果添加到一個list里面。例外的是,hookspec里有一個firstresult選項,如果指定這個選項為true,那么得到第一個返回非空的結果的hookimpl執行后就直接返回,后續的hookimpl將不在被執行,參考后面的例子。
注: hookwrapper還是正常的執行
hook的調用
每一個pluggy.PluginManager 都有一個hook屬性, 可以通過調用這個屬性的call函數來調用hook,需要注意的是,調用時必須使用關鍵字參數語法來調用。
請看下面這個firstresult和hook調用例子:
from pluggy import PluginManager, HookimplMarker, HookspecMarker hookspec = HookspecMarker("myproject") hookimpl = HookimplMarker("myproject") class MySpec1(object): @hookspec def myhook(self, arg1, arg2): pass class MySpec2(object): # 這里將firstresult設置為True @hookspec(firstresult=True) def myhook(self, arg1, arg2): pass class Plugin1(object): @hookimpl def myhook(self, arg1, arg2): """Default implementation. """ return 1 class Plugin2(object): # hookimpl可以定義少於hookspec里定義數量的參數,這里只定義arg1 @hookimpl def myhook(self, arg1): """Default implementation. """ return 2 class Plugin3(object): # 同上,甚至可以不定義hookspec里的參數 @hookimpl def myhook(self): """Default implementation. """ return 3 pm1 = PluginManager("myproject") pm2 = PluginManager("myproject") pm1.add_hookspecs(MySpec1) pm2.add_hookspecs(MySpec2) pm1.register(Plugin1()) pm1.register(Plugin2()) pm1.register(Plugin3()) pm2.register(Plugin1()) pm2.register(Plugin2()) pm2.register(Plugin3()) # hook調用必須使用關鍵字參數的語法 print(pm1.hook.myhook(arg1=None, arg2=None)) print(pm2.hook.myhook(arg1=None, arg2=None))
得到結果如下:
[3, 2, 1] 3
可以看到,由於pm2里的hookspec里有firstresult參數,在得到3這個非空結果時就直接返回了。