在HttpRunner3的示例代码中,发送HTTP请求的代码是这样写的:

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")
),
# 省略
Step(
RunRequest("post data")
.post("/post")
.with_headers(**{"Content-Type": "application/json"})
.with_data("abc")
.validate()
.assert_equal("status_code", 200)
),
# 省略
] if __name__ == "__main__":
TestCaseBasic().test_start()
  • 类TestCaseBasic继承了类HttpRunner。
  • 在类TestCaseBasic的内部定义了teststeps列表,由多个Step类的实例对象组成。
  • 类Step初始化传入类RunRequest的方法get和post就把HTTP请求发出去了。

这到底是怎么实现的?

先看下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)

里面定义了get、post等HTTP请求的Method。方法内部:

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

有个TRequest类:

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

它继承了pydantic.BaseModel,是用来做数据验证的,比如这里的url指定了Url类型,如果传一个str类型,就会校验失败。简而言之,这是给代码规范用的,没有实际的业务功能。

下面有一行注释:requests.Request mode,看来这个跟requests有点关系。

回过头来看看self.__step_context.request,也就是self.__step_context对象有个request属性,它的定义是:

self.__step_context = TStep(name=name)

答案应该就在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] = []

还是个Model,里面的request定义是:

request: Union[TRequest, None] = None

又绕回TRequest了。这个Union是typing模块里面的:Union[X, Y] means either X or Y. 意思就是request的类型要么是TRequest要么是None。

在刚才get的方法中,还有一句return RequestWithOptionalArgs(self.__step_context),RequestWithOptionalArgs的定义如下:

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

可以给HTTP请求添加params、headers等可选项。

看到这里,仍然不知道HTTP请求到底发出去的,因为没有调用呀。

只能往上层找,看调用RunRequest的Step类:

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

Step类的__init__方法也用Union做了类型校验,其中RequestWithOptionalArgs就是RunRequest的gei等方法会返回的,这倒是匹配上了。它还有个request属性。有点眉目了。

再往上层找,看HttpRunner类,有个__run_step_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

就是这里了,它的函数名用了双下划线开头:双下划线前缀会让Python解释器重写属性名称,以避免子类中的命名冲突。 这也称为名称改写(name mangling),即解释器会更改变量的名称,以便在稍后扩展这个类时避免命名冲突。说人话就是,类的私有成员,只能在类的内部调用,不对外暴露。它只在__run_step()方法中调用了1次:step_data = self.__run_step_request(step)

中间有一段:

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

好家伙,self.__session.request(),跟reqeusts那个有点像了。点进去。

一下就跳转到了httprunner.client.py众里寻他千百度,默然回首,它竟然就在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):

继承了requests.Session然后进行了重写。

果然,还是用到了requests库。

参考资料:

https://github.com/httprunner/httprunner

HttpRunner3的HTTP请求是怎么发出去的的更多相关文章

  1. 小程序https Android 安卓可以发request请求,IOS 苹果 发请求失败问题

    如果一个机器可以发送成功,一个机器发送失败,那多半是是域名的https支持的问题 那就用腾讯云的这个ssl测试工具检测下 https://www.qcloud.com/product/ssl#user ...

  2. SOAP-XML请求(iOS应用下集成携程api)

    用携程机票为例: 携程联盟 飞机票.门票 联盟ID:278639 站点ID:739462 密钥KEY:BE57B925-E8CE-4AA2-AC8E-3EE4BBBB686F API_URL:open ...

  3. 用Wireshark抓包分析超过70秒的请求

    超过70秒的请求是通过分析IIS日志发现的: 10.159.63.104是SLB的内网IP. 通过Wireshark抓包分析请求是9:22:21收到的(tcp.stream eq 23080): 09 ...

  4. Firefox使用Poster插件发送post请求

    目的:验证http请求功能正确与否,需要发送post,get请求,则可以使用Poster插件方便简单. 自我总结,有什么改正的地方请指出,感激不尽! 1.安装Poster插件. 点击firefox右上 ...

  5. 转:运行page页面时的事件执行顺序及页面的回发与否深度了解

    using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Secu ...

  6. Ajax跨域之ContentType为application/json请求失败的问题

    项目里的接口都是用springmvc写的,其中在@requestmapping接口中定义了consumes="application/json",也就是该接口只接受ContentT ...

  7. Fiddler无法正常抓取谷歌等浏览器的请求_解决方案

    1-先了解Fiddler工作原理: 正常情况下,fiddler是可以抓chrome的请求的. fiddler会自动给浏览器设置一个代理127.0.0.1端口8888,并且记忆浏览器的代理设置,所有的请 ...

  8. csrf 跨站请求伪造相关以及django的中间件

    django为用户实现防止跨站请求伪造的功能,通过中间件 django.middleware.csrf.CsrfViewMiddleware来完成. 1.django中常用的中间件? - proces ...

  9. [js]ajax-异源请求jsonp

    参考: http://www.cnblogs.com/whatisfantasy/p/6237713.html http://www.cnblogs.com/freely/p/6690804.html ...

随机推荐

  1. hitcontraining_magicheap

    拿到题目例行检查 程序是64位的程序 保护几乎全开,试运行一下程序 十分明显的堆溢出的界面,将程序放入ida中,shift+f12发现了后门程序 进入main主函数进行查看 可以看到当,v3==486 ...

  2. bjdctf_2020_babyrop2

    这道题是一道基本题,正因为它经典,所以需要重点记录一下. 这道题考察格式化字符串泄露canary,然后rop获得libc版本,之后拿到shell.拿到程序之后我们先检查一下保护... 开启了堆栈不可执 ...

  3. [BUUCTF]PWN——xdctf2015_pwn200

    xdctf2015_pwn200 附件 步骤 例行检查,32位程序,开启了nx保护 本地试运行一下程序,看看大概的情况 32位ida载入,习惯性的检索程序里的字符串,没有发现什么铭感的地方,直接看ma ...

  4. Sublime Text3 Package Control Emmet插件安装

    https://www.cnblogs.com/carrie-hong/p/4995735.html https://www.cnblogs.com/tamato-jacob-wealllostcon ...

  5. 在myeclipse里加大tomcat内存,jdk内存方法

    这是在myeclipse里加大的方法: -Xms4096m -Xmx4096m -XX:MaxNewSize=4096m -XX:MaxPermSize=4096m 如图所示: -XX:PermSiz ...

  6. Could not synchronize database state with session问题,说保存空

    Could not synchronize database state with session问题,说保存空 ,可以在post.hbm.xml文件里设置inverse="true&quo ...

  7. 当更新user表时,页面没有的属性,执行update语句不会更改以前的值

    当更新user表时,页面没有的属性,执行update语句不会更改数据库表的值.不会用NULL值去填充

  8. 使用iframe实现上下窗口结构及登录页全窗口展示Demo

    iframe.html 首页 <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> < ...

  9. 使用docker自定义oraclejdk启动jar包

    Dockerfile文件 FROM centos:7 #把java与tomcat添加到容器中 ADD jdk-8u161-linux-x64.tar.gz /usr/local/ #安装 vim编辑器 ...

  10. JAVA获取当前年份,月份、日期、小时、分钟、秒等

    import java.util.Calendar; public class Main { public static void main(String[] args) { Calendar cal ...