How is the HTTP request of HttpRunner3 sent out

Posted by Mr. Tech on Wed, 26 Jan 2022 23:45:34 +0100

In the example code of HttpRunner3, the code for sending HTTP requests is written as follows:

from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase


class TestCaseBasic(HttpRunner):

    config = Config("basic test with httpbin").base_url("https://httpbin.org/")

    teststeps = [
        Step(
            RunRequest("headers")
            .get("/headers")
            .validate()
            .assert_equal("status_code", 200)
            .assert_equal("body.headers.Host", "httpbin.org")
        ),
        # ellipsis
        Step(
            RunRequest("post data")
            .post("/post")
            .with_headers(**{"Content-Type": "application/json"})
            .with_data("abc")
            .validate()
            .assert_equal("status_code", 200)
        ),
        # ellipsis
    ]


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

Class TestCaseBasic inherits class HttpRunner.
A teststeps list is defined inside the class TestCaseBasic, which is composed of multiple Step class instance objects.
Class Step initializes the methods get and post of the incoming class RunRequest to send the HTTP request.

How did this happen?

First look at the source code of RunRequest:

class RunRequest(object):
    def __init__(self, name: Text):
        self.__step_context = TStep(name=name)

    def with_variables(self, **variables) -> "RunRequest":
        self.__step_context.variables.update(variables)
        return self

    def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunRequest":
        if assign_var_name:
            self.__step_context.setup_hooks.append({assign_var_name: hook})
        else:
            self.__step_context.setup_hooks.append(hook)

        return self

    def get(self, url: Text) -> RequestWithOptionalArgs:
        self.__step_context.request = TRequest(method=MethodEnum.GET, url=url)
        return RequestWithOptionalArgs(self.__step_context)

    def post(self, url: Text) -> RequestWithOptionalArgs:
        self.__step_context.request = TRequest(method=MethodEnum.POST, url=url)
        return RequestWithOptionalArgs(self.__step_context)

    def put(self, url: Text) -> RequestWithOptionalArgs:
        self.__step_context.request = TRequest(method=MethodEnum.PUT, url=url)
        return RequestWithOptionalArgs(self.__step_context)

    def head(self, url: Text) -> RequestWithOptionalArgs:
        self.__step_context.request = TRequest(method=MethodEnum.HEAD, url=url)
        return RequestWithOptionalArgs(self.__step_context)

    def delete(self, url: Text) -> RequestWithOptionalArgs:
        self.__step_context.request = TRequest(method=MethodEnum.DELETE, url=url)
        return RequestWithOptionalArgs(self.__step_context)

    def options(self, url: Text) -> RequestWithOptionalArgs:
        self.__step_context.request = TRequest(method=MethodEnum.OPTIONS, url=url)
        return RequestWithOptionalArgs(self.__step_context)

    def patch(self, url: Text) -> RequestWithOptionalArgs:
        self.__step_context.request = TRequest(method=MethodEnum.PATCH, url=url)
        return RequestWithOptionalArgs(self.__step_context)

It defines the methods of HTTP requests such as get and post. Method internal:

self.__step_context.request = TRequest(method=MethodEnum.GET, url=url)

There are three TRequest classes:

class TRequest(BaseModel):
    """requests.Request model"""

    method: MethodEnum
    url: Url
    params: Dict[Text, Text] = {}
    headers: Headers = {}
    req_json: Union[Dict, List, Text] = Field(None, alias="json")
    data: Union[Text, Dict[Text, Any]] = None
    cookies: Cookies = {}
    timeout: float = 120
    allow_redirects: bool = True
    verify: Verify = False
    upload: Dict = {}  # used for upload files

It inherits pydantic Basemodel is used for data verification. For example, the URL here specifies the URL type. If a str type is passed, the verification will fail. In short, this is for code specification, and there is no actual business function.

Below is a line of comments: requests Request mode. It seems that this has something to do with requests.

Let's look back at {self__ step_ context. Request, that is, self__ step_ The context # object has a request attribute, which is defined as:

self.__step_context = TStep(name=name)

The answer should be in TStep:

class TStep(BaseModel):
    name: Name
    request: Union[TRequest, None] = None
    testcase: Union[Text, Callable, None] = None
    variables: VariablesMapping = {}
    setup_hooks: Hooks = []
    teardown_hooks: Hooks = []
    # used to extract request's response field
    extract: VariablesMapping = {}
    # used to export session variables from referenced testcase
    export: Export = []
    validators: Validators = Field([], alias="validate")
    validate_script: List[Text] = []

It is also a Model. The definition of request in it is:

request: Union[TRequest, None] = None

Back to TRequest. This union is in the typing module: Union[X, Y] means either X or Y. it means that the type of request is either TRequest or None.

In the method of get just now, there is also a sentence "return RequestWithOptionalArgs (self. _step_context). The definition of RequestWithOptionalArgs is as follows:

