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():
return __version__
def sum_two(m, n):
return m + n
@@ -15,3 +14,7 @@ def get_testcase_config_variables():
def get_testsuite_config_variables():
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 %}
class {{ class_name }}(HttpRunner):
{{ customization_test_start }}
config = {{ config_chain_style }}
teststeps = [
@@ -407,6 +408,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),
"teststeps_chain_style": [
make_teststep_chain_style(step) for step in teststeps
],
@@ -606,3 +608,22 @@ 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

View File

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

View File

@@ -463,3 +463,102 @@ def parse_variables_mapping(
parsed_variables[var_name] = parsed_value
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,
)
def test_start(self) -> "HttpRunner":
def test_start(self, parametrize=None) -> "HttpRunner":
"""main entrance, discovered by pytest"""
self.__init_tests__()
self.__project_meta = self.__project_meta or load_project_meta(
@@ -435,6 +435,8 @@ class HttpRunner(object):
# parse config name
config_variables = self.__config.variables
if parametrize:
config_variables.update(parametrize)
config_variables.update(self.__session_variables)
self.__config.name = parse_data(
self.__config.name, config_variables, self.__project_meta.functions

View File

@@ -5,6 +5,7 @@ import os.path
import platform
import uuid
from multiprocessing import Queue
import itertools
from typing import Dict, List, Any
import sentry_sdk
@@ -220,3 +221,79 @@ def is_support_multiprocessing() -> bool:
except (ImportError, OSError):
# system that does not support semaphores(dependency of multiprocessing), like Android termux
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,
pytest_files_run_set,
ensure_file_abs_path_valid,
make_test_start_style
)
from httprunner import loader
@@ -214,3 +215,26 @@ 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)
"""
)

View File

@@ -452,3 +452,30 @@ class TestParserBasic(unittest.TestCase):
self.assertEqual(parsed_testcase["headers"]["random"], variables["random"])
self.assertEqual(parsed_testcase["body"], variables["data"])
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),
{"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, [])