pytest架構是什么?
首先,來看一個 pytest 的例子:
def test_a():
print(123)
collected 1 item
test_a.py . [100%]
============ 1 passed in 0.02s =======================
輸出結果很簡單:收集到 1 個用例,并且這條測驗用例執行通過,
此時思考兩個問題:
1.pytest 如何收集到用例的?
2.pytest 如何把 python 代碼,轉換成 pytest 測驗用例(又稱 item) ?
pytest如何做到收集到用例的?
這個很簡單,遍歷執行目錄,如果發現目錄的模塊中存在符合“ pytest 測驗用例要求的 python 物件”,就將之轉換為 pytest 測驗用例,
比如撰寫以下 hook 函式:
def pytest_collect_file(path, parent):
print("hello", path)
hello C:\Users\yuruo\Desktop\tmp\tmp123\tmp\testcase\__init__.py
hello C:\Users\yuruo\Desktop\tmp\tmp123\tmp\testcase\conftest.py
hello C:\Users\yuruo\Desktop\tmp\tmp123\tmp\testcase\test_a.py
會看到所有檔案內容,
pytest 像是包裝盒,將 python 物件包裹起來,比如下圖:

當寫好 python 代碼時:
def test_a:
print(123)
會被包裹成 Function :
<Function test_a>
可以從 hook 函式中查看細節:
def pytest_collection_modifyitems(session, config, items):
pass
1080×441 72.6 KB
于是,理解包裹程序就是解開迷題的關鍵,pytest 是如何包裹 python 物件的?
下面代碼只有兩行,看似簡單,但暗藏玄機!
def test_a:
print(123)
把代碼位置截個圖,如下:
我們可以說,上述代碼是處于“testcase包”下的 “test_a.py模塊”的“test_a函式”, pytest 生成的測驗用例也要有這些資訊:
處于“testcase包”下的 “test_a.py模塊”的“test_a測驗用例:
把上述表達轉換成下圖:
pytest 使用 parent 屬性表示上圖層級關系,比如 Module 是 Function 的上級, Function 的 parent 屬性如下:
<Function test_a>:
parent: <Module test_parse.py>
當然 Module 的 parent 就是 Package:
<Module test_parse.py>:
parent: <Package tests>
這里科普一下,python 的 package 和 module 都是真實存在的物件,你可以從 obj 屬性中看到,比如 Module 的 obj 屬性如下:
1080×153 68.4 KB
如果理解了 pytest 的包裹用途,非常好!我們進行下一步討論:如何構造 pytest 的 item ?
以下面代碼為例:
def test_a:
print(123)
構造 pytest 的 item ,需要:
3.構建 Package
4.構建 Module
5.構建 Function
以構建 Function 為例,需要呼叫其from_parent()方法進行構建,其程序如下圖:
,就可以猜測出,“構建 Function”一定與其 parent 有不小聯系!又因為 Function 的 parent 是 Module :
根據下面 Function 的部分代碼(位于 python.py 檔案):
class Function(PyobjMixin, nodes.Item):
# 用于創建測驗用例
@classmethod
def from_parent(cls, parent, **kw):
"""The public constructor."""
return super().from_parent(parent=parent, **kw)
# 獲取實體
def _getobj(self):
assert self.parent is not None
return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined]
# 運行測驗用例
def runtest(self) -> None:
"""Execute the underlying test function."""
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
得出結論,可以利用 Module 構建 Function!其呼叫偽代碼如下:
Function.from_parent(Module)
既然可以利用 Module 構建 Function, 那如何構建 Module ?
當然是利用 Package 構建 Module!
Module.from_parent(Package)
既然可以利用 Package 構建 Module 那如何構建 Package ?
別問了,快成套娃了,請看下圖呼叫關系:
1080×157 33.3 KB
pytest 從 Config 開始,層層構建,直到 Function !Function 是 pytest 的最小執行單元,
手動構建 item 就是模擬 pytest 構建 Function 的程序,也就是說,需要創建 Config ,然后利用 Config 創建 Session ,然后利用 Session 創建 Package ,…,最后創建 Function,

其實沒這么復雜, pytest 會自動創建好 Config, Session和 Package ,這三者不用手動創建,

