Merge pull request #952 from WandyYing/issue/#931

Issue/#931 add new feature "paramters"
This commit is contained in:
debugtalk
2020-07-05 13:10:01 +08:00
committed by GitHub
11 changed files with 345 additions and 2 deletions

View File

@@ -4,7 +4,6 @@ from httprunner import __version__
def get_httprunner_version(): def get_httprunner_version():
return __version__ return __version__
def sum_two(m, n): def sum_two(m, n):
return m + n return m + n
@@ -15,3 +14,7 @@ def get_testcase_config_variables():
def get_testsuite_config_variables(): def get_testsuite_config_variables():
return {"foo1": "testsuite_config_bar1", "foo2": "testsuite_config_bar2"} return {"foo1": "testsuite_config_bar1", "foo2": "testsuite_config_bar2"}
def get_app_version():
return [3.1, 3.0]

View File

@@ -0,0 +1,4 @@
username,password
test1,111111
test2,222222
test3,333333
1 username password
2 test1 111111
3 test2 222222
4 test3 333333

View File

@@ -0,0 +1,33 @@
config:
name: "request methods testcase: validate with parameters"
parameters:
user_agent: ["iOS/10.1", "iOS/10.2"]
username-password: ${parameterize(request_methods/account.csv)}
app_version: ${get_app_version()}
variables:
app_version: f1
base_url: "https://postman-echo.com"
verify: False
teststeps:
-
name: get with params
variables:
foo1: $username
foo2: $password
sum_v: "${sum_two(1, $app_version)}"
request:
method: GET
url: /get
params:
foo1: $foo1
foo2: $foo2
sum_v: $sum_v
headers:
User-Agent: $user_agent,$app_version
extract:
session_foo2: "body.args.foo2"
validate:
- eq: ["status_code", 200]
- str_eq: ["body.args.sum_v", "${sum_two(1, $app_version)}"]
# - less_than: ["body.args.sum_v", "${sum_two(2, 2)}"] FIXME: TypeError: '<' not supported between instances of 'str' and 'int'

View File

@@ -53,6 +53,7 @@ from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
{% endfor %} {% endfor %}
class {{ class_name }}(HttpRunner): class {{ class_name }}(HttpRunner):
{{ customization_test_start }}
config = {{ config_chain_style }} config = {{ config_chain_style }}
teststeps = [ teststeps = [
@@ -407,6 +408,7 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
"class_name": f"TestCase{testcase_cls_name}", "class_name": f"TestCase{testcase_cls_name}",
"imports_list": imports_list, "imports_list": imports_list,
"config_chain_style": make_config_chain_style(config), "config_chain_style": make_config_chain_style(config),
"customization_test_start": make_test_start_style(config),
"teststeps_chain_style": [ "teststeps_chain_style": [
make_teststep_chain_style(step) for step in teststeps make_teststep_chain_style(step) for step in teststeps
], ],
@@ -606,3 +608,22 @@ def init_make_parser(subparsers):
) )
return parser 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

View File

@@ -37,6 +37,7 @@ class TConfig(BaseModel):
base_url: BaseUrl = "" base_url: BaseUrl = ""
# Text: prepare variables in debugtalk.py, ${gen_variables()} # Text: prepare variables in debugtalk.py, ${gen_variables()}
variables: Union[VariablesMapping, Text] = {} variables: Union[VariablesMapping, Text] = {}
parameters: Union[VariablesMapping, Text] = {}
# setup_hooks: Hooks = [] # setup_hooks: Hooks = []
# teardown_hooks: Hooks = [] # teardown_hooks: Hooks = []
export: Export = [] export: Export = []

View File

