HttpRunner3的变量可以在测试类的用例配置中通过variables添加,也可以在测试步骤中使用extract()with_jmespath()提取出来放到变量x,再用$x传递给下一个接口使用,比如登录到下单流程的部分测试脚本如下:

from httprunner import HttpRunner, Config, Step, RunRequest

class TestLoginPay(HttpRunner):
config = (
Config("登录到下单流程")
.variables(
**{
"skuNum": "3"
}
)
.base_url("http://127.0.0.1:5000")
) teststeps = [
Step(
RunRequest("登录")
.post("/login")
.with_headers(**{"Content-Type": "application/json"})
.with_json({"username": "dongfanger", "password": "123456"})
.extract()
.with_jmespath("body.token", "token")
.validate()
.assert_equal("status_code", 200)
),
Step(
RunRequest("搜索商品")
.get("searchSku?skuName=电子书")
.with_headers(**{"token": "$token"})
.extract()
.with_jmespath("body.skuId", "skuId")
.with_jmespath("body.price", "skuPrice")
.validate()
.assert_equal("status_code", 200)
),
]

首先来看用例配置的这段代码:

config = (
Config("登录到下单流程")
.variables(
**{
"skuNum": "3"
}
)
.base_url("http://127.0.0.1:5000")
)

是怎么实现的。Config的定义如下:

class Config(object):
def __init__(self, name: Text):
self.__name = name
self.__variables = {}
self.__base_url = ""
self.__verify = False
self.__export = []
self.__weight = 1 caller_frame = inspect.stack()[1]
self.__path = caller_frame.filename @property
def name(self) -> Text:
return self.__name @property
def path(self) -> Text:
return self.__path @property
def weight(self) -> int:
return self.__weight def variables(self, **variables) -> "Config":
self.__variables.update(variables)
return self def base_url(self, base_url: Text) -> "Config":
self.__base_url = base_url
return self def verify(self, verify: bool) -> "Config":
self.__verify = verify
return self def export(self, *export_var_name: Text) -> "Config":
self.__export.extend(export_var_name)
return self def locust_weight(self, weight: int) -> "Config":
self.__weight = weight
return self def perform(self) -> TConfig:
return TConfig(
name=self.__name,
base_url=self.__base_url,
verify=self.__verify,
variables=self.__variables,
export=list(set(self.__export)),
path=self.__path,
weight=self.__weight,
)

其中variables的定义是:

def variables(self, **variables) -> "Config":
self.__variables.update(variables)
return self

self.__variables = {}是个字典。为什么要加个前缀**呢?这个**换个模样估计就看懂了:

def foo(**kwargs):
print(kwargs) >>> foo(x=1, y=2)
{'x': 1, 'y': 2} >>> foo(**{'x': 1, 'y': 2})
{'x': 1, 'y': 2}

self.__variables.update(variables)里面的upate方法是字典添加到字典里,比如:

tinydict = {'Name': 'Zara', 'Age': 7}
tinydict2 = {'Sex': 'female' } tinydict.update(tinydict2) # {'Age': 7, 'Name': 'Zara', 'Sex': 'female'}

整个方法的意思就是从variables中取出variables字典,然后添加到self.__variables字典里。

这个Config会在runner.py里面的HttpRunner类初始化时加载到self.__config

def __init_tests__(self) -> NoReturn:
self.__config = self.config.perform()
self.__teststeps = []
for step in self.teststeps:
self.__teststeps.append(step.perform())

然后self.__config会在各个地方调用。比如在__run_step_request中把base_url拼装起来。:

# 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", {})

而接下来的代码是真把我看晕了。

第一个问题:config里面的变量是怎么用到测试步骤里面的?

答案就是:

step.variables = merge_variables(step.variables, self.__config.variables)

通过merge_variables函数合并到了step.variables,step是下面这个类的实例:

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] = []

step.variables在run_testcase里面赋值:

  • 第一部分是把前面步骤提取的变量合并进来。
  • 第二部分是把用例配置里面的变量合并进来,这就是第一个问题的答案。

第二个问题:变量是怎么提取出来的?

先看看RequestWithOptionalArgs类的extract方法:

def extract(self) -> StepRequestExtraction:
return StepRequestExtraction(self.__step_context)
class StepRequestExtraction(object):
def __init__(self, step_context: TStep):
self.__step_context = step_context def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepRequestExtraction":
self.__step_context.extract[var_name] = jmes_path
return self # def with_regex(self):
# # TODO: extract response html with regex
# pass
#
# def with_jsonpath(self):
# # TODO: extract response json with jsonpath
# pass def validate(self) -> StepRequestValidation:
return StepRequestValidation(self.__step_context) def perform(self) -> TStep:
return self.__step_context

