Pytest 框架執行用例流程淺談

2023-08-30 06:01:18

背景:

  根據以下簡單的程式碼範例,我們將從原始碼的角度分析其中的關鍵載入執行步驟,對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,表示在終端輸出最終的測試結果和統計資訊。