比如撰寫以下 hook 代碼,打斷點查看其 parent 引數:
def pytest_collect_file(path, parent):
pass
1080×387 112 KB
如果遍歷的路徑是某個包(可從path引數中查看具體路徑),比如下圖的包:
其 parent 引數就是 Package ,此時可以利用這個 Package 創建 Module :
撰寫如下代碼即可構建 pytest 的 Module ,如果發現是 yaml 檔案,就根據 yaml 檔案內容動態創建 Module 和 module :
from _pytest.python import Module, Package
def pytest_collect_file(path, parent):
if path.ext == ".yaml":
pytest_module = Module.from_parent(parent, fspath=path)
# 回傳自已定義的 python module
pytest_module._getobj = lambda : MyModule
return pytest_module
需要注意,上面代碼利用猴子補丁改寫了 _getobj 方法,為什么這么做?
Module 利用 _getobj 方法尋找并匯入(import陳述句) path 包下的 module ,其原始碼如下:
# _pytest/python.py Module
class Module(nodes.File, PyCollector):
def _getobj(self):
return self._importtestmodule()
def _importtestmodule(self):
# We assume we are only called once per module.
importmode = self.config.getoption("--import-mode")
try:
# 關鍵代碼:從路徑匯入 module
mod = import_path(self.fspath, mode=importmode)
except SyntaxError as e:
raise self.CollectError(
ExceptionInfo.from_current().getrepr(style="short")
) from e
# 省略部分代碼...
但是,如果使用資料驅動,即用戶創建的資料檔案 test_parse.yaml ,它不是 .py 檔案,不會被 python 識別成 module (只有 .py 檔案才能被識別成 module),
這時,就不能讓 pytest 匯入(import陳述句) test_parse.yaml ,需要動態改寫 _getobj ,回傳自定義的 module !
因此,可以借助 lambda 運算式回傳自定義的 module :
lambda : MyModule
這就涉及元編程技術:動態構建 python 的 module ,并向 module 中動態加入類或者函式:
import types
# 動態創建 module
module = types.ModuleType(name)
def function_template(*args, **kwargs):
print(123)
# 向 module 中加入函式
setattr(module, "test_abc", function_template)
綜上,將自己定義的 module 放入 pytest 的 Module 中即可生成 item :
# conftest.py
import types
from _pytest.python import Module
def pytest_collect_file(path, parent):
if path.ext == ".yaml":
pytest_module = Module.from_parent(parent, fspath=path)
# 動態創建 module
module = types.ModuleType(path.purebasename)
def function_template(*args, **kwargs):
print(123)
# 向 module 中加入函式
setattr(module, "test_abc", function_template)
pytest_module._getobj = lambda: module
return pytest_module
創建一個 yaml 檔案,使用 pytest 運行:
849×99 3.93 KB
======= test session starts ====
platform win32 -- Python 3.8.1, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: C:\Users\yuruo\Desktop\tmp
plugins: allure-pytest-2.8.11, forked-1.3.0, rerunfailures-9.1.1, timeout-1.4.2, xdist-2.2.1
collected 1 item
test_a.yaml 123
.
======= 1 passed in 0.02s =====
PS C:\Users\yuruo\Desktop\tmp>
現在停下來,回顧一下,我們做了什么?
借用 pytest hook ,將 .yaml 檔案轉換成 python module,
作為一個資料驅動測驗框架,我們沒做什么?
沒有決議 yaml 檔案內容!上述生成的 module ,其內的函式如下:
def function_template(*args, **kwargs):
print(123)
只是簡單列印 123 ,資料驅動測驗框架需要決議 yaml 內容,根據內容動態生成函式或類,比如下面 yaml 內容:
test_abc:
- print: 123
表達的含義是“定義函式 test_abc,該函式列印 123”,
可以利用 yaml.safe_load 加載 yaml 內容,并進行關鍵字決議,其中path.strpath代表 yaml 檔案的地址:
import types
import yaml
from _pytest.python import Module
def pytest_collect_file(path, parent):
if path.ext == ".yaml":
pytest_module = Module.from_parent(parent, fspath=path)
# 動態創建 module
module = types.ModuleType(path.purebasename)
# 決議 yaml 內容
with open(path.strpath) as f:
yam_content = yaml.safe_load(f)
for function_name, steps in yam_content.items():
def function_template(*args, **kwargs):
"""
函式模塊
"""
# 遍歷多個測驗步驟 [print: 123, print: 456]
for step_dic in steps:
# 決議一個測驗步驟 print: 123
for step_key, step_value in step_dic.items():
if step_key == "print":
print(step_value)
# 向 module 中加入函式
setattr(module, function_name, function_template)
pytest_module._getobj = lambda: module
return pytest_module
上述測驗用例運行結果如下:
=== test session starts ===
platform win32 -- Python 3.8.1, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: C:\Users\yuruo\Desktop\tmp
plugins: allure-pytest-2.8.11, forked-1.3.0, rerunfailures-9.1.1, timeout-1.4.2, xdist-2.2.1
collected 1 item
test_a.yaml 123
.
=== 1 passed in 0.02s ====
當然,也支持復雜一些的測驗用例:
test_abc:
- print: 123
- print: 456
test_abd:
- print: 123
- print: 456
其結果如下:
== test session starts ==
platform win32 -- Python 3.8.1, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: C:\Users\yuruo\Desktop\tmp
plugins: allure-pytest-2.8.11, forked-1.3.0, rerunfailures-9.1.1, timeout-1.4.2, xdist-2.2.1
collected 2 items
test_a.yaml 123
456
.123
456
.
== 2 passed in 0.02s ==
利用pytest創建資料驅動測驗框架就介紹到這里啦,希望能給大家帶來一定的幫助,大家有什么不懂的地方或者有疑惑也可以留言討論哈,讓我們共同進步呦!
喜歡軟體測驗的小伙伴們,如果我的博客對你有幫助、如果你喜歡我的博客內容,請 “點贊” “評論” “收藏” 一鍵三連哦,更多技術文章
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/490010.html
標籤:其他
