zoukankan      html  css  js  c++  java
  • HttpRunner3源码阅读:10.测试执行的处理 runner

    runner

    HttpRunner的执行函数存在的位置,程序内部执行运行入口了,文件名称很明显了 runner.py,其中最主要的为run_testcase(),__run_step_request(), __run_step_testcase(),方法

    可用资料

    导包

    import os
    import time
    import uuid  # 32个十六进制数字组成的字符串
    from datetime import datetime
    from typing import List, Dict, Text, NoReturn
    try:  # 导成功就用allure报告
        import allure
    
        USE_ALLURE = True
    except ModuleNotFoundError:
        USE_ALLURE = False
    
    from loguru import logger
    
    from httprunner import utils, exceptions
    from httprunner.client import HttpSession
    from httprunner.exceptions import ValidationFailure, ParamsError
    from httprunner.ext.uploader import prepare_upload_step
    from httprunner.loader import load_project_meta, load_testcase_file
    from httprunner.parser import build_url, parse_data, parse_variables_mapping
    from httprunner.response import ResponseObject
    from httprunner.testcase import Config, Step
    from httprunner.utils import merge_variables
    from httprunner.models import (
        TConfig,
        TStep,
        VariablesMapping,
        StepData,
        TestCaseSummary,
        TestCaseTime,
        TestCaseInOut,
        ProjectMeta,
        TestCase,
        Hooks,
    )
    
    

    源码附注释

    
    class HttpRunner(object):
        # 属性: Config 对象
        config: Config
        # 步骤列表: Step
        teststeps: List[Step]
        # 测试结果
        success: bool = False  # indicate testcase execution result
        __config: TConfig
        __teststeps: List[TStep]
        __project_meta: ProjectMeta = None
        __case_id: Text = ""
        __export: List[Text] = []
        __step_datas: List[StepData] = []
        __session: HttpSession = None
        __session_variables: VariablesMapping = {}
        # time
        __start_at: float = 0
        __duration: float = 0
        # log
        __log_path: Text = ""
    
        def __init_tests__(self) -> NoReturn:
            self.__config = self.config.perform()
            self.__teststeps = []
            for step in self.teststeps:
                self.__teststeps.append(step.perform())
    
        @property
        def raw_testcase(self) -> TestCase:
            if not hasattr(self, "__config"):
                self.__init_tests__()
              # 对象模型 
            return TestCase(config=self.__config, teststeps=self.__teststeps)
    
        def with_project_meta(self, project_meta: ProjectMeta) -> "HttpRunner":
            self.__project_meta = project_meta
            return self
    
        def with_session(self, session: HttpSession) -> "HttpRunner":
            self.__session = session
            return self
    
        def with_case_id(self, case_id: Text) -> "HttpRunner":
            self.__case_id = case_id
            return self
    
        def with_variables(self, variables: VariablesMapping) -> "HttpRunner":
            self.__session_variables = variables
            return self
    
        def with_export(self, export: List[Text]) -> "HttpRunner":
            self.__export = export
            return self
    
        def __call_hooks(
            self, hooks: Hooks, step_variables: VariablesMapping, hook_msg: Text,
        ) -> NoReturn:
            """ call hook actions.
                调用hooks 函数,结果写到 步骤变量中
            Args:
                hooks (list): each hook in hooks list maybe in two format.
    
                    format1 (str): only call hook functions.
                        ${func()}
                    format2 (dict): assignment, the value returned by hook function will be assigned to variable.
                        {"var": "${func()}"}
    
                step_variables: current step variables to call hook, include two special variables
    
                    request: parsed request dict
                    response: ResponseObject for current response
    
                hook_msg: setup/teardown request/testcase
    
            """
            logger.info(f"call hook actions: {hook_msg}")
    
            if not isinstance(hooks, List):
                logger.error(f"Invalid hooks format: {hooks}")
                return
    
            for hook in hooks:
                if isinstance(hook, Text):
                    # format 1: ["${func()}"]
                    logger.debug(f"call hook function: {hook}")
                    parse_data(hook, step_variables, self.__project_meta.functions)
                elif isinstance(hook, Dict) and len(hook) == 1:
                    # format 2: {"var": "${func()}"}
                    var_name, hook_content = list(hook.items())[0]
                    hook_content_eval = parse_data(
                        hook_content, step_variables, self.__project_meta.functions
                    )
                    logger.debug(
                        f"call hook function: {hook_content}, got value: {hook_content_eval}"
                    )
                    logger.debug(f"assign variable: {var_name} = {hook_content_eval}")
                    step_variables[var_name] = hook_content_eval
                else:
                    logger.error(f"Invalid hook format: {hook}")
    
    
        def __run_step_request(self, step: TStep) -> StepData:
            """run teststep: request"""
            step_data = StepData(name=step.name)
    
            # parse
            # 准备文件上传步骤
            prepare_upload_step(step, self.__project_meta.functions)
            # 请求字典
            request_dict = step.request.dict()
            request_dict.pop("upload", None)
            # 解析变量&方法数据
            parsed_request_dict = parse_data(
                request_dict, step.variables, self.__project_meta.functions
            )
            # 设置请求头
            parsed_request_dict["headers"].setdefault(
                "HRUN-Request-ID",
                f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}",
            )
            # 步骤参数字典 加 key request
            step.variables["request"] = parsed_request_dict
    
            # setup hooks  setup 的钩子函数
            if step.setup_hooks:
                self.__call_hooks(step.setup_hooks, step.variables, "setup request")
    
            # prepare arguments
            # 移除字典中的method,返回对应的value
            method = parsed_request_dict.pop("method")
            url_path = parsed_request_dict.pop("url")
            # 组装最终请求url
            url = build_url(self.__config.base_url, url_path)
            parsed_request_dict["verify"] = self.__config.verify
            parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {})
    
            # request 发起请求
            resp = self.__session.request(method, url, **parsed_request_dict)
            resp_obj = ResponseObject(resp)
            step.variables["response"] = resp_obj
    
            # teardown hooks 请求完成之后执行函数
            if step.teardown_hooks:
                self.__call_hooks(step.teardown_hooks, step.variables, "teardown request")
    
            def log_req_resp_details(): # 详细日志
                err_msg = "
    {} DETAILED REQUEST & RESPONSE {}
    ".format("*" * 32, "*" * 32)
    
                # log request
                err_msg += "====== request details ======
    "
                err_msg += f"url: {url}
    "
                err_msg += f"method: {method}
    "
                headers = parsed_request_dict.pop("headers", {})
                err_msg += f"headers: {headers}
    "
                for k, v in parsed_request_dict.items():
                    v = utils.omit_long_data(v)
                    err_msg += f"{k}: {repr(v)}
    "
    
                err_msg += "
    "
    
                # log response
                err_msg += "====== response details ======
    "
                err_msg += f"status_code: {resp.status_code}
    "
                err_msg += f"headers: {resp.headers}
    "
                err_msg += f"body: {repr(resp.text)}
    "
                logger.error(err_msg)
    
            # extract 提取参数
            extractors = step.extract
            extract_mapping = resp_obj.extract(extractors)
            step_data.export_vars = extract_mapping
    
            variables_mapping = step.variables
            variables_mapping.update(extract_mapping)
    
            # validate 验证
            validators = step.validators
            session_success = False
            try:
                resp_obj.validate(
                    validators, variables_mapping, self.__project_meta.functions
                )
                session_success = True
            except ValidationFailure:
                session_success = False
                log_req_resp_details()
                # log testcase duration before raise ValidationFailure
                self.__duration = time.time() - self.__start_at
                raise
            finally:
                self.success = session_success
                step_data.success = session_success
    
                if hasattr(self.__session, "data"):
                    # httprunner.client.HttpSession, not locust.clients.HttpSession
                    # save request & response meta data
                    self.__session.data.success = session_success
                    self.__session.data.validators = resp_obj.validation_results
    
                    # save step data
                    step_data.data = self.__session.data
            # 返回步骤数据
            return step_data
    
        def __run_step_testcase(self, step: TStep) -> StepData:
            """run teststep: referenced testcase 步骤中引入的其他测试用例"""
            step_data = StepData(name=step.name)
            step_variables = step.variables
            step_export = step.export
    
            # setup hooks
            if step.setup_hooks:
                self.__call_hooks(step.setup_hooks, step_variables, "setup testcase")
               # 运行测试用例 
            if hasattr(step.testcase, "config") and hasattr(step.testcase, "teststeps"):
                testcase_cls = step.testcase
                case_result = (
                    testcase_cls()
                    .with_session(self.__session)
                    .with_case_id(self.__case_id)
                    .with_variables(step_variables)
                    .with_export(step_export)
                    .run()
                )
    
            elif isinstance(step.testcase, Text):
                if os.path.isabs(step.testcase):
                    ref_testcase_path = step.testcase
                else:
                    ref_testcase_path = os.path.join(
                        self.__project_meta.RootDir, step.testcase
                    )
    
                case_result = (
                    HttpRunner()
                    .with_session(self.__session)
                    .with_case_id(self.__case_id)
                    .with_variables(step_variables)
                    .with_export(step_export)
                    .run_path(ref_testcase_path)
                )
    
            else:
                raise exceptions.ParamsError(
                    f"Invalid teststep referenced testcase: {step.dict()}"
                )
    
            # teardown hooks
            if step.teardown_hooks:
                self.__call_hooks(step.teardown_hooks, step.variables, "teardown testcase")
    
            step_data.data = case_result.get_step_datas()  # list of step data
            step_data.export_vars = case_result.get_export_variables()
            step_data.success = case_result.success
            self.success = case_result.success
    
            if step_data.export_vars:
                logger.info(f"export variables: {step_data.export_vars}")
    
            return step_data
    
        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} <<<<<<
    ")
            return step_data.export_vars
    
        def __parse_config(self, config: TConfig) -> NoReturn:
            """解析配置"""
            config.variables.update(self.__session_variables)
            config.variables = parse_variables_mapping(
                config.variables, self.__project_meta.functions
            )
            config.name = parse_data(
                config.name, config.variables, self.__project_meta.functions
            )
            config.base_url = parse_data(
                config.base_url, config.variables, self.__project_meta.functions
            )
    
        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
    
        def run_path(self, path: Text) -> "HttpRunner":  # 文件类测试用例
            if not os.path.isfile(path):
                raise exceptions.ParamsError(f"Invalid testcase path: {path}")
    
            testcase_obj = load_testcase_file(path)   # 转成测试用例对象
            return self.run_testcase(testcase_obj)  # 运行用例
    
        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)
    
        def get_step_datas(self) -> List[StepData]: # 步骤数据列表
            return self.__step_datas
    
        def get_export_variables(self) -> Dict:  # 导出变量字典
            # override testcase export vars with step export
            export_var_names = self.__export or self.__config.export
            export_vars_mapping = {}
            for var_name in export_var_names:
                if var_name not in self.__session_variables:
                    raise ParamsError(
                        f"failed to export variable {var_name} from session variables {self.__session_variables}"
                    )
    
                export_vars_mapping[var_name] = self.__session_variables[var_name]
    
            return export_vars_mapping
    
        def get_summary(self) -> TestCaseSummary: # 结果集
            """get testcase result summary"""
            start_at_timestamp = self.__start_at
            start_at_iso_format = datetime.utcfromtimestamp(start_at_timestamp).isoformat()
            return TestCaseSummary(
                name=self.__config.name,
                success=self.success,
                case_id=self.__case_id,
                time=TestCaseTime(
                    start_at=self.__start_at,
                    start_at_iso_format=start_at_iso_format,
                    duration=self.__duration,
                ),
                in_out=TestCaseInOut(
                    config_vars=self.__config.variables,
                    export_vars=self.get_export_variables(),
                ),
                log=self.__log_path,
                step_datas=self.__step_datas,
            )
    
        def test_start(self, param: Dict = None) -> "HttpRunner":
            """main entrance, discovered by pytest test_start函数 由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}")
    
    
    作者:zy7y
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文链接,否则保留追究法律责任的权利。
  • 相关阅读:
    arm-linux-gcc-4.5.1的安装…
    OK6410之tftp下载内核,nfs…
    非常详细的ok6410的linux系统移植…
    2009-2010网络最热的&nbsp;嵌入式…
    Vue-基础(二)
    【SpringBoot】Springboot1.5.9整合WebSocket
    Hadoop本地环境安装
    Vue-基础(一)
    【Jwt】JSON Web Token
    【JDK8】Java8 新增BASE64加解密API
  • 原文地址:https://www.cnblogs.com/zy7y/p/15119929.html
Copyright © 2011-2022 走看看