HttpRunner3的用例是怎麼運行起來的

在PyCharm中打開examples/httpbin/basic_test.py

image-20220117193731185

首先映入眼簾的是左上角那個綠色小箭頭,點了一下,可以直接運行,意味着HttpRunner是能夠直接被pytest驅動運行的,這可就有點意思了,難道HttpRunner的底層是pytest?帶着這個疑問我全局搜索了一下pytest:

image-20220117194052189

在cli.py文件中,如果參數是run,那麼會執行pytest.main(["h"]),難道真是我猜測的這樣?在basic_test.py最後有兩行代碼:

if __name__ == "__main__":
    TestCaseBasic().test_start()

試着從這裏追蹤,應該就能對調用鏈路拿捏個十拿九穩了。test_start()的源碼如下:

def test_start(self, param: Dict = None) -> "HttpRunner":
    """main entrance, discovered by pytest"""
    self.__init_tests__()
    self.__project_meta = self.__project_meta or load_project_meta(
        self.__config.path
    )
    self.__case_id = self.__case_id or str(uuid.uuid4())
    self.__log_path = self.__log_path or os.path.join(
        self.__project_meta.RootDir, "logs", f"{self.__case_id}.run.log"
    )
    log_handler = logger.add(self.__log_path, level="DEBUG")

    # parse config name
    config_variables = self.__config.variables
    if param:
        config_variables.update(param)
    config_variables.update(self.__session_variables)
    self.__config.name = parse_data(
        self.__config.name, config_variables, self.__project_meta.functions
    )

    if USE_ALLURE:
        # update allure report meta
        allure.dynamic.title(self.__config.name)
        allure.dynamic.description(f"TestCase ID: {self.__case_id}")

    logger.info(
        f"Start to run testcase: {self.__config.name}, TestCase ID: {self.__case_id}"
    )

    try:
        return self.run_testcase(
            TestCase(config=self.__config, teststeps=self.__teststeps)
        )
    finally:
        logger.remove(log_handler)
        logger.info(f"generate testcase log: {self.__log_path}")

第一行註釋就是證明了我的猜想是對的:main entrance, discovered by pytest,主程序入口,會被pytest發現。本文不去探究每行代碼是什麼意思,重點關注跟pytest相關的運行流程。跟着這段代碼:

return self.run_testcase(
    TestCase(config=self.__config, teststeps=self.__teststeps)
)

繼續往下走,調用了self.run_testcase,它的源碼如下:

def run_testcase(self, testcase: TestCase) -> "HttpRunner":
    """run specified testcase

    Examples:
        >>> testcase_obj = TestCase(config=TConfig(...), teststeps=[TStep(...)])
        >>> HttpRunner().with_project_meta(project_meta).run_testcase(testcase_obj)

    """
    self.__config = testcase.config
    self.__teststeps = testcase.teststeps

    # prepare
    self.__project_meta = self.__project_meta or load_project_meta(
        self.__config.path
    )
    self.__parse_config(self.__config)
    self.__start_at = time.time()
    self.__step_datas: List[StepData] = []
    self.__session = self.__session or HttpSession()
    # save extracted variables of teststeps
    extracted_variables: VariablesMapping = {}

    # run teststeps
    for step in self.__teststeps:
        # override variables
        # step variables > extracted variables from previous steps
        step.variables = merge_variables(step.variables, extracted_variables)
        # step variables > testcase config variables
        step.variables = merge_variables(step.variables, self.__config.variables)

        # parse variables
        step.variables = parse_variables_mapping(
            step.variables, self.__project_meta.functions
        )

        # run step
        if USE_ALLURE:
            with allure.step(f"step: {step.name}"):
                extract_mapping = self.__run_step(step)
        else:
            extract_mapping = self.__run_step(step)

        # save extracted variables to session variables
        extracted_variables.update(extract_mapping)

    self.__session_variables.update(extracted_variables)
    self.__duration = time.time() - self.__start_at
    return self

跟着這段代碼:

# run step
if USE_ALLURE:
    with allure.step(f"step: {step.name}"):
        extract_mapping = self.__run_step(step)
else:
    extract_mapping = self.__run_step(step)

繼續往下走,self.__run_step的源碼如下:

def __run_step(self, step: TStep) -> Dict:
    """run teststep, teststep maybe a request or referenced testcase"""
    logger.info(f"run step begin: {step.name} >>>>>>")

    if step.request:
        step_data = self.__run_step_request(step)
    elif step.testcase:
        step_data = self.__run_step_testcase(step)
    else:
        raise ParamsError(
            f"teststep is neither a request nor a referenced testcase: {step.dict()}"
        )

    self.__step_datas.append(step_data)
    logger.info(f"run step end: {step.name} <<<<<<\n")
    return step_data.export_vars

有兩個分支:

if step.request:
    step_data = self.__run_step_request(step)
elif step.testcase:
    step_data = self.__run_step_testcase(step)

self.__run_step_request(step)直接調用的request:

resp = self.__session.request(method, url, **parsed_request_dict)

self.__run_step_testcase(step)直接調用的HttpRunner():

case_result = (
    testcase_cls()
    .with_session(self.__session)
    .with_case_id(self.__case_id)
    .with_variables(step_variables)
    .with_export(step_export)
    .run()
)

真相只有一個,一定在HttpRunner裏面。HttpRunner是run.py模塊裏面的一個類:

image-20220117203536198

剛纔看到所有代碼,其實都是在runner.py模塊的HttpRunner類裏面。看看run函數的代碼:

def run(self) -> "HttpRunner":
    """ run current testcase

    Examples:
        >>> TestCaseRequestWithFunctions().run()

    """
    self.__init_tests__()
    testcase_obj = TestCase(config=self.__config, teststeps=self.__teststeps)
    return self.run_testcase(testcase_obj)

又調用了self.run_testcase,循環回去了。

貌似陷入了死循環,實際上答案已經有了,這不就是遞歸麼?再回頭來看剛纔這兩個分支:

image-20220117211717252

如果是request,那麼就調用self.__session.request(method, url, **parsed_request_dict),這是遞歸的終止條件:

image-20220117211746802

如果是testcase,那麼表示這是子用例,那麼就遞歸下去,這是遞歸的子表達式:

image-20220117211917991

原來,通過TestCaseBasic().test_start()來執行測試,並沒有調pytest,而是直接通過requests發送HTTP請求的,控制檯和文件日誌也是使用loguru庫來自定義輸出的。不得不對源碼佩服得五體投地。

回到開頭那個問題,爲什麼還有pytest的相關代碼呢,實際上如果是通過命令行的run來執行用例,那麼就是用直接用的pytest了:

image-20220117212644290

image-20220117212709211

一句話總結:如果是用命令行的run命令,那麼就是通過pytest來調用的;如果是用代碼裏的test_start()方法,那麼就是調requests作者自創的。

最後一個問題是,爲什麼在PyCharm中點那個綠色的小箭頭,也能運行代碼呢,答案很簡單,這個類TestCaseBasic是Test開頭的,這個方法test_start是test_開頭的,這不就是pytest的規則麼

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