HttpRunner3的用例是怎麼執行起來的
在PyCharm中開啟 examples/httpbin/basic_test.py
:
首先映入眼簾的是左上角那個綠色小箭頭,點了一下,可以直接執行,意味著HttpRunner是能夠直接被pytest驅動執行的,這可就有點意思了,難道HttpRunner的底層是pytest?帶著這個疑問我全域性搜尋了一下pytest:
在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模組裡面的一個類:
剛才看到所有程式碼,其實都是在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,迴圈回去了。
貌似陷入了死迴圈,實際上答案已經有了,這不就是 遞迴 麼?再回頭來看剛才這兩個分支:
如果是request,那麼就呼叫 self.__session.request(method, url, **parsed_request_dict)
,這是遞迴的終止條件:
如果是testcase,那麼表示這是子用例,那麼就遞迴下去,這是遞迴的子表示式:
原來,通過 TestCaseBasic().test_start()
來執行測試, 並沒有調pytest,而是直接通過requests傳送HTTP請求的,控制檯和檔案日誌也是使用loguru庫來自定義輸出的 。不得不對原始碼佩服得五體投地。
回到開頭那個問題,為什麼還有pytest的相關程式碼呢,實際上如果是通過命令列的run來執行用例,那麼就是用直接用的pytest了:
一句話總結:如果是用命令列的run命令,那麼就是通過pytest來呼叫的;如果是用程式碼裡的test_start()方法,那麼就是調requests作者自創的。
最後一個問題是,為什麼在PyCharm中點那個綠色的小箭頭,也能執行程式碼呢,答案很簡單, 這個類TestCaseBasic是 Test
開頭的,這個方法test_start是 test_
開頭的,這不就是pytest的規則麼 。
- 模板化的封裝,降低業務程式碼開發
- 分享一個 SpringCloud Feign 中所埋藏的坑
- MySQL 事務常見面試題總結 | JavaGuide 稽核中
- 型別安全的 Go HTTP 請求
- 從幾次事故引起的對專案質量保障的思考
- 聯盟鏈 Hyperledger Fabric 應用場景
- 上半年最中意的 GitHub 更新「GitHub 熱點速覽 v.22.21」
- 為什麼我寫了路由懶載入但程式碼卻沒有分割?
- 這個設計原則,你認同嗎?
- SpringCloud基礎概念學習筆記(Eureka、Ribbon、Feign、Zuul)
- 自動微分原理
- layui資料表格搜尋
- Python 中的記憶體管理
- spring 配置檔案 --bean
- 【leetcode】239. 滑動視窗最大值
- Spring 原始碼(17)Spring Bean的建立過程(8)Bean的初始化
- SpringBoot進階教程(七十四)整合ELK
- 連結串列的基本操作和高頻演算法題
- 【python】python連線Oracle資料庫
- Python技法:浮點數取整、格式化和NaN處理