HttpRunner3 variables can be added through variables in the use case configuration of the test class, or extract() and with() can be used in the test steps_ Jmespath() is extracted and put into variable x, and then $X is passed to the next interface for use. For example, some test scripts for logging in to the ordering process are as follows:
from httprunner import HttpRunner, Config, Step, RunRequest class TestLoginPay(HttpRunner): config = ( Config("Log in to the order process") .variables( **{ "skuNum": "3" } ) .base_url("http://127.0.0.1:5000") ) teststeps = [ Step( RunRequest("Sign in") .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("Search for products") .get("searchSku?skuName=e-book") .with_headers(**{"token": "$token"}) .extract() .with_jmespath("body.skuId", "skuId") .with_jmespath("body.price", "skuPrice") .validate() .assert_equal("status_code", 200) ), ]
First, let's look at the code of use case configuration:
config = ( Config("Log in to the order process") .variables( **{ "skuNum": "3" } ) .base_url("http://127.0.0.1:5000") )
How did it happen. Config is defined as follows:
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 are defined as:
def variables(self, **variables) -> "Config": self.__variables.update(variables) return self
self.__variables = {} is a dictionary. Why add a prefix * *? This * * may be understood by changing its appearance:
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. The update method in update (variables) is to add the dictionary to the dictionary, for example:
tinydict = {'Name': 'Zara', 'Age': 7} tinydict2 = {'Sex': 'female' } tinydict.update(tinydict2) # {'Age': 7, 'Name': 'Zara', 'Sex': 'female'}
The whole method means to take the variables dictionary from variables and add it to self__ Variables in the dictionary.
This Config will be in the runner The HttpRunner class in. Py is loaded into self. When initializing__ Config:
def __init_tests__(self) -> NoReturn: self.__config = self.config.perform() self.__teststeps = [] for step in self.teststeps: self.__teststeps.append(step.perform())
Then self__ Config will be called everywhere. Like in__ run_ step_ Base in request_ URL to assemble.:
# 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", {})
And the next code really knocked me out.
First question: how are the variables in config used in the test steps?
The answer is:
step.variables = merge_variables(step.variables, self.__config.variables)
Through merge_ The variables function is merged into step Variables and step are instances of the following classes:
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 in run_ Assignment in testcase:
- The first part is to merge the variables extracted in the previous steps.
- The second part is to merge the variables in the use case configuration, which is the answer to the first question.
The second question: how are variables extracted?
First look at the extract method of the RequestWithOptionalArgs class:
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
This is the extract() and with () used in the test script_ jmespath().
You can see that the author wrote here that TODO supports regular expressions and JsonPath expressions.
Then the variable name and JmesPath expression are stored in self__ step_ context. In extract, this will be used in:
So as to pass in the extract method of the ResponseObject class:
Then self_ search_ Jmespath finds the value according to the expression:
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
Save to extract_ In mapping:
Then save to step_data.export_vars:
Then in__ run_ Return in step:
Finally, through extracted_ Save variables to self__ session_ In variables:
self.__session_variables is runner The attribute of HttpRunne class in py module can be understood as a session level variable pool.
Third question: why can variables be used directly with $?
In run_ There is a piece of code in the testcase method to parse variables:
# parse variables step.variables = parse_variables_mapping( step.variables, self.__project_meta.functions )
parse_variables_mapping is defined as follows:
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
Very complicated. There is a function 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()
There's a regex in it_ findall_ Variables function:
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
In this function, I see the figure of the $symbol, which is parsed here. The whole parsing process is quite complex, which is implemented by the author instead of using the ready-made package. There is also a parser of 548 lines_ test. Py test code, to be clear, it is estimated that we have to write another special article.