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:
debugtalk
2020-07-06 21:58:08 +08:00
committed by GitHub
32 changed files with 358 additions and 233 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ name: integration_test
on: on:
push: push:
branches: branches:
- master - dev
pull_request: pull_request:
branches: branches:
- master - master
+1 -1
View File
@@ -3,7 +3,7 @@ name: unittest
on: on:
push: push:
branches: branches:
- master - dev
pull_request: pull_request:
branches: branches:
- master - master
+12
View File
@@ -1,5 +1,17 @@
# Release History # 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) ## 3.1.2 (2020-06-29)
**Fixed** **Fixed**
+3 -1
View File
@@ -1,10 +1,12 @@
# NOTE: Generated By HttpRunner v3.1.2 # NOTE: Generated By HttpRunner v3.1.3
# FROM: basic.yml # FROM: basic.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
class TestCaseBasic(HttpRunner): class TestCaseBasic(HttpRunner):
config = Config("basic test with httpbin").base_url("https://httpbin.org/") config = Config("basic test with httpbin").base_url("https://httpbin.org/")
teststeps = [ teststeps = [
+3 -1
View File
@@ -1,10 +1,12 @@
# NOTE: Generated By HttpRunner v3.1.2 # NOTE: Generated By HttpRunner v3.1.3
# FROM: hooks.yml # FROM: hooks.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
class TestCaseHooks(HttpRunner): class TestCaseHooks(HttpRunner):
config = Config("basic test with httpbin").base_url("${get_httpbin_server()}") config = Config("basic test with httpbin").base_url("${get_httpbin_server()}")
teststeps = [ teststeps = [
+3 -1
View File
@@ -1,10 +1,12 @@
# NOTE: Generated By HttpRunner v3.1.2 # NOTE: Generated By HttpRunner v3.1.3
# FROM: load_image.yml # FROM: load_image.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
class TestCaseLoadImage(HttpRunner): class TestCaseLoadImage(HttpRunner):
config = Config("load images").base_url("${get_httpbin_server()}") config = Config("load images").base_url("${get_httpbin_server()}")
teststeps = [ teststeps = [
+3 -1
View File
@@ -1,10 +1,12 @@
# NOTE: Generated By HttpRunner v3.1.2 # NOTE: Generated By HttpRunner v3.1.3
# FROM: upload.yml # FROM: upload.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
class TestCaseUpload(HttpRunner): class TestCaseUpload(HttpRunner):
config = Config("test upload file with httpbin").base_url("${get_httpbin_server()}") config = Config("test upload file with httpbin").base_url("${get_httpbin_server()}")
teststeps = [ teststeps = [
+3 -1
View File
@@ -1,10 +1,12 @@
# NOTE: Generated By HttpRunner v3.1.2 # NOTE: Generated By HttpRunner v3.1.3
# FROM: validate.yml # FROM: validate.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
class TestCaseValidate(HttpRunner): class TestCaseValidate(HttpRunner):
config = Config("basic test with httpbin").base_url("http://httpbin.org/") config = Config("basic test with httpbin").base_url("http://httpbin.org/")
teststeps = [ teststeps = [
+5
View File
@@ -4,6 +4,7 @@ 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
@@ -18,3 +19,7 @@ def get_testsuite_config_variables():
def get_app_version(): def get_app_version():
return [3.1, 3.0] 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: request_methods/request_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
class TestCaseRequestWithFunctions(HttpRunner): class TestCaseRequestWithFunctions(HttpRunner):
config = ( config = (
Config("request with functions") Config("request with functions")
.variables( .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 # FROM: request_methods/request_with_testcase_reference.yml
import sys import sys
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
from request_methods.request_with_functions_test import ( from request_methods.request_with_functions_test import (
@@ -14,6 +16,7 @@ from request_methods.request_with_functions_test import (
class TestCaseRequestWithTestcaseReference(HttpRunner): class TestCaseRequestWithTestcaseReference(HttpRunner):
config = ( config = (
Config("request with referenced testcase") Config("request with referenced testcase")
.variables( .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: request_methods/hardcode.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
class TestCaseHardcode(HttpRunner): class TestCaseHardcode(HttpRunner):
config = ( config = (
Config("request methods testcase in hardcode") Config("request methods testcase in hardcode")
.base_url("https://postman-echo.com") .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: request_methods/request_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
class TestCaseRequestWithFunctions(HttpRunner): class TestCaseRequestWithFunctions(HttpRunner):
config = ( config = (
Config("request methods testcase with functions") Config("request methods testcase with functions")
.variables( .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 # FROM: request_methods/request_with_testcase_reference.yml
import sys import sys
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
from request_methods.request_with_functions_test import ( from request_methods.request_with_functions_test import (
@@ -14,6 +16,7 @@ from request_methods.request_with_functions_test import (
class TestCaseRequestWithTestcaseReference(HttpRunner): class TestCaseRequestWithTestcaseReference(HttpRunner):
config = ( config = (
Config("request methods testcase: reference testcase") Config("request methods testcase: reference testcase")
.variables( .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: request_methods/request_with_variables.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
class TestCaseRequestWithVariables(HttpRunner): class TestCaseRequestWithVariables(HttpRunner):
config = ( config = (
Config("request methods testcase with variables") Config("request methods testcase with variables")
.variables(**{"foo1": "testcase_config_bar1", "foo2": "testcase_config_bar2"}) .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: request_methods/validate_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
class TestCaseValidateWithFunctions(HttpRunner): class TestCaseValidateWithFunctions(HttpRunner):
config = ( config = (
Config("request methods testcase: validate with functions") Config("request methods testcase: validate with functions")
.variables(**{"foo1": "session_bar1"}) .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: request_methods/validate_with_variables.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
class TestCaseValidateWithVariables(HttpRunner): class TestCaseValidateWithVariables(HttpRunner):
config = ( config = (
Config("request methods testcase: validate with variables") Config("request methods testcase: validate with variables")
.variables(**{"foo1": "session_bar1"}) .variables(**{"foo1": "session_bar1"})
+3 -1
View File
@@ -1,8 +1,9 @@
__version__ = "3.1.2" __version__ = "3.1.3"
__description__ = "One-stop solution for HTTP(S) testing." __description__ = "One-stop solution for HTTP(S) testing."
# import firstly for monkey patch if needed # import firstly for monkey patch if needed
from httprunner.ext.locust import main_locusts from httprunner.ext.locust import main_locusts
from httprunner.parser import parse_parameters as Parameters
from httprunner.runner import HttpRunner from httprunner.runner import HttpRunner
from httprunner.testcase import Config, Step, RunRequest, RunTestCase from httprunner.testcase import Config, Step, RunRequest, RunTestCase
@@ -14,4 +15,5 @@ __all__ = [
"Step", "Step",
"RunRequest", "RunRequest",
"RunTestCase", "RunTestCase",
"Parameters",
] ]
+4
View File
@@ -182,6 +182,10 @@ def _ensure_step_attachment(step: Dict) -> Dict:
test_dict["export"] = step["export"] test_dict["export"] = step["export"]
if "validate" in step: 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"]) test_dict["validate"] = _convert_validators(step["validate"])
if "validate_script" in step: if "validate_script" in step:
+6 -1
View File
@@ -251,7 +251,12 @@ class HarParser(object):
encoding = resp_content_dict.get("encoding") encoding = resp_content_dict.get("encoding")
if encoding and encoding == "base64": 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: else:
content = text content = text
+19 -27
View File
@@ -37,23 +37,31 @@ pytest_files_run_set: Set = set()
__TEMPLATE__ = jinja2.Template( __TEMPLATE__ = jinja2.Template(
"""# NOTE: Generated By HttpRunner v{{ version }} """# NOTE: Generated By HttpRunner v{{ version }}
# FROM: {{ testcase_path }} # FROM: {{ testcase_path }}
{% if imports_list and diff_levels > 0 %} {% if imports_list and diff_levels > 0 %}
import sys import sys
from pathlib import Path 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 %} {% endif %}
{% if parameters %}
import pytest
from httprunner import Parameters
{% endif %}
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
{% for import_str in imports_list %} {% for import_str in imports_list %}
{{ import_str }} {{ import_str }}
{% endfor %} {% endfor %}
class {{ class_name }}(HttpRunner): 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 }} config = {{ config_chain_style }}
teststeps = [ teststeps = [
@@ -367,6 +375,9 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
ref_testcase_path = __ensure_absolute(teststep["testcase"]) ref_testcase_path = __ensure_absolute(teststep["testcase"])
test_content = load_test_file(ref_testcase_path) 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 # api in v2 format, convert to v3 testcase
if "request" in test_content and "name" in test_content: if "request" in test_content and "name" in test_content:
test_content = ensure_testcase_v3_api(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}", "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), "parameters": config.get("parameters"),
"teststeps_chain_style": [ "teststeps_chain_style": [
make_teststep_chain_style(step) for step in teststeps make_teststep_chain_style(step) for step in teststeps
], ],
@@ -608,22 +619,3 @@ 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
+52 -44
View File
@@ -1,7 +1,7 @@
import ast import ast
import builtins import builtins
import re 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 loguru import logger
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
@@ -465,51 +465,44 @@ def parse_variables_mapping(
return parsed_variables 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. """ parse parameters and generate cartesian product.
Args: 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: parameter value may be in three types:
(1) data list, e.g. ["iOS/10.1", "iOS/10.2", "iOS/10.3"] (1) data list, e.g. ["iOS/10.1", "iOS/10.2", "iOS/10.3"]
(2) call built-in parameterize function, "${parameterize(account.csv)}" (2) call built-in parameterize function, "${parameterize(account.csv)}"
(3) call custom function in debugtalk.py, "${gen_app_version()}" (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: Returns:
list: cartesian product list list: cartesian product list
Examples: Examples:
>>> parameters = [ >>> parameters = {
{"user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"]}, "user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"],
{"username-password": "${parameterize(account.csv)}"}, "username-password": "${parameterize(account.csv)}",
{"app_version": "${gen_app_version()}"} "app_version": "${gen_app_version()}",
] }
>>> parse_parameters(parameters) >>> parse_parameters(parameters)
""" """
variables_mapping = variables_mapping or {} parsed_parameters_list: List[List[Dict]] = []
functions_mapping = functions_mapping or {}
parsed_parameters_list = []
# load project_meta functions # load project_meta functions
from httprunner.loader import load_project_meta project_meta = loader.load_project_meta("")
project_meta = load_project_meta("") functions_mapping = project_meta.functions
functions_mapping.update(project_meta.functions)
parameters = utils.ensure_mapping_format(parameters)
for parameter_name, parameter_content in parameters.items(): for parameter_name, parameter_content in parameters.items():
parameter_name_list = parameter_name.split("-") parameter_name_list = parameter_name.split("-")
if isinstance(parameter_content, list): if isinstance(parameter_content, List):
# (1) data list # (1) data list
# e.g. {"app_version": ["2.8.5", "2.8.6"]} # e.g. {"app_version": ["2.8.5", "2.8.6"]}
# => [{"app_version": "2.8.5", "app_version": "2.8.6"}] # => [{"app_version": "2.8.5", "app_version": "2.8.6"}]
# e.g. {"username-password": [["user1", "111111"], ["test2", "222222"]} # e.g. {"username-password": [["user1", "111111"], ["test2", "222222"]}
# => [{"username": "user1", "password": "111111"}, {"username": "user2", "password": "222222"}] # => [{"username": "user1", "password": "111111"}, {"username": "user2", "password": "222222"}]
parameter_content_list = [] parameter_content_list: List[Dict] = []
for parameter_item in parameter_content: for parameter_item in parameter_content:
if not isinstance(parameter_item, (list, tuple)): if not isinstance(parameter_item, (list, tuple)):
# "2.8.5" => ["2.8.5"] # "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"} # ["app_version"], ["2.8.5"] => {"app_version": "2.8.5"}
# ["username", "password"], ["user1", "111111"] => {"username": "user1", "password": "111111"} # ["username", "password"], ["user1", "111111"] => {"username": "user1", "password": "111111"}
parameter_content_dict = dict(zip(parameter_name_list, parameter_item)) parameter_content_dict = dict(zip(parameter_name_list, parameter_item))
parameter_content_list.append(parameter_content_dict) 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: for parameter_item in parsed_parameter_content:
if isinstance(parameter_item, dict): if isinstance(parameter_item, Dict):
# get subset by parameter name # get subset by parameter name
# {"app_version": "${gen_app_version()}"} # {"app_version": "${gen_app_version()}"}
# gen_app_version() => [{'app_version': '2.8.5'}, {'app_version': '2.8.6'}] # 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": "user1", "password": "111111"},
# {"username": "user2", "password": "222222"} # {"username": "user2", "password": "222222"}
# ] # ]
parameter_dict = {key: parameter_item[key] for key in parameter_name_list} parameter_dict: Dict = {
# elif isinstance(parameter_item, (list, tuple)): key: parameter_item[key] for key in parameter_name_list
# # {"username-password": "${get_account()}"} }
# # get_account() => [("user1", "111111"), ("user2", "222222")] elif isinstance(parameter_item, (List, tuple)):
# parameter_dict = dict(zip(parameter_name_list, parameter_item)) 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: elif len(parameter_name_list) == 1:
# {"user_agent": "${get_user_agent()}"} # {"user_agent": "${get_user_agent()}"}
# get_user_agent() => ["iOS/10.1", "iOS/10.2"] # get_user_agent() => ["iOS/10.1", "iOS/10.2"]
parameter_dict = { # parameter_dict will get: {"user_agent": "iOS/10.1", "user_agent": "iOS/10.2"}
parameter_name_list[0]: parameter_item 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) 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) parsed_parameters_list.append(parameter_content_list)
return utils.gen_cartesian_product(*parsed_parameters_list) return utils.gen_cartesian_product(*parsed_parameters_list)
+22 -6
View File
@@ -2,12 +2,13 @@ from typing import Dict, Text, Any, NoReturn
import jmespath import jmespath
import requests import requests
from jmespath.exceptions import JMESPathError
from loguru import logger from loguru import logger
from httprunner import exceptions from httprunner import exceptions
from httprunner.exceptions import ValidationFailure, ParamsError 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.models import VariablesMapping, Validators, FunctionsMapping
from httprunner.parser import parse_data, parse_string_value, get_mapping_function
def get_uniform_comparator(comparator: Text): def get_uniform_comparator(comparator: Text):
@@ -143,14 +144,25 @@ class ResponseObject(object):
self.__dict__[key] = value self.__dict__[key] = value
return value return value
@property def _search_jmespath(self, expr: Text) -> Any:
def resp_obj_meta(self): resp_obj_meta = {
return {
"status_code": self.status_code, "status_code": self.status_code,
"headers": self.headers, "headers": self.headers,
"cookies": self.cookies, "cookies": self.cookies,
"body": self.body, "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]: def extract(self, extractors: Dict[Text, Text]) -> Dict[Text, Any]:
if not extractors: if not extractors:
@@ -158,7 +170,7 @@ class ResponseObject(object):
extract_mapping = {} extract_mapping = {}
for key, field in extractors.items(): 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 extract_mapping[key] = field_value
logger.info(f"extract mapping: {extract_mapping}") logger.info(f"extract mapping: {extract_mapping}")
@@ -197,7 +209,11 @@ class ResponseObject(object):
) )
check_item = parse_string_value(check_item) 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 # comparator
assert_method = u_validator["assert"] assert_method = u_validator["assert"]
+3 -3
View File
@@ -421,7 +421,7 @@ class HttpRunner(object):
step_datas=self.__step_datas, 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""" """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,8 +435,8 @@ class HttpRunner(object):
# parse config name # parse config name
config_variables = self.__config.variables config_variables = self.__config.variables
if parametrize: if param:
config_variables.update(parametrize) config_variables.update(param)
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
+1 -1
View File
@@ -159,7 +159,7 @@ class StepRequestValidation(object):
return self return self
def assert_string_equals( def assert_string_equals(
self, jmes_path: Text, expected_value: int, message: Text = "" self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation": ) -> "StepRequestValidation":
self.__step_context.validators.append( self.__step_context.validators.append(
{"string_equals": [jmes_path, expected_value, message]} {"string_equals": [jmes_path, expected_value, message]}
+2 -37
View File
@@ -6,7 +6,7 @@ import platform
import uuid import uuid
from multiprocessing import Queue from multiprocessing import Queue
import itertools import itertools
from typing import Dict, List, Any from typing import Dict, List, Any, Union, Text
import sentry_sdk import sentry_sdk
from loguru import logger from loguru import logger
@@ -223,42 +223,7 @@ def is_support_multiprocessing() -> bool:
return False return False
def ensure_mapping_format(variables): def gen_cartesian_product(*args: List[Dict]) -> List[Dict]:
""" 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 """ generate cartesian product for lists
Args: Args:
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "httprunner" name = "httprunner"
version = "3.1.2" version = "3.1.3"
description = "One-stop solution for HTTP(S) testing." description = "One-stop solution for HTTP(S) testing."
license = "Apache-2.0" license = "Apache-2.0"
readme = "README.md" readme = "README.md"
+1 -25
View File
@@ -1,6 +1,7 @@
import os import os
import unittest import unittest
from httprunner import loader
from httprunner.make import ( from httprunner.make import (
main_make, main_make,
convert_testcase_path, convert_testcase_path,
@@ -9,9 +10,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
class TestMake(unittest.TestCase): class TestMake(unittest.TestCase):
@@ -215,26 +214,3 @@ 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)
"""
)
+101 -35
View File
@@ -1,9 +1,10 @@
import os
import time import time
import unittest import unittest
from httprunner import parser from httprunner import parser
from httprunner.exceptions import VariableNotFound, FunctionNotFound from httprunner.exceptions import VariableNotFound, FunctionNotFound
from httprunner.parser import regex_findall_variables from httprunner.loader import load_project_meta
class TestParserBasic(unittest.TestCase): class TestParserBasic(unittest.TestCase):
@@ -27,17 +28,19 @@ class TestParserBasic(unittest.TestCase):
self.assertEqual(parser.parse_string_value("${func}"), "${func}") self.assertEqual(parser.parse_string_value("${func}"), "${func}")
def test_regex_findall_variables(self): def test_regex_findall_variables(self):
self.assertEqual(regex_findall_variables("$variable"), ["variable"]) self.assertEqual(parser.regex_findall_variables("$variable"), ["variable"])
self.assertEqual(regex_findall_variables("${variable}123"), ["variable"]) self.assertEqual(parser.regex_findall_variables("${variable}123"), ["variable"])
self.assertEqual(regex_findall_variables("/blog/$postid"), ["postid"]) self.assertEqual(parser.regex_findall_variables("/blog/$postid"), ["postid"])
self.assertEqual(regex_findall_variables("/$var1/$var2"), ["var1", "var2"]) self.assertEqual(
self.assertEqual(regex_findall_variables("abc"), []) parser.regex_findall_variables("/$var1/$var2"), ["var1", "var2"]
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(parser.regex_findall_variables("abc"), [])
self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$$$a"), ["a"]) self.assertEqual(parser.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(parser.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(parser.regex_findall_variables("Z:2>1*0*1+1$$$a"), ["a"])
self.assertEqual(regex_findall_variables("Z:2>1*0*1+1$$a$$b"), []) 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): def test_extract_variables(self):
self.assertEqual(parser.extract_variables("$var"), {"var"}) self.assertEqual(parser.extract_variables("$var"), {"var"})
@@ -454,28 +457,91 @@ class TestParserBasic(unittest.TestCase):
self.assertEqual(parsed_testcase["headers"]["sum"], 3) self.assertEqual(parsed_testcase["headers"]["sum"], 3)
def test_parse_parameters_testcase(self): def test_parse_parameters_testcase(self):
variables = { parameters = {
"user_agent": "chrome", "user_agent": ["iOS/10.1", "iOS/10.2"],
"sum": 5, "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": "test1",
"username-password": "${parameterize(request_methods/account.csv)}", "password": "111111",
"sum": "${add_two_nums(1, 2)}", "user_agent": "iOS/10.1",
} "sum": 3,
] },
parsed_params,
functions = { )
"add_two_nums": lambda a, b=1: [a + b, b-a], self.assertIn(
} {
"username": "test1",
parsed_params = parser.parse_parameters(param, variables, functions) "password": "111111",
self.assertIn({'username': 'test1', 'password': '111111', 'user_agent': 'iOS/10.1', 'sum': 3}, parsed_params) "user_agent": "iOS/10.1",
self.assertIn({'username': 'test1', 'password': '111111', 'user_agent': 'iOS/10.1', 'sum': 1}, parsed_params) "sum": 1,
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) 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(
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.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,
)
+17 -1
View File
@@ -28,12 +28,28 @@ class TestResponse(unittest.TestCase):
self.assertEqual(extract_mapping["var_2"], "Olympia") self.assertEqual(extract_mapping["var_2"], "Olympia")
def test_validate(self): def test_validate(self):
variables_mapping = {"index": 1}
self.resp_obj.validate( self.resp_obj.validate(
[ [
{"eq": ["body.json.locations[0].name", "Seattle"]}, {"eq": ["body.json.locations[0].name", "Seattle"]},
{"eq": ["body.json.locations[0]", {"name": "Seattle", "state": "WA"}]}, {"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": ["body.json.locations[$index].name", "New York"]},
{"eq": ["$var_empty", ""]},
], ],
variables_mapping=variables_mapping, 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,
)
+10 -36
View File
@@ -126,54 +126,28 @@ class TestUtils(unittest.TestCase):
{"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): def test_cartesian_product_one(self):
parameters_content_list = [ parameters_content_list = [[{"a": 1}, {"a": 2}]]
[
{"a": 1},
{"a": 2}
]
]
product_list = utils.gen_cartesian_product(*parameters_content_list) product_list = utils.gen_cartesian_product(*parameters_content_list)
self.assertEqual( self.assertEqual(product_list, [{"a": 1}, {"a": 2}])
product_list,
[
{"a": 1},
{"a": 2}
]
)
def test_cartesian_product_multiple(self): def test_cartesian_product_multiple(self):
parameters_content_list = [ parameters_content_list = [
[ [{"a": 1}, {"a": 2}],
{"a": 1}, [{"x": 111, "y": 112}, {"x": 121, "y": 122}],
{"a": 2}
],
[
{"x": 111, "y": 112},
{"x": 121, "y": 122}
]
] ]
product_list = utils.gen_cartesian_product(*parameters_content_list) product_list = utils.gen_cartesian_product(*parameters_content_list)
self.assertEqual( self.assertEqual(
product_list, product_list,
[ [
{'a': 1, 'x': 111, 'y': 112}, {"a": 1, "x": 111, "y": 112},
{'a': 1, 'x': 121, 'y': 122}, {"a": 1, "x": 121, "y": 122},
{'a': 2, 'x': 111, 'y': 112}, {"a": 2, "x": 111, "y": 112},
{'a': 2, 'x': 121, 'y': 122} {"a": 2, "x": 121, "y": 122},
] ],
) )
def test_cartesian_product_empty(self): def test_cartesian_product_empty(self):
parameters_content_list = [] parameters_content_list = []
product_list = utils.gen_cartesian_product(*parameters_content_list) product_list = utils.gen_cartesian_product(*parameters_content_list)
self.assertEqual(product_list, []) self.assertEqual(product_list, [])