mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-14 06:47:39 +08:00
Merge pull request #955 from httprunner/dev
## 3.1.3 (2020-07-06) **Added** - feat: implement `parameters` feature **Fixed** - fix: validate with variable or function whose evaluation result is "" or not text - fix: raise TestCaseFormatError if teststep validate invalid - fix: raise TestCaseFormatError if ref testcase is invalid
This commit is contained in:
2
.github/workflows/integration_test.yml
vendored
2
.github/workflows/integration_test.yml
vendored
@@ -3,7 +3,7 @@ name: integration_test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
2
.github/workflows/unittest.yml
vendored
2
.github/workflows/unittest.yml
vendored
@@ -3,7 +3,7 @@ name: unittest
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# Release History
|
||||
|
||||
## 3.1.3 (2020-07-06)
|
||||
|
||||
**Added**
|
||||
|
||||
- feat: implement `parameters` feature
|
||||
|
||||
**Fixed**
|
||||
|
||||
- fix: validate with variable or function whose evaluation result is "" or not text
|
||||
- fix: raise TestCaseFormatError if teststep validate invalid
|
||||
- fix: raise TestCaseFormatError if ref testcase is invalid
|
||||
|
||||
## 3.1.2 (2020-06-29)
|
||||
|
||||
**Fixed**
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.2
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: basic.yml
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseBasic(HttpRunner):
|
||||
|
||||
config = Config("basic test with httpbin").base_url("https://httpbin.org/")
|
||||
|
||||
teststeps = [
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.2
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: hooks.yml
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseHooks(HttpRunner):
|
||||
|
||||
config = Config("basic test with httpbin").base_url("${get_httpbin_server()}")
|
||||
|
||||
teststeps = [
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.2
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: load_image.yml
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseLoadImage(HttpRunner):
|
||||
|
||||
config = Config("load images").base_url("${get_httpbin_server()}")
|
||||
|
||||
teststeps = [
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.2
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: upload.yml
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseUpload(HttpRunner):
|
||||
|
||||
config = Config("test upload file with httpbin").base_url("${get_httpbin_server()}")
|
||||
|
||||
teststeps = [
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.2
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: validate.yml
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseValidate(HttpRunner):
|
||||
|
||||
config = Config("basic test with httpbin").base_url("http://httpbin.org/")
|
||||
|
||||
teststeps = [
|
||||
|
||||
@@ -4,6 +4,7 @@ from httprunner import __version__
|
||||
def get_httprunner_version():
|
||||
return __version__
|
||||
|
||||
|
||||
def sum_two(m, n):
|
||||
return m + n
|
||||
|
||||
@@ -18,3 +19,7 @@ def get_testsuite_config_variables():
|
||||
|
||||
def get_app_version():
|
||||
return [3.1, 3.0]
|
||||
|
||||
|
||||
def calculate_two_nums(a, b=1):
|
||||
return [a + b, b - a]
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.2
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: request_methods/request_with_functions.yml
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseRequestWithFunctions(HttpRunner):
|
||||
|
||||
config = (
|
||||
Config("request with functions")
|
||||
.variables(
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.2
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: request_methods/request_with_testcase_reference.yml
|
||||
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
from request_methods.request_with_functions_test import (
|
||||
@@ -14,6 +16,7 @@ from request_methods.request_with_functions_test import (
|
||||
|
||||
|
||||
class TestCaseRequestWithTestcaseReference(HttpRunner):
|
||||
|
||||
config = (
|
||||
Config("request with referenced testcase")
|
||||
.variables(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.2
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: request_methods/hardcode.yml
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseHardcode(HttpRunner):
|
||||
|
||||
config = (
|
||||
Config("request methods testcase in hardcode")
|
||||
.base_url("https://postman-echo.com")
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.2
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: request_methods/request_with_functions.yml
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseRequestWithFunctions(HttpRunner):
|
||||
|
||||
config = (
|
||||
Config("request methods testcase with functions")
|
||||
.variables(
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: request_methods/request_with_parameters.yml
|
||||
|
||||
|
||||
import pytest
|
||||
from httprunner import Parameters
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseRequestWithParameters(HttpRunner):
|
||||
@pytest.mark.parametrize(
|
||||
"param",
|
||||
Parameters(
|
||||
{
|
||||
"user_agent": ["iOS/10.1", "iOS/10.2"],
|
||||
"username-password": "${parameterize(request_methods/account.csv)}",
|
||||
"app_version": "${get_app_version()}",
|
||||
}
|
||||
),
|
||||
)
|
||||
def test_start(self, param):
|
||||
super().test_start(param)
|
||||
|
||||
config = (
|
||||
Config("request methods testcase: validate with parameters")
|
||||
.variables(**{"app_version": "f1"})
|
||||
.base_url("https://postman-echo.com")
|
||||
.verify(False)
|
||||
)
|
||||
|
||||
teststeps = [
|
||||
Step(
|
||||
RunRequest("get with params")
|
||||
.with_variables(
|
||||
**{
|
||||
"foo1": "$username",
|
||||
"foo2": "$password",
|
||||
"sum_v": "${sum_two(1, $app_version)}",
|
||||
}
|
||||
)
|
||||
.get("/get")
|
||||
.with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"})
|
||||
.with_headers(**{"User-Agent": "$user_agent,$app_version"})
|
||||
.extract()
|
||||
.with_jmespath("body.args.foo2", "session_foo2")
|
||||
.validate()
|
||||
.assert_equal("status_code", 200)
|
||||
.assert_string_equals("body.args.sum_v", "${sum_two(1, $app_version)}")
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
TestCaseRequestWithParameters().test_start()
|
||||
@@ -1,11 +1,13 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.2
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: request_methods/request_with_testcase_reference.yml
|
||||
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
from request_methods.request_with_functions_test import (
|
||||
@@ -14,6 +16,7 @@ from request_methods.request_with_functions_test import (
|
||||
|
||||
|
||||
class TestCaseRequestWithTestcaseReference(HttpRunner):
|
||||
|
||||
config = (
|
||||
Config("request methods testcase: reference testcase")
|
||||
.variables(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.2
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: request_methods/request_with_variables.yml
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseRequestWithVariables(HttpRunner):
|
||||
|
||||
config = (
|
||||
Config("request methods testcase with variables")
|
||||
.variables(**{"foo1": "testcase_config_bar1", "foo2": "testcase_config_bar2"})
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.2
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: request_methods/validate_with_functions.yml
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseValidateWithFunctions(HttpRunner):
|
||||
|
||||
config = (
|
||||
Config("request methods testcase: validate with functions")
|
||||
.variables(**{"foo1": "session_bar1"})
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# NOTE: Generated By HttpRunner v3.1.2
|
||||
# NOTE: Generated By HttpRunner v3.1.3
|
||||
# FROM: request_methods/validate_with_variables.yml
|
||||
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
|
||||
|
||||
class TestCaseValidateWithVariables(HttpRunner):
|
||||
|
||||
config = (
|
||||
Config("request methods testcase: validate with variables")
|
||||
.variables(**{"foo1": "session_bar1"})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
__version__ = "3.1.2"
|
||||
__version__ = "3.1.3"
|
||||
__description__ = "One-stop solution for HTTP(S) testing."
|
||||
|
||||
# import firstly for monkey patch if needed
|
||||
from httprunner.ext.locust import main_locusts
|
||||
from httprunner.parser import parse_parameters as Parameters
|
||||
from httprunner.runner import HttpRunner
|
||||
from httprunner.testcase import Config, Step, RunRequest, RunTestCase
|
||||
|
||||
@@ -14,4 +15,5 @@ __all__ = [
|
||||
"Step",
|
||||
"RunRequest",
|
||||
"RunTestCase",
|
||||
"Parameters",
|
||||
]
|
||||
|
||||
@@ -182,6 +182,10 @@ def _ensure_step_attachment(step: Dict) -> Dict:
|
||||
test_dict["export"] = step["export"]
|
||||
|
||||
if "validate" in step:
|
||||
if not isinstance(step["validate"], List):
|
||||
raise exceptions.TestCaseFormatError(
|
||||
f'Invalid teststep validate: {step["validate"]}'
|
||||
)
|
||||
test_dict["validate"] = _convert_validators(step["validate"])
|
||||
|
||||
if "validate_script" in step:
|
||||
|
||||
@@ -251,7 +251,12 @@ class HarParser(object):
|
||||
|
||||
encoding = resp_content_dict.get("encoding")
|
||||
if encoding and encoding == "base64":
|
||||
content = base64.b64decode(text).decode("utf-8")
|
||||
content = base64.b64decode(text)
|
||||
try:
|
||||
content = content.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
logger.warning(f"failed to decode base64 content with utf-8 !")
|
||||
return
|
||||
else:
|
||||
content = text
|
||||
|
||||
|
||||
@@ -37,23 +37,31 @@ pytest_files_run_set: Set = set()
|
||||
__TEMPLATE__ = jinja2.Template(
|
||||
"""# NOTE: Generated By HttpRunner v{{ version }}
|
||||
# FROM: {{ testcase_path }}
|
||||
|
||||
{% if imports_list and diff_levels > 0 %}
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__)
|
||||
{% for _ in range(diff_levels) %}
|
||||
.parent
|
||||
{% endfor %}
|
||||
))
|
||||
sys.path.insert(0, str(Path(__file__){% for _ in range(diff_levels) %}.parent{% endfor %}))
|
||||
{% endif %}
|
||||
|
||||
{% if parameters %}
|
||||
import pytest
|
||||
from httprunner import Parameters
|
||||
{% endif %}
|
||||
|
||||
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
|
||||
{% for import_str in imports_list %}
|
||||
{{ import_str }}
|
||||
{% endfor %}
|
||||
|
||||
class {{ class_name }}(HttpRunner):
|
||||
{{ customization_test_start }}
|
||||
|
||||
{% if parameters %}
|
||||
@pytest.mark.parametrize("param", Parameters({{parameters}}))
|
||||
def test_start(self, param):
|
||||
super().test_start(param)
|
||||
{% endif %}
|
||||
|
||||
config = {{ config_chain_style }}
|
||||
|
||||
teststeps = [
|
||||
@@ -367,6 +375,9 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
|
||||
ref_testcase_path = __ensure_absolute(teststep["testcase"])
|
||||
test_content = load_test_file(ref_testcase_path)
|
||||
|
||||
if not isinstance(test_content, Dict):
|
||||
raise exceptions.TestCaseFormatError(f"Invalid teststep: {teststep}")
|
||||
|
||||
# api in v2 format, convert to v3 testcase
|
||||
if "request" in test_content and "name" in test_content:
|
||||
test_content = ensure_testcase_v3_api(test_content)
|
||||
@@ -408,7 +419,7 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
|
||||
"class_name": f"TestCase{testcase_cls_name}",
|
||||
"imports_list": imports_list,
|
||||
"config_chain_style": make_config_chain_style(config),
|
||||
"customization_test_start": make_test_start_style(config),
|
||||
"parameters": config.get("parameters"),
|
||||
"teststeps_chain_style": [
|
||||
make_teststep_chain_style(step) for step in teststeps
|
||||
],
|
||||
@@ -608,22 +619,3 @@ def init_make_parser(subparsers):
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def make_test_start_style(config: Dict) -> Text:
|
||||
test_start_style = ""
|
||||
if "parameters" in config.keys():
|
||||
params = config["parameters"]
|
||||
test_start_style = f"""
|
||||
import pytest
|
||||
from httprunner.parser import parse_parameters
|
||||
|
||||
param = [{params}]
|
||||
|
||||
@pytest.mark.parametrize('parametrize', parse_parameters(param))
|
||||
def test_start(self, parametrize):
|
||||
super().test_start(parametrize)
|
||||
"""
|
||||
else:
|
||||
pass
|
||||
return test_start_style
|
||||
@@ -1,7 +1,7 @@
|
||||
import ast
|
||||
import builtins
|
||||
import re
|
||||
from typing import Any, Set, Text, Callable, List, Dict
|
||||
from typing import Any, Set, Text, Callable, List, Dict, Union
|
||||
|
||||
from loguru import logger
|
||||
from sentry_sdk import capture_exception
|
||||
@@ -465,51 +465,44 @@ def parse_variables_mapping(
|
||||
return parsed_variables
|
||||
|
||||
|
||||
def parse_parameters(parameters, variables_mapping=None, functions_mapping=None):
|
||||
def parse_parameters(parameters: Dict,) -> List[Dict]:
|
||||
""" parse parameters and generate cartesian product.
|
||||
|
||||
Args:
|
||||
parameters (list) parameters: parameter name and value in list
|
||||
parameters (Dict) parameters: parameter name and value mapping
|
||||
parameter value may be in three types:
|
||||
(1) data list, e.g. ["iOS/10.1", "iOS/10.2", "iOS/10.3"]
|
||||
(2) call built-in parameterize function, "${parameterize(account.csv)}"
|
||||
(3) call custom function in debugtalk.py, "${gen_app_version()}"
|
||||
|
||||
variables_mapping (dict): variables mapping loaded from testcase config
|
||||
functions_mapping (dict): functions mapping loaded from debugtalk.py
|
||||
|
||||
Returns:
|
||||
list: cartesian product list
|
||||
|
||||
Examples:
|
||||
>>> parameters = [
|
||||
{"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"]},
|
||||
{"username-password": "${parameterize(account.csv)}"},
|
||||
{"app_version": "${gen_app_version()}"}
|
||||
]
|
||||
>>> parameters = {
|
||||
"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"],
|
||||
"username-password": "${parameterize(account.csv)}",
|
||||
"app_version": "${gen_app_version()}",
|
||||
}
|
||||
>>> parse_parameters(parameters)
|
||||
|
||||
"""
|
||||
variables_mapping = variables_mapping or {}
|
||||
functions_mapping = functions_mapping or {}
|
||||
parsed_parameters_list = []
|
||||
parsed_parameters_list: List[List[Dict]] = []
|
||||
|
||||
# load project_meta functions
|
||||
from httprunner.loader import load_project_meta
|
||||
project_meta = load_project_meta("")
|
||||
functions_mapping.update(project_meta.functions)
|
||||
project_meta = loader.load_project_meta("")
|
||||
functions_mapping = project_meta.functions
|
||||
|
||||
parameters = utils.ensure_mapping_format(parameters)
|
||||
for parameter_name, parameter_content in parameters.items():
|
||||
parameter_name_list = parameter_name.split("-")
|
||||
|
||||
if isinstance(parameter_content, list):
|
||||
if isinstance(parameter_content, List):
|
||||
# (1) data list
|
||||
# e.g. {"app_version": ["2.8.5", "2.8.6"]}
|
||||
# => [{"app_version": "2.8.5", "app_version": "2.8.6"}]
|
||||
# e.g. {"username-password": [["user1", "111111"], ["test2", "222222"]}
|
||||
# => [{"username": "user1", "password": "111111"}, {"username": "user2", "password": "222222"}]
|
||||
parameter_content_list = []
|
||||
parameter_content_list: List[Dict] = []
|
||||
for parameter_item in parameter_content:
|
||||
if not isinstance(parameter_item, (list, tuple)):
|
||||
# "2.8.5" => ["2.8.5"]
|
||||
@@ -518,25 +511,21 @@ def parse_parameters(parameters, variables_mapping=None, functions_mapping=None)
|
||||
# ["app_version"], ["2.8.5"] => {"app_version": "2.8.5"}
|
||||
# ["username", "password"], ["user1", "111111"] => {"username": "user1", "password": "111111"}
|
||||
parameter_content_dict = dict(zip(parameter_name_list, parameter_item))
|
||||
|
||||
parameter_content_list.append(parameter_content_dict)
|
||||
else:
|
||||
# (2) & (3)
|
||||
parsed_variables_mapping = parse_variables_mapping(
|
||||
variables_mapping,
|
||||
functions_mapping
|
||||
)
|
||||
parsed_parameter_content = parse_data(
|
||||
parameter_content,
|
||||
parsed_variables_mapping,
|
||||
functions_mapping
|
||||
)
|
||||
if not isinstance(parsed_parameter_content, list):
|
||||
raise exceptions.ParamsError(f"{parsed_parameter_content} parameters syntax error!")
|
||||
|
||||
parameter_content_list = []
|
||||
elif isinstance(parameter_content, Text):
|
||||
# (2) & (3)
|
||||
parsed_parameter_content: List = parse_data(
|
||||
parameter_content, {}, functions_mapping
|
||||
)
|
||||
if not isinstance(parsed_parameter_content, List):
|
||||
raise exceptions.ParamsError(
|
||||
f"parameters content should be in List type, got {parsed_parameter_content} for {parameter_content}"
|
||||
)
|
||||
|
||||
parameter_content_list: List[Dict] = []
|
||||
for parameter_item in parsed_parameter_content:
|
||||
if isinstance(parameter_item, dict):
|
||||
if isinstance(parameter_item, Dict):
|
||||
# get subset by parameter name
|
||||
# {"app_version": "${gen_app_version()}"}
|
||||
# gen_app_version() => [{'app_version': '2.8.5'}, {'app_version': '2.8.6'}]
|
||||
@@ -545,20 +534,39 @@ def parse_parameters(parameters, variables_mapping=None, functions_mapping=None)
|
||||
# {"username": "user1", "password": "111111"},
|
||||
# {"username": "user2", "password": "222222"}
|
||||
# ]
|
||||
parameter_dict = {key: parameter_item[key] for key in parameter_name_list}
|
||||
# elif isinstance(parameter_item, (list, tuple)):
|
||||
# # {"username-password": "${get_account()}"}
|
||||
# # get_account() => [("user1", "111111"), ("user2", "222222")]
|
||||
# parameter_dict = dict(zip(parameter_name_list, parameter_item))
|
||||
parameter_dict: Dict = {
|
||||
key: parameter_item[key] for key in parameter_name_list
|
||||
}
|
||||
elif isinstance(parameter_item, (List, tuple)):
|
||||
if len(parameter_name_list) == len(parameter_item):
|
||||
# {"username-password": "${get_account()}"}
|
||||
# get_account() => [("user1", "111111"), ("user2", "222222")]
|
||||
parameter_dict = dict(zip(parameter_name_list, parameter_item))
|
||||
else:
|
||||
raise exceptions.ParamsError(
|
||||
f"parameter names length are not equal to value length.\n"
|
||||
f"parameter names: {parameter_name_list}\n"
|
||||
f"parameter values: {parameter_item}"
|
||||
)
|
||||
elif len(parameter_name_list) == 1:
|
||||
# {"user_agent": "${get_user_agent()}"}
|
||||
# get_user_agent() => ["iOS/10.1", "iOS/10.2"]
|
||||
parameter_dict = {
|
||||
parameter_name_list[0]: parameter_item
|
||||
}
|
||||
# parameter_dict will get: {"user_agent": "iOS/10.1", "user_agent": "iOS/10.2"}
|
||||
parameter_dict = {parameter_name_list[0]: parameter_item}
|
||||
else:
|
||||
raise exceptions.ParamsError(
|
||||
f"Invalid parameter names and values:\n"
|
||||
f"parameter names: {parameter_name_list}\n"
|
||||
f"parameter values: {parameter_item}"
|
||||
)
|
||||
|
||||
parameter_content_list.append(parameter_dict)
|
||||
|
||||
else:
|
||||
raise exceptions.ParamsError(
|
||||
f"parameter content should be List or Text(variables or functions call), got {parameter_content}"
|
||||
)
|
||||
|
||||
parsed_parameters_list.append(parameter_content_list)
|
||||
|
||||
return utils.gen_cartesian_product(*parsed_parameters_list)
|
||||
|
||||
@@ -2,12 +2,13 @@ from typing import Dict, Text, Any, NoReturn
|
||||
|
||||
import jmespath
|
||||
import requests
|
||||
from jmespath.exceptions import JMESPathError
|
||||
from loguru import logger
|
||||
|
||||
from httprunner import exceptions
|
||||
from httprunner.exceptions import ValidationFailure, ParamsError
|
||||
from httprunner.parser import parse_data, parse_string_value, get_mapping_function
|
||||
from httprunner.models import VariablesMapping, Validators, FunctionsMapping
|
||||
from httprunner.parser import parse_data, parse_string_value, get_mapping_function
|
||||
|
||||
|
||||
def get_uniform_comparator(comparator: Text):
|
||||
@@ -143,14 +144,25 @@ class ResponseObject(object):
|
||||
self.__dict__[key] = value
|
||||
return value
|
||||
|
||||
@property
|
||||
def resp_obj_meta(self):
|
||||
return {
|
||||
def _search_jmespath(self, expr: Text) -> Any:
|
||||
resp_obj_meta = {
|
||||
"status_code": self.status_code,
|
||||
"headers": self.headers,
|
||||
"cookies": self.cookies,
|
||||
"body": self.body,
|
||||
}
|
||||
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
|
||||
|
||||
def extract(self, extractors: Dict[Text, Text]) -> Dict[Text, Any]:
|
||||
if not extractors:
|
||||
@@ -158,7 +170,7 @@ class ResponseObject(object):
|
||||
|
||||
extract_mapping = {}
|
||||
for key, field in extractors.items():
|
||||
field_value = jmespath.search(field, self.resp_obj_meta)
|
||||
field_value = self._search_jmespath(field)
|
||||
extract_mapping[key] = field_value
|
||||
|
||||
logger.info(f"extract mapping: {extract_mapping}")
|
||||
@@ -197,7 +209,11 @@ class ResponseObject(object):
|
||||
)
|
||||
check_item = parse_string_value(check_item)
|
||||
|
||||
check_value = jmespath.search(check_item, self.resp_obj_meta)
|
||||
if check_item and isinstance(check_item, Text):
|
||||
check_value = self._search_jmespath(check_item)
|
||||
else:
|
||||
# variable or function evaluation result is "" or not text
|
||||
check_value = check_item
|
||||
|
||||
# comparator
|
||||
assert_method = u_validator["assert"]
|
||||
|
||||
@@ -421,7 +421,7 @@ class HttpRunner(object):
|
||||
step_datas=self.__step_datas,
|
||||
)
|
||||
|
||||
def test_start(self, parametrize=None) -> "HttpRunner":
|
||||
def test_start(self, param: Dict = None) -> "HttpRunner":
|
||||
"""main entrance, discovered by pytest"""
|
||||
self.__init_tests__()
|
||||
self.__project_meta = self.__project_meta or load_project_meta(
|
||||
@@ -435,8 +435,8 @@ class HttpRunner(object):
|
||||
|
||||
# parse config name
|
||||
config_variables = self.__config.variables
|
||||
if parametrize:
|
||||
config_variables.update(parametrize)
|
||||
if param:
|
||||
config_variables.update(param)
|
||||
config_variables.update(self.__session_variables)
|
||||
self.__config.name = parse_data(
|
||||
self.__config.name, config_variables, self.__project_meta.functions
|
||||
|
||||
@@ -159,7 +159,7 @@ class StepRequestValidation(object):
|
||||
return self
|
||||
|
||||
def assert_string_equals(
|
||||
self, jmes_path: Text, expected_value: int, message: Text = ""
|
||||
self, jmes_path: Text, expected_value: Any, message: Text = ""
|
||||
) -> "StepRequestValidation":
|
||||
self.__step_context.validators.append(
|
||||
{"string_equals": [jmes_path, expected_value, message]}
|
||||
|
||||
@@ -6,7 +6,7 @@ import platform
|
||||
import uuid
|
||||
from multiprocessing import Queue
|
||||
import itertools
|
||||
from typing import Dict, List, Any
|
||||
from typing import Dict, List, Any, Union, Text
|
||||
|
||||
import sentry_sdk
|
||||
from loguru import logger
|
||||
@@ -223,42 +223,7 @@ def is_support_multiprocessing() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def ensure_mapping_format(variables):
|
||||
""" ensure variables are in mapping format.
|
||||
|
||||
Args:
|
||||
variables (list/dict): original variables
|
||||
|
||||
Returns:
|
||||
dict: ensured variables in dict format
|
||||
|
||||
Examples:
|
||||
>>> variables = [
|
||||
{"a": 1},
|
||||
{"b": 2}
|
||||
]
|
||||
>>> print(ensure_mapping_format(variables))
|
||||
{
|
||||
"a": 1,
|
||||
"b": 2
|
||||
}
|
||||
|
||||
"""
|
||||
if isinstance(variables, list):
|
||||
variables_dict = {}
|
||||
for map_dict in variables:
|
||||
variables_dict.update(map_dict)
|
||||
|
||||
return variables_dict
|
||||
|
||||
elif isinstance(variables, dict):
|
||||
return variables
|
||||
|
||||
else:
|
||||
raise exceptions.ParamsError("variables format error!")
|
||||
|
||||
|
||||
def gen_cartesian_product(*args):
|
||||
def gen_cartesian_product(*args: List[Dict]) -> List[Dict]:
|
||||
""" generate cartesian product for lists
|
||||
|
||||
Args:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "httprunner"
|
||||
version = "3.1.2"
|
||||
version = "3.1.3"
|
||||
description = "One-stop solution for HTTP(S) testing."
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from httprunner import loader
|
||||
from httprunner.make import (
|
||||
main_make,
|
||||
convert_testcase_path,
|
||||
@@ -9,9 +10,7 @@ from httprunner.make import (
|
||||
make_teststep_chain_style,
|
||||
pytest_files_run_set,
|
||||
ensure_file_abs_path_valid,
|
||||
make_test_start_style
|
||||
)
|
||||
from httprunner import loader
|
||||
|
||||
|
||||
class TestMake(unittest.TestCase):
|
||||
@@ -215,26 +214,3 @@ from request_methods.request_with_functions_test import (
|
||||
teststep_chain_style,
|
||||
"""Step(RunRequest("get with params").with_variables(**{'foo1': 'bar1', 'foo2': 123, 'sum_v': '${sum_two(1, 2)}'}).get("/get").with_params(**{'foo1': '$foo1', 'foo2': '$foo2', 'sum_v': '$sum_v'}).with_headers(**{'User-Agent': 'HttpRunner/${get_httprunner_version()}'}).extract().with_jmespath('body.args.foo1', 'session_foo1').with_jmespath('body.args.foo2', 'session_foo2').validate().assert_equal("status_code", 200).assert_equal("body.args.sum_v", "3"))""",
|
||||
)
|
||||
|
||||
def test_make_test_start_style(self):
|
||||
params = {
|
||||
"user_agent": ["iOS/10.1", "iOS/10.2"],
|
||||
"username-password": "${parameterize(request_methods/account.csv)}",
|
||||
"app_version": "${get_app_version()}",
|
||||
}
|
||||
config = {
|
||||
"parameters": params
|
||||
}
|
||||
self.assertEqual(
|
||||
make_test_start_style(config),
|
||||
f"""
|
||||
import pytest
|
||||
from httprunner.parser import parse_parameters
|
||||
|
||||
param = [{params}]
|
||||
|
||||
@pytest.mark.parametrize('parametrize', parse_parameters(param))
|
||||
def test_start(self, parametrize):
|
||||
super().test_start(parametrize)
|
||||
"""
|
||||
)
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from httprunner import parser
|
||||
from httprunner.exceptions import VariableNotFound, FunctionNotFound
|
||||
from httprunner.parser import regex_findall_variables
|
||||
from httprunner.loader import load_project_meta
|
||||
|
||||
|
||||
class TestParserBasic(unittest.TestCase):
|
||||
@@ -27,17 +28,19 @@ class TestParserBasic(unittest.TestCase):
|
||||
self.assertEqual(parser.parse_string_value("${func}"), "${func}")
|
||||
|
||||
def test_regex_findall_variables(self):
|
||||
self.assertEqual(regex_findall_variables("$variable"), ["variable"])
|
||||
self.assertEqual(regex_findall_variables("${variable}123"), ["variable"])
|
||||
self.assertEqual(regex_findall_variables("/blog/$postid"), ["postid"])
|
||||
self.assertEqual(regex_findall_variables("/$var1/$var2"), ["var1", "var2"])
|
||||
self.assertEqual(regex_findall_variables("abc"), [])
|
||||
self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$a"), ["a"])
|
||||
self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$$a"), [])
|
||||
self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$$$a"), ["a"])
|
||||
self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$$$$a"), [])
|
||||
self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$$a$b"), ["b"])
|
||||
self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$$a$$b"), [])
|
||||
self.assertEqual(parser.regex_findall_variables("$variable"), ["variable"])
|
||||
self.assertEqual(parser.regex_findall_variables("${variable}123"), ["variable"])
|
||||
self.assertEqual(parser.regex_findall_variables("/blog/$postid"), ["postid"])
|
||||
self.assertEqual(
|
||||
parser.regex_findall_variables("/$var1/$var2"), ["var1", "var2"]
|
||||
)
|
||||
self.assertEqual(parser.regex_findall_variables("abc"), [])
|
||||
self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$a"), ["a"])
|
||||
self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$a"), [])
|
||||
self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$$a"), ["a"])
|
||||
self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$$$a"), [])
|
||||
self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$a$b"), ["b"])
|
||||
self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$a$$b"), [])
|
||||
|
||||
def test_extract_variables(self):
|
||||
self.assertEqual(parser.extract_variables("$var"), {"var"})
|
||||
@@ -454,28 +457,91 @@ class TestParserBasic(unittest.TestCase):
|
||||
self.assertEqual(parsed_testcase["headers"]["sum"], 3)
|
||||
|
||||
def test_parse_parameters_testcase(self):
|
||||
variables = {
|
||||
"user_agent": "chrome",
|
||||
"sum": 5,
|
||||
parameters = {
|
||||
"user_agent": ["iOS/10.1", "iOS/10.2"],
|
||||
"username-password": "${parameterize(request_methods/account.csv)}",
|
||||
"sum": "${calculate_two_nums(1, 2)}",
|
||||
}
|
||||
param = [
|
||||
load_project_meta(
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)),
|
||||
"examples",
|
||||
"postman_echo",
|
||||
"request_methods",
|
||||
),
|
||||
)
|
||||
parsed_params = parser.parse_parameters(parameters)
|
||||
self.assertEqual(len(parsed_params), 2 * 3 * 2)
|
||||
|
||||
self.assertIn(
|
||||
{
|
||||
"user_agent": ["iOS/10.1", "iOS/10.2"],
|
||||
"username-password": "${parameterize(request_methods/account.csv)}",
|
||||
"sum": "${add_two_nums(1, 2)}",
|
||||
}
|
||||
]
|
||||
|
||||
functions = {
|
||||
"add_two_nums": lambda a, b=1: [a + b, b-a],
|
||||
}
|
||||
|
||||
parsed_params = parser.parse_parameters(param, variables, functions)
|
||||
self.assertIn({'username': 'test1', 'password': '111111', 'user_agent': 'iOS/10.1', 'sum': 3}, parsed_params)
|
||||
self.assertIn({'username': 'test1', 'password': '111111', 'user_agent': 'iOS/10.1', 'sum': 1}, parsed_params)
|
||||
self.assertIn({'username': 'test1', 'password': '111111', 'user_agent': 'iOS/10.2', 'sum': 3}, parsed_params)
|
||||
self.assertIn({'username': 'test1', 'password': '111111', 'user_agent': 'iOS/10.2', 'sum': 1}, parsed_params)
|
||||
self.assertIn({'username': 'test2', 'password': '222222', 'user_agent': 'iOS/10.1', 'sum': 3}, parsed_params)
|
||||
self.assertIn({'username': 'test2', 'password': '222222', 'user_agent': 'iOS/10.1', 'sum': 1}, parsed_params)
|
||||
self.assertIn({'username': 'test2', 'password': '222222', 'user_agent': 'iOS/10.2', 'sum': 3}, parsed_params)
|
||||
self.assertIn({'username': 'test2', 'password': '222222', 'user_agent': 'iOS/10.2', 'sum': 1}, parsed_params)
|
||||
"username": "test1",
|
||||
"password": "111111",
|
||||
"user_agent": "iOS/10.1",
|
||||
"sum": 3,
|
||||
},
|
||||
parsed_params,
|
||||
)
|
||||
self.assertIn(
|
||||
{
|
||||
"username": "test1",
|
||||
"password": "111111",
|
||||
"user_agent": "iOS/10.1",
|
||||
"sum": 1,
|
||||
},
|
||||
parsed_params,
|
||||
)
|
||||
self.assertIn(
|
||||
{
|
||||
"username": "test1",
|
||||
"password": "111111",
|
||||
"user_agent": "iOS/10.2",
|
||||
"sum": 3,
|
||||
},
|
||||
parsed_params,
|
||||
)
|
||||
self.assertIn(
|
||||
{
|
||||
"username": "test1",
|
||||
"password": "111111",
|
||||
"user_agent": "iOS/10.2",
|
||||
"sum": 1,
|
||||
},
|
||||
parsed_params,
|
||||
)
|
||||
self.assertIn(
|
||||
{
|
||||
"username": "test2",
|
||||
"password": "222222",
|
||||
"user_agent": "iOS/10.1",
|
||||
"sum": 3,
|
||||
},
|
||||
parsed_params,
|
||||
)
|
||||
self.assertIn(
|
||||
{
|
||||
"username": "test2",
|
||||
"password": "222222",
|
||||
"user_agent": "iOS/10.1",
|
||||
"sum": 1,
|
||||
},
|
||||
parsed_params,
|
||||
)
|
||||
self.assertIn(
|
||||
{
|
||||
"username": "test2",
|
||||
"password": "222222",
|
||||
"user_agent": "iOS/10.2",
|
||||
"sum": 3,
|
||||
},
|
||||
parsed_params,
|
||||
)
|
||||
self.assertIn(
|
||||
{
|
||||
"username": "test2",
|
||||
"password": "222222",
|
||||
"user_agent": "iOS/10.2",
|
||||
"sum": 1,
|
||||
},
|
||||
parsed_params,
|
||||
)
|
||||
|
||||
@@ -28,12 +28,28 @@ class TestResponse(unittest.TestCase):
|
||||
self.assertEqual(extract_mapping["var_2"], "Olympia")
|
||||
|
||||
def test_validate(self):
|
||||
variables_mapping = {"index": 1}
|
||||
self.resp_obj.validate(
|
||||
[
|
||||
{"eq": ["body.json.locations[0].name", "Seattle"]},
|
||||
{"eq": ["body.json.locations[0]", {"name": "Seattle", "state": "WA"}]},
|
||||
],
|
||||
)
|
||||
|
||||
def test_validate_variables(self):
|
||||
variables_mapping = {"index": 1, "var_empty": ""}
|
||||
self.resp_obj.validate(
|
||||
[
|
||||
{"eq": ["body.json.locations[$index].name", "New York"]},
|
||||
{"eq": ["$var_empty", ""]},
|
||||
],
|
||||
variables_mapping=variables_mapping,
|
||||
)
|
||||
|
||||
def test_validate_functions(self):
|
||||
variables_mapping = {"index": 1}
|
||||
functions_mapping = {"get_num": lambda x: x}
|
||||
self.resp_obj.validate(
|
||||
[{"eq": ["${get_num(0)}", 0]}, {"eq": ["${get_num($index)}", 1]},],
|
||||
variables_mapping=variables_mapping,
|
||||
functions_mapping=functions_mapping,
|
||||
)
|
||||
|
||||
@@ -126,54 +126,28 @@ class TestUtils(unittest.TestCase):
|
||||
{"base_url": "https://httpbin.org", "foo1": "bar1"},
|
||||
)
|
||||
|
||||
def test_ensure_mapping_format(self):
|
||||
map_list = [
|
||||
{"a": 1},
|
||||
{"b": 2}
|
||||
]
|
||||
ordered_dict = utils.ensure_mapping_format(map_list)
|
||||
self.assertIsInstance(ordered_dict, dict)
|
||||
self.assertIn("a", ordered_dict)
|
||||
|
||||
def test_cartesian_product_one(self):
|
||||
parameters_content_list = [
|
||||
[
|
||||
{"a": 1},
|
||||
{"a": 2}
|
||||
]
|
||||
]
|
||||
parameters_content_list = [[{"a": 1}, {"a": 2}]]
|
||||
product_list = utils.gen_cartesian_product(*parameters_content_list)
|
||||
self.assertEqual(
|
||||
product_list,
|
||||
[
|
||||
{"a": 1},
|
||||
{"a": 2}
|
||||
]
|
||||
)
|
||||
self.assertEqual(product_list, [{"a": 1}, {"a": 2}])
|
||||
|
||||
def test_cartesian_product_multiple(self):
|
||||
parameters_content_list = [
|
||||
[
|
||||
{"a": 1},
|
||||
{"a": 2}
|
||||
],
|
||||
[
|
||||
{"x": 111, "y": 112},
|
||||
{"x": 121, "y": 122}
|
||||
]
|
||||
[{"a": 1}, {"a": 2}],
|
||||
[{"x": 111, "y": 112}, {"x": 121, "y": 122}],
|
||||
]
|
||||
product_list = utils.gen_cartesian_product(*parameters_content_list)
|
||||
self.assertEqual(
|
||||
product_list,
|
||||
[
|
||||
{'a': 1, 'x': 111, 'y': 112},
|
||||
{'a': 1, 'x': 121, 'y': 122},
|
||||
{'a': 2, 'x': 111, 'y': 112},
|
||||
{'a': 2, 'x': 121, 'y': 122}
|
||||
]
|
||||
{"a": 1, "x": 111, "y": 112},
|
||||
{"a": 1, "x": 121, "y": 122},
|
||||
{"a": 2, "x": 111, "y": 112},
|
||||
{"a": 2, "x": 121, "y": 122},
|
||||
],
|
||||
)
|
||||
|
||||
def test_cartesian_product_empty(self):
|
||||
parameters_content_list = []
|
||||
product_list = utils.gen_cartesian_product(*parameters_content_list)
|
||||
self.assertEqual(product_list, [])
|
||||
self.assertEqual(product_list, [])
|
||||
|
||||
Reference in New Issue
Block a user