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.