这就是在测试脚本中用到的extract()和with_jmespath()。

可以看到作者这里写了TODO支持正则表达式和JsonPath表达式。

然后把变量名和JmesPath表达式存入了self.__step_context.extract中,这会用在:

从而传入另外这个ResponseObject类的extract方法:

然后self._search_jmespath根据表达式把值找到:

def _search_jmespath(self, expr: Text) -> Any:
resp_obj_meta = {
"status_code": self.status_code,
"headers": self.headers,
"cookies": self.cookies,
"body": self.body,
}
if not expr.startswith(tuple(resp_obj_meta.keys())):
return expr try:
check_value = jmespath.search(expr, resp_obj_meta)
except JMESPathError as ex:
logger.error(
f"failed to search with jmespath\n"
f"expression: {expr}\n"
f"data: {resp_obj_meta}\n"
f"exception: {ex}"
)
raise return check_value

存入extract_mapping中:

再存入step_data.export_vars

然后在__run_step中返回:

最后通过extracted_variables存入到self.__session_variables中:

self.__session_variables是runner.py模块中HttpRunne类的属性,可以理解为一个session级别的变量池。

第三个问题:为什么用$就能直接使用变量?

在run_testcase方法中有一段代码,解析变量:

# parse variables
step.variables = parse_variables_mapping(
step.variables, self.__project_meta.functions
)

parse_variables_mapping的定义如下:

def parse_variables_mapping(
variables_mapping: VariablesMapping, functions_mapping: FunctionsMapping = None
) -> VariablesMapping: parsed_variables: VariablesMapping = {} while len(parsed_variables) != len(variables_mapping):
for var_name in variables_mapping: if var_name in parsed_variables:
continue var_value = variables_mapping[var_name]
variables = extract_variables(var_value) # check if reference variable itself
if var_name in variables:
# e.g.
# variables_mapping = {"token": "abc$token"}
# variables_mapping = {"key": ["$key", 2]}
raise exceptions.VariableNotFound(var_name) # check if reference variable not in variables_mapping
not_defined_variables = [
v_name for v_name in variables if v_name not in variables_mapping
]
if not_defined_variables:
# e.g. {"varA": "123$varB", "varB": "456$varC"}
# e.g. {"varC": "${sum_two($a, $b)}"}
raise exceptions.VariableNotFound(not_defined_variables) try:
parsed_value = parse_data(
var_value, parsed_variables, functions_mapping
)
except exceptions.VariableNotFound:
continue parsed_variables[var_name] = parsed_value return parsed_variables

非常的复杂。其中有个函数extract_variables:

def extract_variables(content: Any) -> Set:
""" extract all variables in content recursively.
"""
if isinstance(content, (list, set, tuple)):
variables = set()
for item in content:
variables = variables | extract_variables(item)
return variables elif isinstance(content, dict):
variables = set()
for key, value in content.items():
variables = variables | extract_variables(value)
return variables elif isinstance(content, str):
return set(regex_findall_variables(content)) return set()

里面有个regex_findall_variables函数:

def regex_findall_variables(raw_string: Text) -> List[Text]:
try:
match_start_position = raw_string.index("$", 0)
except ValueError:
return [] vars_list = []
while match_start_position < len(raw_string): # Notice: notation priority
# $$ > $var # search $$
dollar_match = dolloar_regex_compile.match(raw_string, match_start_position)
if dollar_match:
match_start_position = dollar_match.end()
continue # search variable like ${var} or $var
var_match = variable_regex_compile.match(raw_string, match_start_position)
if var_match:
var_name = var_match.group(1) or var_match.group(2)
vars_list.append(var_name)
match_start_position = var_match.end()
continue curr_position = match_start_position
try:
# find next $ location
match_start_position = raw_string.index("$", curr_position + 1)
except ValueError:
# break while loop
break return vars_list

在这个函数中看到了$符号的身影,就是在这里解析的了。而整个解析过程那是相当的复杂,没有用现成的包,而是作者自己实现的。并且还有个长达548行的parser_test.py测试代码,要说清楚,估计得另外再写一篇专项文章了。

