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測試程式碼,要說清楚,估計得另外再寫一篇專項文章了。