根據以下簡單的程式碼範例,我們將從原始碼的角度分析其中的關鍵載入執行步驟,對pytest整體流程架構有個初步學習。
程式碼範例:
import pytest def test_add(): assert 1 + 1 == 2 def test_sub(): assert 2 - 1 == 1
通過 pytest test_example.py 執行此程式碼範例後,會觸發pytest的入口函數main(),這個函數定義在src/pytest/__main__.py中,它的作用是建立一個PytestConfig物件,並呼叫其
do_configure()和do_main()方法。PytestConfig物件是pytest的核心設定類,它負責解析命令列引數、讀取組態檔、註冊外掛、建立Session物件等。PytestConfig物件定義在
src/_pytest/config/__init__.py中,它繼承了pluggy.HookimplMarker類,也就是說它可以作為一個外掛管理器,呼叫各種hook函數。
```python # src/pytest/__main__.py def main(): # 建立PytestConfig物件 config = PytestConfig() # 呼叫config.do_configure()方法 config.do_configure() # 呼叫config.do_main()方法 config.do_main() ``` ```python # src/_pytest/config/__init__.py class PytestConfig(pluggy.HookimplMarker): def __init__(self): # 解析命令列引數 self.parse_args() # 讀取組態檔 self.read_config_files() # 註冊外掛 self.register_plugins() # 建立Session物件 self.session = Session(self) def do_configure(self): # 呼叫hook函數pytest_configure self.hook.pytest_configure(config=self) def do_main(self): # 呼叫hook函數pytest_sessionstart self.hook.pytest_sessionstart(session=self.session) # 呼叫Session物件的main()方法 self.session.main() ```
Session物件是pytest的核心上下文類,它負責管理整個測試過程的資訊,包括收集測試用例、執行測試用例、生成測試報告等。Session物件定義在src/_pytest/main.py中,它繼承了
Collector類,也就是說它可以作為一個測試用例收集器。Session物件的main()方法是執行測試用例的主要入口,它會呼叫perform_collect()方法來收集測試用例,並返回一個列表items;然後
呼叫runtestloop()方法來回圈執行items中的每個Item物件;最後呼叫hook函數pytest_sessionfinish來結束測試對談,並返回一個退出碼exitstatus。
```python # src/_pytest/main.py class Session(Collector): def __init__(self, config): # 初始化一些屬性和狀態資訊 def main(self): # 呼叫perform_collect()方法收集測試用例,並返回items列表 items = self.perform_collect() # 呼叫runtestloop()方法迴圈執行items中的每個Item物件,並返回退出碼exitstatus exitstatus = self.runtestloop(items) # 呼叫hook函數pytest_sessionfinish來結束測試對談,並返回退出碼exitstatus self.hook.pytest_sessionfinish(session=self, exitstatus=exitstatus) return exitstatus def perform_collect(self): # 呼叫hook函數pytest_collectstart表示開始收集測試用例 self.hook.pytest_collectstart(collector=self) # 呼叫自身的collect()方法來遞迴遍歷指定的測試檔案或目錄,並返回一個列表items items = self.collect() # 呼叫hook函數pytest_collectreport表示收集測試用例結束,並生成收集報告 self.hook.pytest_collectreport(report=CollectReport(self, "passed", items)) # 呼叫hook函數pytest_collection_modifyitems允許對收集到的Item物件進行修改 self.hook.pytest_collection_modifyitems(session=self, config=self.config, items=items) # 呼叫hook函數pytest_deselected表示從收集到的Item物件中篩選出需要執行的Item物件 self.hook.pytest_deselected(items=self.deselected) # 呼叫hook函數pytest_collection_finish表示收集和篩選測試用例完成,並返回最終要執行的Item物件列表 self.hook.pytest_collection_finish(session=self) return items def runtestloop(self, items): # 呼叫hook函數pytest_runtestloop表示開始迴圈執行測試用例 self.hook.pytest_runtestloop(session=self) # 遍歷items列表,依次取出每個Item物件 for item in items: # 呼叫Item物件的runtestprotocol()方法來執行單個測試用例的協定 item.runtestprotocol() # 返回退出碼0表示成功 return 0 ```
Item物件是pytest的核心測試類,它負責封裝和執行單個測試用例的資訊,包括名稱、位置、引數化資訊、標記資訊等。Item物件定義在src/_pytest/python.py中,它繼承了Node類,也
就是說它可以作為一個測試節點。Item物件的runtestprotocol()方法是執行單個測試用例的主要入口,它會呼叫hook函數pytest_runtest_logstart來開始記錄紀錄檔資訊;然後呼叫runtest()方法
來執行測試用例的前置、主體和後置部分;最後呼叫hook函數pytest_runtest_logfinish來結束記錄紀錄檔資訊,並生成紀錄檔報告。
```python # src/_pytest/python.py class Item(Node): def __init__(self, name, parent, config, session): # 初始化一些屬性和狀態資訊 def runtestprotocol(self): # 呼叫hook函數pytest_runtest_logstart表示開始記錄紀錄檔資訊 self.ihook.pytest_runtest_logstart(nodeid=self.nodeid, location=self.location) # 呼叫runtest()方法來執行測試用例的前置、主體和後置部分,並返回一個列表reports reports = self.runtest() # 呼叫hook函數pytest_runtest_logfinish表示結束記錄紀錄檔資訊,並生成紀錄檔報告 self.ihook.pytest_runtest_logfinish(nodeid=self.nodeid, location=self.location) return reports def runtest(self): # 建立一個空列表reports reports = [] # 呼叫_setup()方法來執行測試用例的前置操作,並將返回的報告新增到reports列表中 reports.append(self._setup()) # 如果前置操作沒有失敗或跳過,則呼叫_call()方法來執行測試用例的主體部分,並將返回的報告新增到reports列表中 if reports[-1].passed: reports.append(self._call()) # 呼叫_teardown()方法來執行測試用例的後置操作,並將返回的報告新增到reports列表中 reports.append(self._teardown()) # 返回reports列表 return reports def _setup(self): # 呼叫hook函數pytest_runtest_setup表示開始執行前置操作,並返回一個報告setup_report setup_report = self.ihook.pytest_runtest_setup(item=self) return setup_report def _call(self): # 呼叫hook函數pytest_runtest_call表示開始執行主體部分,並返回一個報告call_report call_report = self.ihook.pytest_runtest_call(item=self) return call_report def _teardown(self): # 呼叫hook函數pytest_runtest_teardown表示開始執行後置操作,並返回一個報告teardown_report teardown_report = self.ihook.pytest_runtest_teardown(item=self) return teardown_report ```
Pytest的載入流程大致如下:
- Pytest首先會解析命令列引數,確定要執行的測試檔案、測試目錄、測試類、測試函數等,以及一些設定選項。
- Pytest會根據組態檔(pytest.ini、setup.cfg、tox.ini等)和命令列引數,建立一個Config物件,用於儲存設定資訊。
- Pytest會建立一個Session物件,用於管理整個測試過程的上下文資訊,包括收集測試用例、執行測試用例、生成測試報告等。
- Pytest會呼叫hook函數pytest_sessionstart,表示測試對談開始。
- Pytest會呼叫hook函數pytest_collectstart,表示開始收集測試用例。
- Pytest會根據Config物件中的資訊,遞迴遍歷指定的測試檔案或目錄,尋找符合pytest約定的測試用例(以test_開頭的函數或方法,以Test開頭的類等)。
- Pytest會將找到的測試用例封裝成Item物件,並新增到Session物件的items列表中。Item物件包含了測試用例的名稱、位置、引數化資訊、標記資訊等。
- Pytest會呼叫hook函數pytest_collectreport,表示收集測試用例結束,並生成收集報告。
- Pytest會呼叫hook函數pytest_collection_modifyitems,允許對收集到的Item物件進行修改,例如重新排序、新增或刪除標記等。
- Pytest會呼叫hook函數pytest_deselected,表示從收集到的Item物件中篩選出需要執行的Item物件,並將不需要執行的Item物件放入Session物件的deselected列表中。
- Pytest會呼叫hook函數pytest_collection_finish,表示收集和篩選測試用例完成,並返回最終要執行的Item物件列表。
- Pytest會根據是否使用多程序或多執行緒模式,建立相應的WorkerController物件,用於管理多個Worker物件。Worker物件負責執行具體的測試用例,並將結果返回給WorkerController物件。
- Pytest會呼叫hook函數pytest_runtestloop,表示開始迴圈執行測試用例。
- Pytest會遍歷Session物件中的items列表,依次取出每個Item物件,並呼叫hook函數pytest_runtest_protocol,表示開始執行單個測試用例的協定。
- Pytest會呼叫hook函數pytest_runtest_logstart,表示開始記錄單個測試用例的紀錄檔資訊。
- Pytest會呼叫hook函數pytest_runtest_setup,表示開始執行單個測試用例的前置操作(例如setup函數或方法)。
- Pytest會呼叫hook函數pytest_runtest_call,表示開始執行單個測試用例的主體部分(例如測試函數或方法)。
- Pytest會呼叫hook函數pytest_runtest_teardown,表示開始執行單個測試用例的後置操作(例如teardown函數或方法)。
- Pytest會呼叫hook函數pytest_runtest_logfinish,表示結束記錄單個測試用例的紀錄檔資訊,並生成紀錄檔報告。
- Pytest會呼叫hook函數pytest_runtest_makereport,表示根據單個測試用例的執行結果,生成測試報告(包括setup、call和teardown三個階段的報告)。
- Pytest會重複上述步驟,直到所有的Item物件都被執行完畢。
- Pytest會呼叫hook函數pytest_sessionfinish,表示測試對談結束,並生成最終的測試報告(包括所有Item物件的報告)。
- Pytest會呼叫hook函數pytest_terminal_summary,表示在終端輸出最終的測試結果和統計資訊。