HttpRunner3的变量是如何传递的的更多相关文章

  1. JavaScript传递变量:值传递?引用传递?

    今天在看 seajs-2.2.1/src/util-events.js源码,里面有段代码不是很理解: var events = data.events = {} // Bind event seajs ...

  2. java变量和javascript变量之间的传递

    java代码是运行在服务器上的,返回的值只是静态的存在于页面,而javascript是脚本语言,是运行在客户端的!它们是两种完全不同的语言,变量不能直接进行交换,但是通过一定的方式,变量可以传递. j ...

  3. Matlab M文件变量检测与传递

    M文件中变量的检测与传递 可变数量的输入输出变量(varargin,vararout): Matlab的输入输出变量数量可变,并具有以下特点: 可变输入输出变量必须在正常变量之后 varargin和v ...

  4. OpenCV-Python setMouseCallback回调函数中图像变量img的传递方法解析

    ☞ ░ 前往老猿Python博文目录 ░ 一.使用全局变量进行变量传递 OpenCV-Python中可以使用setMouseCallback来设置鼠标事件的回调函数,我们来看个样例. 1.1.案例1代 ...

  5. 变量的复制&传递

    变量的复制 变量的类型 可以分为基本数据类型(Null.Undefined.Number.String.Boolean)和引用类型(Funtion.Object.Array) 基本数据类型是按照值访问 ...

  6. 环境变量LD_LIBRARY_PATH的传递

    http://bbs.chinaunix.net/thread-3680861-1-1.html execv明显没有传环境变量,execle或execve才会带在启动shell设置的LD_LIBRAR ...

  7. 关于变量参数的传递,python让人蛋痛的地方

    def find_file(file_table): with open(file_table, 'r', encoding='utf-8') as read_f: line_dict = {} fo ...

  8. Matlab中调用VS编译的exe文件并传递变量 的方法

    经历::在网上找了很多方法,都没有实现在matlab中调用vs的exe文件并且能够传递变量参数,一些小细节花费了自己很多时间,比喻忽略了一些空格!  网上很多的方法都是纯粹复制别人的方法,自己都没有去 ...

  9. jade变量声明和数据传递

    声明一个变量 - var course = 'jade'; 取得一个变量 #{course} 大括号里面写入变量命,前面加个#号就可以取得变量 在括号里面可以进行诸多都运行操作,比如大小写 #{cou ...

随机推荐

  1. 小迪安全 Web安全 基础入门 - 第十天 - 信息打点-APP&小程序篇&抓包封包&XP框架&反编译&资产提取

    一.本节知识点思维导图 二.APP-外在资产收集 1.将APP安装在模拟器中,修改模拟器代理设置,使用Fiddler.Burpsuite.Charles等抓包工具抓取APP访问的http协议数据包,抓 ...

  2. xfs文件系统修复方法https://blog.csdn.net/yuanfang_way/article/details/78700089

    首先尝试mount和umount文件系统,以便重放日志,修复文件系统,如果不行,再进行如下操作. 1.检查文件系统:先确保umount xfs_check /dev/sdd(盘符); echo $?  ...

  3. 当ligerui的grid出现固定列与非固定列不在同一水平线上时,改怎么处理

    当ligerui的grid出现固定列与非固定列不在同一水平线上时,如下图所示: 此时可以增加fixedCellHeight:false属性进行解决.这个属性在IE上不起作用,那么该怎么处理,可以这样处 ...

  4. c++内存分布之虚函数(多继承)

    系列 c++内存分布之虚函数(单一继承) c++内存分布之虚函数(多继承) [本文] 结论 1.虚函数表指针 和 虚函数表 1.1 影响虚函数表指针个数的因素只和派生类的父类个数有关.多一个父类,派生 ...

  5. 【剑指Offer】把二叉树打印成多行 解题报告(Python)

    [剑指Offer]把二叉树打印成多行 解题报告(Python) 标签(空格分隔): 剑指Offer 题目地址:https://www.nowcoder.com/ta/coding-interviews ...

  6. 1127 - Funny Knapsack

    1127 - Funny Knapsack    PDF (English) Statistics Forum Time Limit: 2 second(s) Memory Limit: 32 MB ...

  7. python学习第五天:python基础(string、list、tuple)

    首先,什么是sequence(序列)操作? 字符串的特性被称为sequence(序列) H o w a r e y o u ? 就好像存储在一个个连续的单元格里面,每个单元格存储一个字符,每个字符就是 ...

  8. Java 计算加几个月之后的时间

    Java 计算加几个月之后的时间 public static Date getAfterMonth(String inputDate,int number) { Calendar c = Calend ...

  9. 常见分布式唯一ID生成策略

    方法一: 用数据库的 auto_increment 来生成 优点: 此方法使用数据库原有的功能,所以相对简单 能够保证唯一性 能够保证递增性 id 之间的步长是固定且可自定义的 缺点: 可用性难以保证 ...

  10. Not All Samples Are Created Equal: Deep Learning with Importance Sampling

    目录 概 主要内容 "代码" Katharopoulos A, Fleuret F. Not All Samples Are Created Equal: Deep Learnin ...