How is the variable of HttpRunner3 passed

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.

Added by teeba on Thu, 20 Jan 2022 01:54:13 +0200