@@ -463,3 +463,102 @@ def parse_variables_mapping(
parsed_variables[var_name] = parsed_value parsed_variables[var_name] = parsed_value
return parsed_variables return parsed_variables
def parse_parameters(parameters, variables_mapping=None, functions_mapping=None):
""" parse parameters and generate cartesian product.
Args:
parameters (list) parameters: parameter name and value in list
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()}"}
]
>>> parse_parameters(parameters)
"""
variables_mapping = variables_mapping or {}
functions_mapping = functions_mapping or {}
parsed_parameters_list = []
# load project_meta functions
from httprunner.loader import load_project_meta
project_meta = load_project_meta("")
functions_mapping.update(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):
# (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 = []
for parameter_item in parameter_content:
if not isinstance(parameter_item, (list, tuple)):
# "2.8.5" => ["2.8.5"]
parameter_item = [parameter_item]
# ["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 = []
for parameter_item in parsed_parameter_content:
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'}]
# {"username-password": "${get_account()}"}
# get_account() => [
# {"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))
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_content_list.append(parameter_dict)
parsed_parameters_list.append(parameter_content_list)
return utils.gen_cartesian_product(*parsed_parameters_list)

View File

@@ -421,7 +421,7 @@ class HttpRunner(object):
step_datas=self.__step_datas, step_datas=self.__step_datas,
) )
def test_start(self) -> "HttpRunner": def test_start(self, parametrize=None) -> "HttpRunner":
"""main entrance, discovered by pytest""" """main entrance, discovered by pytest"""
self.__init_tests__() self.__init_tests__()
self.__project_meta = self.__project_meta or load_project_meta( self.__project_meta = self.__project_meta or load_project_meta(
@@ -435,6 +435,8 @@ class HttpRunner(object):
# parse config name # parse config name
config_variables = self.__config.variables config_variables = self.__config.variables
if parametrize:
config_variables.update(parametrize)
config_variables.update(self.__session_variables) config_variables.update(self.__session_variables)
self.__config.name = parse_data( self.__config.name = parse_data(
self.__config.name, config_variables, self.__project_meta.functions self.__config.name, config_variables, self.__project_meta.functions

View File

@@ -5,6 +5,7 @@ import os.path
import platform import platform
import uuid import uuid
from multiprocessing import Queue from multiprocessing import Queue
import itertools
from typing import Dict, List, Any from typing import Dict, List, Any
import sentry_sdk import sentry_sdk
@@ -220,3 +221,79 @@ def is_support_multiprocessing() -> bool:
except (ImportError, OSError): except (ImportError, OSError):
# system that does not support semaphores(dependency of multiprocessing), like Android termux # system that does not support semaphores(dependency of multiprocessing), like Android termux
return False 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):
""" generate cartesian product for lists
Args:
args (list of list): lists to be generated with cartesian product
Returns:
list: cartesian product in list
Examples:
>>> arg1 = [{"a": 1}, {"a": 2}]
>>> arg2 = [{"x": 111, "y": 112}, {"x": 121, "y": 122}]
>>> args = [arg1, arg2]
>>> gen_cartesian_product(*args)
>>> # same as below
>>> gen_cartesian_product(arg1, arg2)
[
{'a': 1, 'x': 111, 'y': 112},
{'a': 1, 'x': 121, 'y': 122},
{'a': 2, 'x': 111, 'y': 112},
{'a': 2, 'x': 121, 'y': 122}
]
"""
if not args:
return []
elif len(args) == 1:
return args[0]
product_list = []
for product_item_tuple in itertools.product(*args):
product_item_dict = {}
for item in product_item_tuple:
product_item_dict.update(item)
product_list.append(product_item_dict)
return product_list

View File

@@ -9,6 +9,7 @@ from httprunner.make import (
make_teststep_chain_style, make_teststep_chain_style,
pytest_files_run_set, pytest_files_run_set,
ensure_file_abs_path_valid, ensure_file_abs_path_valid,
make_test_start_style
) )
from httprunner import loader from httprunner import loader
@@ -214,3 +215,26 @@ from request_methods.request_with_functions_test import (
teststep_chain_style, 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"))""", """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)
"""
)

View File

@@ -452,3 +452,30 @@ class TestParserBasic(unittest.TestCase):
self.assertEqual(parsed_testcase["headers"]["random"], variables["random"]) self.assertEqual(parsed_testcase["headers"]["random"], variables["random"])
self.assertEqual(parsed_testcase["body"], variables["data"]) self.assertEqual(parsed_testcase["body"], variables["data"])
self.assertEqual(parsed_testcase["headers"]["sum"], 3) self.assertEqual(parsed_testcase["headers"]["sum"], 3)
def test_parse_parameters_testcase(self):
variables = {
"user_agent": "chrome",
"sum": 5,
}
param = [
{
"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)

View File

@@ -125,3 +125,55 @@ class TestUtils(unittest.TestCase):
override_config_variables(step_variables, config_variables), override_config_variables(step_variables, config_variables),
{"base_url": "https://httpbin.org", "foo1": "bar1"}, {"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}
]
]
product_list = utils.gen_cartesian_product(*parameters_content_list)
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}
]
]
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}
]
)
def test_cartesian_product_empty(self):
parameters_content_list = []
product_list = utils.gen_cartesian_product(*parameters_content_list)
self.assertEqual(product_list, [])