class RequestWithOptionalArgs(object):
    def __init__(self, step_context: TStep):
        self.__step_context = step_context

    def with_params(self, **params) -> "RequestWithOptionalArgs":
        self.__step_context.request.params.update(params)
        return self

    def with_headers(self, **headers) -> "RequestWithOptionalArgs":
        self.__step_context.request.headers.update(headers)
        return self

    def with_cookies(self, **cookies) -> "RequestWithOptionalArgs":
        self.__step_context.request.cookies.update(cookies)
        return self

    def with_data(self, data) -> "RequestWithOptionalArgs":
        self.__step_context.request.data = data
        return self

    def with_json(self, req_json) -> "RequestWithOptionalArgs":
        self.__step_context.request.req_json = req_json
        return self

    def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs":
        self.__step_context.request.timeout = timeout
        return self

    def set_verify(self, verify: bool) -> "RequestWithOptionalArgs":
        self.__step_context.request.verify = verify
        return self

    def set_allow_redirects(self, allow_redirects: bool) -> "RequestWithOptionalArgs":
        self.__step_context.request.allow_redirects = allow_redirects
        return self

    def upload(self, **file_info) -> "RequestWithOptionalArgs":
        self.__step_context.request.upload.update(file_info)
        return self

    def teardown_hook(
        self, hook: Text, assign_var_name: Text = None
    ) -> "RequestWithOptionalArgs":
        if assign_var_name:
            self.__step_context.teardown_hooks.append({assign_var_name: hook})
        else:
            self.__step_context.teardown_hooks.append(hook)

        return self

    def extract(self) -> StepRequestExtraction:
        return StepRequestExtraction(self.__step_context)

    def validate(self) -> StepRequestValidation:
        return StepRequestValidation(self.__step_context)

    def perform(self) -> TStep:
        return self.__step_context

You can add params, headers and other options to HTTP requests.

Seeing this, I still don't know where the HTTP request was sent, because there was no call.

You can only look at the upper layer. See the Step class calling RunRequest:

class Step(object):
    def __init__(
        self,
        step_context: Union[
            StepRequestValidation,
            StepRequestExtraction,
            RequestWithOptionalArgs,
            RunTestCase,
            StepRefCase,
        ],
    ):
        self.__step_context = step_context.perform()

    @property
    def request(self) -> TRequest:
        return self.__step_context.request

    @property
    def testcase(self) -> TestCase:
        return self.__step_context.testcase

    def perform(self) -> TStep:
        return self.__step_context

Of Step class__ init__ The method also uses Union for type verification, in which RequestWithOptionalArgs is the gei and other methods of RunRequest, which is matched. It also has a request attribute. A little.

Go to the upper level and look at the HttpRunner class. There is a__ run_ step_ Method of request:

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:]}",
    )
    step.variables["request"] = parsed_request_dict

    # setup hooks
    if step.setup_hooks:
        self.__call_hooks(step.setup_hooks, step.variables, "setup request")

    # prepare arguments
    method = parsed_request_dict.pop("method")
    url_path = parsed_request_dict.pop("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 = "\n{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)

        # log request
        err_msg += "====== request details ======\n"
        err_msg += f"url: {url}\n"
        err_msg += f"method: {method}\n"
        headers = parsed_request_dict.pop("headers", {})
        err_msg += f"headers: {headers}\n"
        for k, v in parsed_request_dict.items():
            v = utils.omit_long_data(v)
            err_msg += f"{k}: {repr(v)}\n"

        err_msg += "\n"

        # log response
        err_msg += "====== response details ======\n"
        err_msg += f"status_code: {resp.status_code}\n"
        err_msg += f"headers: {resp.headers}\n"
        err_msg += f"body: {repr(resp.text)}\n"
        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

That's it. Its function name starts with a double underscore: the double underscore prefix will let the Python interpreter rewrite the attribute name to avoid naming conflicts in subclasses. This is also known as name mangling, which means that the interpreter changes the name of the variable to avoid naming conflicts when extending the class later. In human words, private members of a class can only be called inside the class without being exposed. It's only in__ run_ The step () method called 1 times: step_data = self.__run_step_request(step)​​.

There is a paragraph in the middle:

# request
resp = self.__session.request(method, url, **parsed_request_dict)
resp_obj = ResponseObject(resp)
step.variables["response"] = resp_obj

Good guy, self__ session. Request (), a bit like reqeusts. Click in.

Jump to httprunner client. Py, the crowd looked for him thousands of times. Looking back silently, it was even in the client.

class HttpSession(requests.Session):
    """
    Class for performing HTTP requests and holding (session-) cookies between requests (in order
    to be able to log in and out of websites). Each request is logged so that HttpRunner can
    display statistics.

    This is a slightly extended version of `python-request <http://python-requests.org>`_'s
    :py:class:`requests.Session` class and mostly this class works exactly the same.
    """

    def __init__(self):
        super(HttpSession, self).__init__()
        self.data = SessionData()

    def update_last_req_resp_record(self, resp_obj):
        """
        update request and response info from Response() object.
        """
        # TODO: fix
        self.data.req_resps.pop()
        self.data.req_resps.append(get_req_resp_record(resp_obj))

    def request(self, method, url, name=None, **kwargs):

Inherited requests Session is then rewritten.

Sure enough, the requests library was used.

Topics: Python AI