mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-12 02:21:29 +08:00
@@ -9,11 +9,23 @@ import yaml
|
||||
from httprunner import exceptions, logger, parser, validator
|
||||
from httprunner.compat import OrderedDict
|
||||
|
||||
|
||||
project_mapping = {
|
||||
"debugtalk": {},
|
||||
"env": {},
|
||||
"def-api": {},
|
||||
"def-testcase": {}
|
||||
}
|
||||
""" dict: save project loaded api/testcases definitions, environments and debugtalk.py module.
|
||||
"""
|
||||
|
||||
testcases_cache_mapping = {}
|
||||
|
||||
|
||||
###############################################################################
|
||||
## file loader
|
||||
###############################################################################
|
||||
|
||||
|
||||
def _check_format(file_path, content):
|
||||
""" check testcase format if valid
|
||||
"""
|
||||
@@ -102,10 +114,14 @@ def load_file(file_path):
|
||||
|
||||
|
||||
def load_folder_files(folder_path, recursive=True):
|
||||
""" load folder path, return all files in list format.
|
||||
@param
|
||||
folder_path: specified folder path to load
|
||||
recursive: if True, will load files recursively
|
||||
""" load folder path, return all files endswith yml/yaml/json in list.
|
||||
|
||||
Args:
|
||||
folder_path (str): specified folder path to load
|
||||
recursive (bool): load files recursively if True
|
||||
|
||||
Returns:
|
||||
list: files endswith yml/yaml/json
|
||||
"""
|
||||
if isinstance(folder_path, (list, set)):
|
||||
files = []
|
||||
@@ -140,6 +156,24 @@ def load_folder_files(folder_path, recursive=True):
|
||||
|
||||
def load_dot_env_file(path):
|
||||
""" load .env file
|
||||
|
||||
Args:
|
||||
path (str): .env file path.
|
||||
If path is None, it will find .env file in current working directory.
|
||||
|
||||
Returns:
|
||||
dict: environment variables mapping
|
||||
|
||||
{
|
||||
"UserName": "debugtalk",
|
||||
"Password": "123456",
|
||||
"PROJECT_KEY": "ABCDEFGH"
|
||||
}
|
||||
|
||||
Raises:
|
||||
exceptions.FileNotFound: If specified env file is not exist.
|
||||
exceptions.FileFormatError: If env file format is invalid.
|
||||
|
||||
"""
|
||||
if not path:
|
||||
path = os.path.join(os.getcwd(), ".env")
|
||||
@@ -163,6 +197,7 @@ def load_dot_env_file(path):
|
||||
|
||||
env_variables_mapping[variable.strip()] = value.strip()
|
||||
|
||||
project_mapping["env"] = env_variables_mapping
|
||||
return env_variables_mapping
|
||||
|
||||
|
||||
@@ -289,7 +324,10 @@ def load_debugtalk_module(start_path=None):
|
||||
}
|
||||
|
||||
imported_module = importlib.import_module(module_name)
|
||||
return load_python_module(imported_module)
|
||||
loaded_module = load_python_module(imported_module)
|
||||
|
||||
project_mapping["debugtalk"] = loaded_module
|
||||
return loaded_module
|
||||
|
||||
|
||||
def get_module_item(module_mapping, item_type, item_name):
|
||||
@@ -326,88 +364,15 @@ def get_module_item(module_mapping, item_type, item_name):
|
||||
|
||||
|
||||
###############################################################################
|
||||
## suite loader
|
||||
## testcase loader
|
||||
###############################################################################
|
||||
|
||||
|
||||
overall_def_dict = {
|
||||
"api": {},
|
||||
"suite": {}
|
||||
}
|
||||
testcases_cache_mapping = {}
|
||||
|
||||
|
||||
def _load_test_dependencies():
|
||||
""" load all api and suite definitions.
|
||||
default api folder is "$CWD/tests/api/".
|
||||
default suite folder is "$CWD/tests/suite/".
|
||||
"""
|
||||
# TODO: cache api and suite loading
|
||||
# load api definitions
|
||||
api_def_folder = os.path.join(os.getcwd(), "tests", "api")
|
||||
for test_file in load_folder_files(api_def_folder):
|
||||
_load_api_file(test_file)
|
||||
|
||||
# load suite definitions
|
||||
suite_def_folder = os.path.join(os.getcwd(), "tests", "suite")
|
||||
for suite_file in load_folder_files(suite_def_folder):
|
||||
suite = _load_test_file(suite_file)
|
||||
if "def" not in suite["config"]:
|
||||
raise exceptions.ParamsError("def missed in suite file: {}!".format(suite_file))
|
||||
|
||||
call_func = suite["config"]["def"]
|
||||
function_meta = parser.parse_function(call_func)
|
||||
suite["function_meta"] = function_meta
|
||||
overall_def_dict["suite"][function_meta["func_name"]] = suite
|
||||
|
||||
|
||||
def _load_api_file(file_path):
|
||||
""" load api definition from file and store in overall_def_dict["api"]
|
||||
api file should be in format below:
|
||||
[
|
||||
{
|
||||
"api": {
|
||||
"def": "api_login",
|
||||
"request": {},
|
||||
"validate": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"api": {
|
||||
"def": "api_logout",
|
||||
"request": {},
|
||||
"validate": []
|
||||
}
|
||||
}
|
||||
]
|
||||
"""
|
||||
api_items = load_file(file_path)
|
||||
if not isinstance(api_items, list):
|
||||
raise exceptions.FileFormatError("API format error: {}".format(file_path))
|
||||
|
||||
for api_item in api_items:
|
||||
if not isinstance(api_item, dict) or len(api_item) != 1:
|
||||
raise exceptions.FileFormatError("API format error: {}".format(file_path))
|
||||
|
||||
key, api_dict = api_item.popitem()
|
||||
if key != "api" or not isinstance(api_dict, dict) or "def" not in api_dict:
|
||||
raise exceptions.FileFormatError("API format error: {}".format(file_path))
|
||||
|
||||
api_def = api_dict.pop("def")
|
||||
function_meta = parser.parse_function(api_def)
|
||||
func_name = function_meta["func_name"]
|
||||
|
||||
if func_name in overall_def_dict["api"]:
|
||||
logger.log_warning("API definition duplicated: {}".format(func_name))
|
||||
|
||||
api_dict["function_meta"] = function_meta
|
||||
overall_def_dict["api"][func_name] = api_dict
|
||||
|
||||
|
||||
def _load_test_file(file_path):
|
||||
""" load testcase file or testsuite file
|
||||
@param file_path: absolute valid file path
|
||||
file_path should be in format below:
|
||||
|
||||
Args:
|
||||
file_path (str): absolute valid file path. file_path should be in the following format:
|
||||
|
||||
[
|
||||
{
|
||||
"config": {
|
||||
@@ -423,6 +388,13 @@ def _load_test_file(file_path):
|
||||
"validate": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"test": {
|
||||
"name": "add product to cart",
|
||||
"suite": "create_and_check()",
|
||||
"validate": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"test": {
|
||||
"name": "checkout cart",
|
||||
@@ -431,19 +403,24 @@ def _load_test_file(file_path):
|
||||
}
|
||||
}
|
||||
]
|
||||
@return testset dict
|
||||
{
|
||||
"config": {},
|
||||
"testcases": [testcase11, testcase12]
|
||||
}
|
||||
|
||||
Returns:
|
||||
dict: testcase dict
|
||||
{
|
||||
"config": {},
|
||||
"teststeps": [teststep11, teststep12]
|
||||
}
|
||||
|
||||
"""
|
||||
testset = {
|
||||
testcase = {
|
||||
"config": {
|
||||
"path": file_path
|
||||
},
|
||||
"testcases": [] # TODO: rename to tests
|
||||
"teststeps": []
|
||||
}
|
||||
|
||||
for item in load_file(file_path):
|
||||
# TODO: add json schema validation
|
||||
if not isinstance(item, dict) or len(item) != 1:
|
||||
raise exceptions.FileFormatError("Testcase format error: {}".format(file_path))
|
||||
|
||||
@@ -452,43 +429,69 @@ def _load_test_file(file_path):
|
||||
raise exceptions.FileFormatError("Testcase format error: {}".format(file_path))
|
||||
|
||||
if key == "config":
|
||||
testset["config"].update(test_block)
|
||||
testcase["config"].update(test_block)
|
||||
|
||||
elif key == "test":
|
||||
|
||||
def extend_api_definition(block):
|
||||
ref_call = block["api"]
|
||||
def_block = _get_block_by_name(ref_call, "def-api")
|
||||
_extend_block(block, def_block)
|
||||
|
||||
# reference api
|
||||
if "api" in test_block:
|
||||
ref_call = test_block["api"]
|
||||
def_block = _get_block_by_name(ref_call, "api")
|
||||
_override_block(def_block, test_block)
|
||||
testset["testcases"].append(test_block)
|
||||
elif "suite" in test_block:
|
||||
extend_api_definition(test_block)
|
||||
testcase["teststeps"].append(test_block)
|
||||
|
||||
# reference testcase
|
||||
elif "suite" in test_block: # TODO: replace suite with testcase
|
||||
ref_call = test_block["suite"]
|
||||
block = _get_block_by_name(ref_call, "suite")
|
||||
testset["testcases"].extend(block["testcases"])
|
||||
block = _get_block_by_name(ref_call, "def-testcase")
|
||||
# TODO: bugfix lost block config variables
|
||||
for teststep in block["teststeps"]:
|
||||
if "api" in teststep:
|
||||
extend_api_definition(teststep)
|
||||
testcase["teststeps"].append(teststep)
|
||||
|
||||
# define directly
|
||||
else:
|
||||
testset["testcases"].append(test_block)
|
||||
testcase["teststeps"].append(test_block)
|
||||
|
||||
else:
|
||||
logger.log_warning(
|
||||
"unexpected block key: {}. block key should only be 'config' or 'test'.".format(key)
|
||||
)
|
||||
|
||||
return testset
|
||||
return testcase
|
||||
|
||||
|
||||
def _get_block_by_name(ref_call, ref_type):
|
||||
""" get test content by reference name
|
||||
@params:
|
||||
ref_call: e.g. api_v1_Account_Login_POST($UserName, $Password)
|
||||
ref_type: "api" or "suite"
|
||||
""" get test content by reference name.
|
||||
|
||||
Args:
|
||||
ref_call (str): call function.
|
||||
e.g. api_v1_Account_Login_POST($UserName, $Password)
|
||||
ref_type (enum): "def-api" or "def-testcase"
|
||||
|
||||
Returns:
|
||||
dict: api/testcase definition.
|
||||
|
||||
Raises:
|
||||
exceptions.ParamsError: call args number is not equal to defined args number.
|
||||
|
||||
"""
|
||||
function_meta = parser.parse_function(ref_call)
|
||||
func_name = function_meta["func_name"]
|
||||
call_args = function_meta["args"]
|
||||
block = _get_test_definition(func_name, ref_type)
|
||||
def_args = block.get("function_meta").get("args", [])
|
||||
def_args = block.get("function_meta", {}).get("args", [])
|
||||
|
||||
if len(call_args) != len(def_args):
|
||||
raise exceptions.ParamsError("call args mismatch defined args!")
|
||||
err_msg = "{}: call args number is not equal to defined args number!\n".format(func_name)
|
||||
err_msg += "defined args: {}\n".format(def_args)
|
||||
err_msg += "reference args: {}".format(call_args)
|
||||
logger.log_error(err_msg)
|
||||
raise exceptions.ParamsError(err_msg)
|
||||
|
||||
args_mapping = {}
|
||||
for index, item in enumerate(def_args):
|
||||
@@ -505,80 +508,104 @@ def _get_block_by_name(ref_call, ref_type):
|
||||
|
||||
def _get_test_definition(name, ref_type):
|
||||
""" get expected api or testcase.
|
||||
@params:
|
||||
name: api or testcase name
|
||||
ref_type: "api" or "suite"
|
||||
@return
|
||||
expected api info if found, otherwise raise ApiNotFound exception
|
||||
|
||||
Args:
|
||||
name (str): api or testcase name
|
||||
ref_type (enum): "def-api" or "def-testcase"
|
||||
|
||||
Returns:
|
||||
dict: expected api/testcase info if found.
|
||||
|
||||
Raises:
|
||||
exceptions.ApiNotFound: api not found
|
||||
exceptions.TestcaseNotFound: testcase not found
|
||||
|
||||
"""
|
||||
block = overall_def_dict.get(ref_type, {}).get(name)
|
||||
block = project_mapping.get(ref_type, {}).get(name)
|
||||
|
||||
if not block:
|
||||
err_msg = "{} not found!".format(name)
|
||||
if ref_type == "api":
|
||||
if ref_type == "def-api":
|
||||
raise exceptions.ApiNotFound(err_msg)
|
||||
else:
|
||||
# ref_type == "suite":
|
||||
# ref_type == "def-testcase":
|
||||
raise exceptions.TestcaseNotFound(err_msg)
|
||||
|
||||
return block
|
||||
|
||||
|
||||
def _override_block(def_block, current_block):
|
||||
""" override def_block with current_block
|
||||
@param def_block:
|
||||
{
|
||||
"name": "get token",
|
||||
"request": {...},
|
||||
"validate": [{'eq': ['status_code', 200]}]
|
||||
}
|
||||
@param current_block:
|
||||
{
|
||||
"name": "get token",
|
||||
"extract": [{"token": "content.token"}],
|
||||
"validate": [{'eq': ['status_code', 201]}, {'len_eq': ['content.token', 16]}]
|
||||
}
|
||||
@return
|
||||
{
|
||||
"name": "get token",
|
||||
"request": {...},
|
||||
"extract": [{"token": "content.token"}],
|
||||
"validate": [{'eq': ['status_code', 201]}, {'len_eq': ['content.token', 16]}]
|
||||
}
|
||||
def _extend_block(ref_block, def_block):
|
||||
""" extend ref_block with def_block.
|
||||
|
||||
Args:
|
||||
def_block (dict): api definition dict.
|
||||
ref_block (dict): reference block
|
||||
|
||||
Returns:
|
||||
dict: extended reference block.
|
||||
|
||||
Examples:
|
||||
>>> def_block = {
|
||||
"name": "get token 1",
|
||||
"request": {...},
|
||||
"validate": [{'eq': ['status_code', 200]}]
|
||||
}
|
||||
>>> ref_block = {
|
||||
"name": "get token 2",
|
||||
"extract": [{"token": "content.token"}],
|
||||
"validate": [{'eq': ['status_code', 201]}, {'len_eq': ['content.token', 16]}]
|
||||
}
|
||||
>>> _extend_block(def_block, ref_block)
|
||||
{
|
||||
"name": "get token 2",
|
||||
"request": {...},
|
||||
"extract": [{"token": "content.token"}],
|
||||
"validate": [{'eq': ['status_code', 201]}, {'len_eq': ['content.token', 16]}]
|
||||
}
|
||||
|
||||
"""
|
||||
# TODO: override variables
|
||||
def_validators = def_block.get("validate") or def_block.get("validators", [])
|
||||
current_validators = current_block.get("validate") or current_block.get("validators", [])
|
||||
ref_validators = ref_block.get("validate") or ref_block.get("validators", [])
|
||||
|
||||
def_extrators = def_block.get("extract") \
|
||||
or def_block.get("extractors") \
|
||||
or def_block.get("extract_binds", [])
|
||||
current_extractors = current_block.get("extract") \
|
||||
or current_block.get("extractors") \
|
||||
or current_block.get("extract_binds", [])
|
||||
ref_extractors = ref_block.get("extract") \
|
||||
or ref_block.get("extractors") \
|
||||
or ref_block.get("extract_binds", [])
|
||||
|
||||
current_block.update(def_block)
|
||||
current_block["validate"] = _merge_validator(
|
||||
ref_block.update(def_block)
|
||||
ref_block["validate"] = _merge_validator(
|
||||
def_validators,
|
||||
current_validators
|
||||
ref_validators
|
||||
)
|
||||
current_block["extract"] = _merge_extractor(
|
||||
ref_block["extract"] = _merge_extractor(
|
||||
def_extrators,
|
||||
current_extractors
|
||||
ref_extractors
|
||||
)
|
||||
|
||||
|
||||
def _get_validators_mapping(validators):
|
||||
""" get validators mapping from api or test validators
|
||||
@param (list) validators:
|
||||
[
|
||||
{"check": "v1", "expect": 201, "comparator": "eq"},
|
||||
{"check": {"b": 1}, "expect": 200, "comparator": "eq"}
|
||||
]
|
||||
@return
|
||||
{
|
||||
("v1", "eq"): {"check": "v1", "expect": 201, "comparator": "eq"},
|
||||
('{"b": 1}', "eq"): {"check": {"b": 1}, "expect": 200, "comparator": "eq"}
|
||||
}
|
||||
def _convert_validators_to_mapping(validators):
|
||||
""" convert validators list to mapping.
|
||||
|
||||
Args:
|
||||
validators (list): validators in list
|
||||
|
||||
Returns:
|
||||
dict: validators mapping, use (check, comparator) as key.
|
||||
|
||||
Examples:
|
||||
>>> validators = [
|
||||
{"check": "v1", "expect": 201, "comparator": "eq"},
|
||||
{"check": {"b": 1}, "expect": 200, "comparator": "eq"}
|
||||
]
|
||||
>>> _convert_validators_to_mapping(validators)
|
||||
{
|
||||
("v1", "eq"): {"check": "v1", "expect": 201, "comparator": "eq"},
|
||||
('{"b": 1}', "eq"): {"check": {"b": 1}, "expect": 200, "comparator": "eq"}
|
||||
}
|
||||
|
||||
"""
|
||||
validators_mapping = {}
|
||||
|
||||
@@ -596,48 +623,66 @@ def _get_validators_mapping(validators):
|
||||
return validators_mapping
|
||||
|
||||
|
||||
def _merge_validator(def_validators, current_validators):
|
||||
""" merge def_validators with current_validators
|
||||
@params:
|
||||
def_validators: [{'eq': ['v1', 200]}, {"check": "s2", "expect": 16, "comparator": "len_eq"}]
|
||||
current_validators: [{"check": "v1", "expect": 201}, {'len_eq': ['s3', 12]}]
|
||||
@return:
|
||||
[
|
||||
{"check": "v1", "expect": 201, "comparator": "eq"},
|
||||
{"check": "s2", "expect": 16, "comparator": "len_eq"},
|
||||
{"check": "s3", "expect": 12, "comparator": "len_eq"}
|
||||
]
|
||||
def _merge_validator(def_validators, ref_validators):
|
||||
""" merge def_validators with ref_validators.
|
||||
|
||||
Args:
|
||||
def_validators (list):
|
||||
ref_validators (list):
|
||||
|
||||
Returns:
|
||||
list: merged validators
|
||||
|
||||
Examples:
|
||||
>>> def_validators = [{'eq': ['v1', 200]}, {"check": "s2", "expect": 16, "comparator": "len_eq"}]
|
||||
>>> ref_validators = [{"check": "v1", "expect": 201}, {'len_eq': ['s3', 12]}]
|
||||
>>> _merge_validator(def_validators, ref_validators)
|
||||
[
|
||||
{"check": "v1", "expect": 201, "comparator": "eq"},
|
||||
{"check": "s2", "expect": 16, "comparator": "len_eq"},
|
||||
{"check": "s3", "expect": 12, "comparator": "len_eq"}
|
||||
]
|
||||
|
||||
"""
|
||||
if not def_validators:
|
||||
return current_validators
|
||||
return ref_validators
|
||||
|
||||
elif not current_validators:
|
||||
elif not ref_validators:
|
||||
return def_validators
|
||||
|
||||
else:
|
||||
api_validators_mapping = _get_validators_mapping(def_validators)
|
||||
test_validators_mapping = _get_validators_mapping(current_validators)
|
||||
def_validators_mapping = _convert_validators_to_mapping(def_validators)
|
||||
ref_validators_mapping = _convert_validators_to_mapping(ref_validators)
|
||||
|
||||
api_validators_mapping.update(test_validators_mapping)
|
||||
return list(api_validators_mapping.values())
|
||||
def_validators_mapping.update(ref_validators_mapping)
|
||||
return list(def_validators_mapping.values())
|
||||
|
||||
|
||||
def _merge_extractor(def_extrators, current_extractors):
|
||||
""" merge def_extrators with current_extractors
|
||||
@params:
|
||||
def_extrators: [{"var1": "val1"}, {"var2": "val2"}]
|
||||
current_extractors: [{"var1": "val111"}, {"var3": "val3"}]
|
||||
@return:
|
||||
[
|
||||
{"var1": "val111"},
|
||||
{"var2": "val2"},
|
||||
{"var3": "val3"}
|
||||
]
|
||||
def _merge_extractor(def_extrators, ref_extractors):
|
||||
""" merge def_extrators with ref_extractors
|
||||
|
||||
Args:
|
||||
def_extrators (list): [{"var1": "val1"}, {"var2": "val2"}]
|
||||
ref_extractors (list): [{"var1": "val111"}, {"var3": "val3"}]
|
||||
|
||||
Returns:
|
||||
list: merged extractors
|
||||
|
||||
Examples:
|
||||
>>> def_extrators = [{"var1": "val1"}, {"var2": "val2"}]
|
||||
>>> ref_extractors = [{"var1": "val111"}, {"var3": "val3"}]
|
||||
>>> _merge_extractor(def_extrators, ref_extractors)
|
||||
[
|
||||
{"var1": "val111"},
|
||||
{"var2": "val2"},
|
||||
{"var3": "val3"}
|
||||
]
|
||||
|
||||
"""
|
||||
if not def_extrators:
|
||||
return current_extractors
|
||||
return ref_extractors
|
||||
|
||||
elif not current_extractors:
|
||||
elif not ref_extractors:
|
||||
return def_extrators
|
||||
|
||||
else:
|
||||
@@ -650,7 +695,7 @@ def _merge_extractor(def_extrators, current_extractors):
|
||||
var_name = list(api_extrator.keys())[0]
|
||||
extractor_dict[var_name] = api_extrator[var_name]
|
||||
|
||||
for test_extrator in current_extractors:
|
||||
for test_extrator in ref_extractors:
|
||||
if len(test_extrator) != 1:
|
||||
logger.log_warning("incorrect extractor: {}".format(test_extrator))
|
||||
continue
|
||||
@@ -665,17 +710,213 @@ def _merge_extractor(def_extrators, current_extractors):
|
||||
return extractor_list
|
||||
|
||||
|
||||
def load_folder_content(folder_path):
|
||||
""" load api/testcases/testsuites definitions from folder.
|
||||
|
||||
Args:
|
||||
folder_path (str): api/testcases/testsuites files folder.
|
||||
|
||||
Returns:
|
||||
dict: api definition mapping.
|
||||
|
||||
{
|
||||
"tests/api/basic.yml": [
|
||||
{"api": {"def": "api_login", "request": {}, "validate": []}},
|
||||
{"api": {"def": "api_logout", "request": {}, "validate": []}}
|
||||
]
|
||||
}
|
||||
|
||||
"""
|
||||
items_mapping = {}
|
||||
|
||||
for file_path in load_folder_files(folder_path):
|
||||
items_mapping[file_path] = load_file(file_path)
|
||||
|
||||
return items_mapping
|
||||
|
||||
|
||||
def load_api_folder(api_folder_path=None):
|
||||
""" load api definitions from api folder.
|
||||
|
||||
Args:
|
||||
api_folder_path (str): api files folder.
|
||||
|
||||
api file should be in the following format:
|
||||
[
|
||||
{
|
||||
"api": {
|
||||
"def": "api_login",
|
||||
"request": {},
|
||||
"validate": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"api": {
|
||||
"def": "api_logout",
|
||||
"request": {},
|
||||
"validate": []
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Returns:
|
||||
dict: api definition mapping.
|
||||
|
||||
{
|
||||
"api_login": {
|
||||
"function_meta": {"func_name": "api_login", "args": [], "kwargs": {}}
|
||||
"request": {}
|
||||
},
|
||||
"api_logout": {
|
||||
"function_meta": {"func_name": "api_logout", "args": [], "kwargs": {}}
|
||||
"request": {}
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
api_definition_mapping = {}
|
||||
|
||||
api_folder_path = api_folder_path or os.path.join(os.getcwd(), "api")
|
||||
api_items_mapping = load_folder_content(api_folder_path)
|
||||
|
||||
for api_file_path, api_items in api_items_mapping.items():
|
||||
# TODO: add JSON schema validation
|
||||
for api_item in api_items:
|
||||
key, api_dict = api_item.popitem()
|
||||
|
||||
api_def = api_dict.pop("def")
|
||||
function_meta = parser.parse_function(api_def)
|
||||
func_name = function_meta["func_name"]
|
||||
|
||||
if func_name in api_definition_mapping:
|
||||
logger.log_warning("API definition duplicated: {}".format(func_name))
|
||||
|
||||
api_dict["function_meta"] = function_meta
|
||||
api_definition_mapping[func_name] = api_dict
|
||||
|
||||
project_mapping["def-api"] = api_definition_mapping
|
||||
return api_definition_mapping
|
||||
|
||||
|
||||
def load_test_folder(test_folder_path=None):
|
||||
""" load testcases definitions from folder.
|
||||
|
||||
Args:
|
||||
test_folder_path (str): testcases files folder.
|
||||
|
||||
testcase file should be in the following format:
|
||||
[
|
||||
{
|
||||
"config": {
|
||||
"def": "create_and_check",
|
||||
"request": {},
|
||||
"validate": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"test": {
|
||||
"api": "get_user",
|
||||
"validate": []
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Returns:
|
||||
dict: testcases definition mapping.
|
||||
|
||||
{
|
||||
"create_and_check": [
|
||||
{"config": {}},
|
||||
{"test": {}},
|
||||
{"test": {}}
|
||||
],
|
||||
"tests/testcases/create_and_get.yml": [
|
||||
{"config": {}},
|
||||
{"test": {}},
|
||||
{"test": {}}
|
||||
]
|
||||
}
|
||||
|
||||
"""
|
||||
test_definition_mapping = {}
|
||||
|
||||
# TODO: replace suite with testcases
|
||||
test_folder_path = test_folder_path or os.path.join(os.getcwd(), "suite")
|
||||
test_items_mapping = load_folder_content(test_folder_path)
|
||||
|
||||
for test_file_path, items in test_items_mapping.items():
|
||||
# TODO: add JSON schema validation
|
||||
|
||||
testcase = {
|
||||
"config": {
|
||||
"path": test_file_path
|
||||
},
|
||||
"teststeps": []
|
||||
}
|
||||
for item in items:
|
||||
key, block = item.popitem()
|
||||
|
||||
if key == "config":
|
||||
testcase["config"].update(block)
|
||||
|
||||
if "def" not in block:
|
||||
test_definition_mapping[test_file_path] = testcase
|
||||
continue
|
||||
|
||||
testcase_def = block.pop("def")
|
||||
function_meta = parser.parse_function(testcase_def)
|
||||
func_name = function_meta["func_name"]
|
||||
|
||||
if func_name in test_definition_mapping:
|
||||
logger.log_warning("API definition duplicated: {}".format(func_name))
|
||||
|
||||
testcase["function_meta"] = function_meta
|
||||
test_definition_mapping[func_name] = testcase
|
||||
else:
|
||||
# key == "test":
|
||||
testcase["teststeps"].append(block)
|
||||
|
||||
project_mapping["def-testcase"] = test_definition_mapping
|
||||
return test_definition_mapping
|
||||
|
||||
|
||||
def load_project_tests(folder_path=None):
|
||||
""" load api, testcases and debugtalk.py module.
|
||||
|
||||
Args:
|
||||
folder_path (str): folder path.
|
||||
If not set, defautls to current working directory.
|
||||
|
||||
Returns:
|
||||
dict: project tests mapping.
|
||||
|
||||
"""
|
||||
folder_path = folder_path or os.getcwd()
|
||||
|
||||
load_debugtalk_module(folder_path)
|
||||
load_api_folder(os.path.join(folder_path, "api"))
|
||||
load_test_folder(os.path.join(folder_path, "suite"))
|
||||
|
||||
return project_mapping
|
||||
|
||||
|
||||
def load_testcases(path):
|
||||
""" load testcases from file path
|
||||
@param path: path could be in several type
|
||||
- absolute/relative file path
|
||||
- absolute/relative folder path
|
||||
- list/set container with file(s) and/or folder(s)
|
||||
@return testcases list, each testcase is corresponding to a file
|
||||
|
||||
Args:
|
||||
path (str): testcase file/foler path.
|
||||
path could be in several types:
|
||||
- absolute/relative file path
|
||||
- absolute/relative folder path
|
||||
- list/set container with file(s) and/or folder(s)
|
||||
|
||||
Returns:
|
||||
list: testcases list, each testcase is corresponding to a file
|
||||
[
|
||||
testcase_dict_1,
|
||||
testcase_dict_2
|
||||
]
|
||||
|
||||
"""
|
||||
if isinstance(path, (list, set)):
|
||||
testcases_list = []
|
||||
@@ -701,7 +942,7 @@ def load_testcases(path):
|
||||
elif os.path.isfile(path):
|
||||
try:
|
||||
testcase = _load_test_file(path)
|
||||
if testcase["testcases"]:
|
||||
if testcase["teststeps"]:
|
||||
testcases_list = [testcase]
|
||||
else:
|
||||
testcases_list = []
|
||||
@@ -715,15 +956,3 @@ def load_testcases(path):
|
||||
|
||||
testcases_cache_mapping[path] = testcases_list
|
||||
return testcases_list
|
||||
|
||||
|
||||
def load(path):
|
||||
""" main interface for loading testcases
|
||||
@param (str) path: testcase file/folder path
|
||||
@return (list) testcases list
|
||||
"""
|
||||
if validator.is_testcases(path):
|
||||
return path
|
||||
|
||||
_load_test_dependencies()
|
||||
return load_testcases(path)
|
||||
|
||||
@@ -40,7 +40,7 @@ def gen_locustfile(testcase_file_path):
|
||||
"templates",
|
||||
"locustfile_template"
|
||||
)
|
||||
testcases = loader.load(testcase_file_path)
|
||||
testcases = loader.load_testcases(testcase_file_path)
|
||||
host = testcases[0].get("config", {}).get("request", {}).get("base_url", "")
|
||||
|
||||
with io.open(template_path, encoding='utf-8') as template:
|
||||
|
||||
@@ -4,7 +4,8 @@ import copy
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from httprunner import context, exceptions, loader, logger, runner, utils
|
||||
from httprunner import (context, exceptions, loader, logger, runner, utils,
|
||||
validator)
|
||||
from httprunner.compat import is_py3
|
||||
from httprunner.report import (HtmlTestResult, get_platform, get_summary,
|
||||
render_html_report)
|
||||
@@ -33,39 +34,39 @@ class TestCase(unittest.TestCase):
|
||||
|
||||
|
||||
class TestSuite(unittest.TestSuite):
|
||||
""" create test suite with a testset, it may include one or several testcases.
|
||||
each suite should initialize a separate Runner() with testset config.
|
||||
@param
|
||||
(dict) testset
|
||||
""" create test suite with a testcase, it may include one or several teststeps.
|
||||
each suite should initialize a separate Runner() with testcase config.
|
||||
|
||||
Args:
|
||||
testcase (dict): testcase dict
|
||||
{
|
||||
"name": "testset description",
|
||||
"config": {
|
||||
"name": "testset description",
|
||||
"name": "testcase description",
|
||||
"parameters": {},
|
||||
"variables": [],
|
||||
"request": {},
|
||||
"output": []
|
||||
},
|
||||
"testcases": [
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "testcase description",
|
||||
"name": "teststep1 description",
|
||||
"parameters": {},
|
||||
"variables": [], # optional, override
|
||||
"request": {},
|
||||
"extract": {}, # optional
|
||||
"validate": {} # optional
|
||||
},
|
||||
testcase12
|
||||
teststep2
|
||||
]
|
||||
}
|
||||
(dict) variables_mapping:
|
||||
passed in variables mapping, it will override variables in config block
|
||||
variables_mapping (dict): passed in variables mapping, it will override variables in config block.
|
||||
|
||||
"""
|
||||
def __init__(self, testset, variables_mapping=None, http_client_session=None):
|
||||
def __init__(self, testcase, variables_mapping=None, http_client_session=None):
|
||||
super(TestSuite, self).__init__()
|
||||
self.test_runner_list = []
|
||||
|
||||
self.config = testset.get("config", {})
|
||||
self.config = testcase.get("config", {})
|
||||
self.output_variables_list = self.config.get("output", [])
|
||||
self.testset_file_path = self.config.get("path")
|
||||
config_dict_parameters = self.config.get("parameters", [])
|
||||
@@ -79,22 +80,22 @@ class TestSuite(unittest.TestSuite):
|
||||
config_dict_parameters
|
||||
)
|
||||
self.testcase_parser = context.TestcaseParser()
|
||||
testcases = testset.get("testcases", [])
|
||||
teststeps = testcase.get("teststeps", [])
|
||||
|
||||
for config_variables in config_parametered_variables_list:
|
||||
# config level
|
||||
self.config["variables"] = config_variables
|
||||
test_runner = runner.Runner(self.config, http_client_session)
|
||||
|
||||
for testcase_dict in testcases:
|
||||
testcase_dict = copy.copy(testcase_dict)
|
||||
for teststep_dict in teststeps:
|
||||
teststep_dict = copy.copy(teststep_dict)
|
||||
# testcase level
|
||||
testcase_parametered_variables_list = self._get_parametered_variables(
|
||||
testcase_dict.get("variables", []),
|
||||
testcase_dict.get("parameters", [])
|
||||
teststep_dict.get("variables", []),
|
||||
teststep_dict.get("parameters", [])
|
||||
)
|
||||
for testcase_variables in testcase_parametered_variables_list:
|
||||
testcase_dict["variables"] = testcase_variables
|
||||
teststep_dict["variables"] = testcase_variables
|
||||
|
||||
# eval testcase name with bind variables
|
||||
variables = utils.override_variables_binds(
|
||||
@@ -103,13 +104,13 @@ class TestSuite(unittest.TestSuite):
|
||||
)
|
||||
self.testcase_parser.update_binded_variables(variables)
|
||||
try:
|
||||
testcase_name = self.testcase_parser.eval_content_with_bindings(testcase_dict["name"])
|
||||
testcase_name = self.testcase_parser.eval_content_with_bindings(teststep_dict["name"])
|
||||
except (AssertionError, exceptions.ParamsError):
|
||||
logger.log_warning("failed to eval testcase name: {}".format(testcase_dict["name"]))
|
||||
testcase_name = testcase_dict["name"]
|
||||
logger.log_warning("failed to eval testcase name: {}".format(teststep_dict["name"]))
|
||||
testcase_name = teststep_dict["name"]
|
||||
self.test_runner_list.append((test_runner, variables))
|
||||
|
||||
self._add_test_to_suite(testcase_name, test_runner, testcase_dict)
|
||||
self._add_test_to_suite(testcase_name, test_runner, teststep_dict)
|
||||
|
||||
def _get_parametered_variables(self, variables, parameters):
|
||||
""" parameterize varaibles with parameters
|
||||
@@ -159,38 +160,47 @@ class TestSuite(unittest.TestSuite):
|
||||
return outputs
|
||||
|
||||
|
||||
def init_test_suites(path_or_testsets, mapping=None, http_client_session=None):
|
||||
""" initialize TestSuite list with testset path or testset dict
|
||||
@params
|
||||
testsets (dict/list): testset or list of testset
|
||||
testset_dict
|
||||
def init_test_suites(path_or_testcases, mapping=None, http_client_session=None):
|
||||
""" initialize TestSuite list with testcase path or testcase(s).
|
||||
|
||||
Args:
|
||||
path_or_testcases (str/dict/list): testcase file path or testcase dict or testcases list
|
||||
|
||||
testcase_dict
|
||||
or
|
||||
[
|
||||
testset_dict_1,
|
||||
testset_dict_2,
|
||||
testcase_dict_1,
|
||||
testcase_dict_2,
|
||||
{
|
||||
"config": {},
|
||||
"api": {},
|
||||
"testcases": [testcase11, testcase12]
|
||||
"teststeps": [teststep11, teststep12]
|
||||
}
|
||||
]
|
||||
mapping (dict):
|
||||
passed in variables mapping, it will override variables in config block
|
||||
|
||||
mapping (dict): passed in variables mapping, it will override variables in config block.
|
||||
http_client_session (instance): requests.Session(), or locusts.client.Session() instance.
|
||||
|
||||
Returns:
|
||||
list: TestSuite() instance list.
|
||||
|
||||
"""
|
||||
testsets = loader.load(path_or_testsets)
|
||||
if validator.is_testcases(path_or_testcases):
|
||||
testcases = path_or_testcases
|
||||
else:
|
||||
testcases = loader.load_testcases(path_or_testcases)
|
||||
|
||||
# TODO: move comparator uniform here
|
||||
mapping = mapping or {}
|
||||
|
||||
if not testsets:
|
||||
if not testcases:
|
||||
raise exceptions.TestcaseNotFound
|
||||
|
||||
if isinstance(testsets, dict):
|
||||
testsets = [testsets]
|
||||
if isinstance(testcases, dict):
|
||||
testcases = [testcases]
|
||||
|
||||
test_suite_list = []
|
||||
for testset in testsets:
|
||||
test_suite = TestSuite(testset, mapping, http_client_session)
|
||||
for testcase in testcases:
|
||||
test_suite = TestSuite(testcase, mapping, http_client_session)
|
||||
test_suite_list.append(test_suite)
|
||||
|
||||
return test_suite_list
|
||||
@@ -199,39 +209,55 @@ def init_test_suites(path_or_testsets, mapping=None, http_client_session=None):
|
||||
class HttpRunner(object):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" initialize test runner
|
||||
@param (dict) kwargs: key-value arguments used to initialize TextTestRunner
|
||||
- resultclass: HtmlTestResult or TextTestResult
|
||||
- failfast: False/True, stop the test run on the first error or failure.
|
||||
- dot_env_path: .env file path
|
||||
""" initialize HttpRunner.
|
||||
|
||||
Args:
|
||||
kwargs (dict): key-value arguments used to initialize TextTestRunner.
|
||||
Commonly used arguments:
|
||||
|
||||
resultclass (class): HtmlTestResult or TextTestResult
|
||||
failfast (bool): False/True, stop the test run on the first error or failure.
|
||||
dot_env_path (str): .env file path.
|
||||
|
||||
Attributes:
|
||||
project_mapping (dict): save project loaded api/testcases, environments and debugtalk.py module.
|
||||
|
||||
"""
|
||||
dot_env_path = kwargs.pop("dot_env_path", None)
|
||||
utils.set_os_environ(loader.load_dot_env_file(dot_env_path))
|
||||
loader.load_dot_env_file(dot_env_path)
|
||||
loader.load_project_tests("tests") # TODO: remove tests
|
||||
self.project_mapping = loader.project_mapping
|
||||
utils.set_os_environ(self.project_mapping["env"])
|
||||
|
||||
kwargs.setdefault("resultclass", HtmlTestResult)
|
||||
self.runner = unittest.TextTestRunner(**kwargs)
|
||||
|
||||
def run(self, path_or_testsets, mapping=None):
|
||||
""" start to run test with varaibles mapping
|
||||
@param path_or_testsets: YAML/JSON testset file path or testset list
|
||||
path: path could be in several type
|
||||
- absolute/relative file path
|
||||
- absolute/relative folder path
|
||||
- list/set container with file(s) and/or folder(s)
|
||||
testsets: testset or list of testset
|
||||
- (dict) testset_dict
|
||||
- (list) list of testset_dict
|
||||
[
|
||||
testset_dict_1,
|
||||
testset_dict_2
|
||||
]
|
||||
@param (dict) mapping:
|
||||
if mapping specified, it will override variables in config block
|
||||
def run(self, path_or_testcases, mapping=None):
|
||||
""" start to run test with varaibles mapping.
|
||||
|
||||
Args:
|
||||
path_or_testcases (str/list/dict): YAML/JSON testcase file path or testcase list
|
||||
path: path could be in several type
|
||||
- absolute/relative file path
|
||||
- absolute/relative folder path
|
||||
- list/set container with file(s) and/or folder(s)
|
||||
testcases: testcase dict or list of testcases
|
||||
- (dict) testset_dict
|
||||
- (list) list of testset_dict
|
||||
[
|
||||
testset_dict_1,
|
||||
testset_dict_2
|
||||
]
|
||||
mapping (dict): if mapping specified, it will override variables in config block.
|
||||
|
||||
Returns:
|
||||
instance: HttpRunner() instance
|
||||
|
||||
"""
|
||||
try:
|
||||
test_suite_list = init_test_suites(path_or_testsets, mapping)
|
||||
test_suite_list = init_test_suites(path_or_testcases, mapping)
|
||||
except exceptions.TestcaseNotFound:
|
||||
logger.log_error("Testcases not found in {}".format(path_or_testsets))
|
||||
logger.log_error("Testcases not found in {}".format(path_or_testcases))
|
||||
sys.exit(1)
|
||||
|
||||
self.summary = {
|
||||
@@ -243,8 +269,7 @@ class HttpRunner(object):
|
||||
}
|
||||
|
||||
def accumulate_stat(origin_stat, new_stat):
|
||||
""" accumulate new_stat to origin_stat
|
||||
"""
|
||||
"""accumulate new_stat to origin_stat."""
|
||||
for key in new_stat:
|
||||
if key not in origin_stat:
|
||||
origin_stat[key] = new_stat[key]
|
||||
@@ -272,11 +297,15 @@ class HttpRunner(object):
|
||||
return self
|
||||
|
||||
def gen_html_report(self, html_report_name=None, html_report_template=None):
|
||||
""" generate html report and return report path
|
||||
@param (str) html_report_name:
|
||||
output html report file name
|
||||
@param (str) html_report_template:
|
||||
report template file path, template should be in Jinja2 format
|
||||
""" generate html report and return report path.
|
||||
|
||||
Args:
|
||||
html_report_name (str): output html report file name
|
||||
html_report_template (str): report template file path, template should be in Jinja2 format
|
||||
|
||||
Returns:
|
||||
str: generated html report path
|
||||
|
||||
"""
|
||||
return render_html_report(
|
||||
self.summary,
|
||||
@@ -287,8 +316,8 @@ class HttpRunner(object):
|
||||
|
||||
class LocustTask(object):
|
||||
|
||||
def __init__(self, path_or_testsets, locust_client, mapping=None):
|
||||
self.test_suite_list = init_test_suites(path_or_testsets, mapping, locust_client)
|
||||
def __init__(self, path_or_testcases, locust_client, mapping=None):
|
||||
self.test_suite_list = init_test_suites(path_or_testcases, mapping, locust_client)
|
||||
|
||||
def run(self):
|
||||
for test_suite in self.test_suite_list:
|
||||
|
||||
@@ -1,34 +1,16 @@
|
||||
# encoding: utf-8
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import hmac
|
||||
import io
|
||||
import itertools
|
||||
import json
|
||||
import os.path
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime
|
||||
|
||||
from httprunner import exceptions, logger
|
||||
from httprunner.compat import OrderedDict, basestring, is_py2
|
||||
|
||||
SECRET_KEY = "DebugTalk"
|
||||
|
||||
|
||||
def gen_random_string(str_len):
|
||||
return ''.join(
|
||||
random.choice(string.ascii_letters + string.digits) for _ in range(str_len))
|
||||
|
||||
def gen_md5(*str_args):
|
||||
return hashlib.md5("".join(str_args).encode('utf-8')).hexdigest()
|
||||
|
||||
def get_sign(*args):
|
||||
content = ''.join(args).encode('ascii')
|
||||
sign_key = SECRET_KEY.encode('ascii')
|
||||
sign = hmac.new(sign_key, content, hashlib.sha1).hexdigest()
|
||||
return sign
|
||||
|
||||
def remove_prefix(text, prefix):
|
||||
""" remove prefix from text
|
||||
|
||||
@@ -6,35 +6,48 @@ TODO: refactor with JSON schema validate
|
||||
"""
|
||||
|
||||
def is_testcase(data_structure):
|
||||
""" check if data_structure is a testcase
|
||||
testcase should always be in the following data structure:
|
||||
{
|
||||
"name": "desc1",
|
||||
"config": {},
|
||||
"api": {},
|
||||
"testcases": [testcase11, testcase12]
|
||||
}
|
||||
""" check if data_structure is a testcase.
|
||||
|
||||
Args:
|
||||
data_structure (dict): testcase should always be in the following data structure:
|
||||
|
||||
{
|
||||
"name": "desc1",
|
||||
"config": {},
|
||||
"api": {},
|
||||
"testcases": [testcase11, testcase12]
|
||||
}
|
||||
|
||||
Returns:
|
||||
bool: True if data_structure is valid testcase, otherwise False.
|
||||
|
||||
"""
|
||||
if not isinstance(data_structure, dict):
|
||||
return False
|
||||
|
||||
if "name" not in data_structure or "testcases" not in data_structure:
|
||||
if "name" not in data_structure or "teststeps" not in data_structure:
|
||||
return False
|
||||
|
||||
if not isinstance(data_structure["testcases"], list):
|
||||
if not isinstance(data_structure["teststeps"], list):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_testcases(data_structure):
|
||||
""" check if data_structure is testcase or testcases list
|
||||
testsets should always be in the following data structure:
|
||||
testset_dict
|
||||
or
|
||||
[
|
||||
testset_dict_1,
|
||||
testset_dict_2
|
||||
]
|
||||
""" check if data_structure is testcase or testcases list.
|
||||
|
||||
Args:
|
||||
data_structure (dict): testcase(s) should always be in the following data structure:
|
||||
|
||||
testcase_dict
|
||||
or
|
||||
[
|
||||
testcase_dict_1,
|
||||
testcase_dict_2
|
||||
]
|
||||
Returns:
|
||||
bool: True if data_structure is valid testcase(s), otherwise False.
|
||||
|
||||
"""
|
||||
if not isinstance(data_structure, list):
|
||||
return is_testcase(data_structure)
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
from functools import wraps
|
||||
|
||||
from httprunner import utils
|
||||
from flask import Flask, make_response, request
|
||||
from httprunner.built_in import gen_random_string
|
||||
|
||||
try:
|
||||
from httpbin import app as httpbin_app
|
||||
HTTPBIN_HOST = "127.0.0.1"
|
||||
HTTPBIN_PORT = 3458
|
||||
except ImportError:
|
||||
httpbin_app = None
|
||||
HTTPBIN_HOST = "httpbin.org"
|
||||
HTTPBIN_PORT = 80
|
||||
|
||||
FLASK_APP_PORT = 5000
|
||||
HTTPBIN_SERVER = "http://{}:{}".format(HTTPBIN_HOST, HTTPBIN_PORT)
|
||||
SECRET_KEY = "DebugTalk"
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@@ -31,6 +45,17 @@ data structure:
|
||||
"""
|
||||
token_dict = {}
|
||||
|
||||
|
||||
def get_sign(*args):
|
||||
content = ''.join(args).encode('ascii')
|
||||
sign_key = SECRET_KEY.encode('ascii')
|
||||
sign = hmac.new(sign_key, content, hashlib.sha1).hexdigest()
|
||||
return sign
|
||||
|
||||
def gen_md5(*args):
|
||||
return hashlib.md5("".join(args).encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def validate_request(func):
|
||||
|
||||
@wraps(func)
|
||||
@@ -74,7 +99,7 @@ def get_token():
|
||||
data = request.get_json()
|
||||
sign = data.get('sign', "")
|
||||
|
||||
expected_sign = utils.get_sign(user_agent, device_sn, os_platform, app_version)
|
||||
expected_sign = get_sign(user_agent, device_sn, os_platform, app_version)
|
||||
|
||||
if expected_sign != sign:
|
||||
result = {
|
||||
@@ -83,7 +108,7 @@ def get_token():
|
||||
}
|
||||
response = make_response(json.dumps(result), 403)
|
||||
else:
|
||||
token = utils.gen_random_string(16)
|
||||
token = gen_random_string(16)
|
||||
token_dict[device_sn] = token
|
||||
|
||||
result = {
|
||||
|
||||
@@ -3,26 +3,17 @@ import time
|
||||
import unittest
|
||||
|
||||
import requests
|
||||
from httprunner import utils
|
||||
from tests.api_server import FLASK_APP_PORT, HTTPBIN_HOST, HTTPBIN_PORT
|
||||
from tests.api_server import app as flask_app
|
||||
|
||||
try:
|
||||
from httpbin import app as httpbin_app
|
||||
HTTPBIN_HOST = "127.0.0.1"
|
||||
HTTPBIN_PORT = 3458
|
||||
except ImportError:
|
||||
HTTPBIN_HOST = "httpbin.org"
|
||||
HTTPBIN_PORT = 80
|
||||
|
||||
FLASK_APP_PORT = 5000
|
||||
HTTPBIN_SERVER = "http://{}:{}".format(HTTPBIN_HOST, HTTPBIN_PORT)
|
||||
from tests.api_server import gen_md5, gen_random_string, get_sign, httpbin_app
|
||||
|
||||
|
||||
def run_flask():
|
||||
flask_app.run(port=FLASK_APP_PORT)
|
||||
|
||||
|
||||
def run_httpbin():
|
||||
if HTTPBIN_HOST == "127.0.0.1":
|
||||
if httpbin_app:
|
||||
httpbin_app.run(host=HTTPBIN_HOST, port=HTTPBIN_PORT)
|
||||
|
||||
|
||||
@@ -59,7 +50,7 @@ class ApiServerUnittest(unittest.TestCase):
|
||||
'app_version': app_version
|
||||
}
|
||||
data = {
|
||||
'sign': utils.get_sign(user_agent, device_sn, os_platform, app_version)
|
||||
'sign': get_sign(user_agent, device_sn, os_platform, app_version)
|
||||
}
|
||||
|
||||
resp = self.api_client.post(url, json=data, headers=headers)
|
||||
@@ -71,7 +62,7 @@ class ApiServerUnittest(unittest.TestCase):
|
||||
|
||||
def get_authenticated_headers(self):
|
||||
user_agent = 'iOS/10.3'
|
||||
device_sn = utils.gen_random_string(15)
|
||||
device_sn = gen_random_string(15)
|
||||
os_platform = 'ios'
|
||||
app_version = '2.8.6'
|
||||
|
||||
|
||||
@@ -1,34 +1,13 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
|
||||
from tests.base import HTTPBIN_SERVER
|
||||
from tests.api_server import HTTPBIN_SERVER, SECRET_KEY, gen_md5, get_sign
|
||||
|
||||
try:
|
||||
import urllib
|
||||
except NameError:
|
||||
import urllib.parse as urllib
|
||||
|
||||
SECRET_KEY = "DebugTalk"
|
||||
BASE_URL = "http://127.0.0.1:5000"
|
||||
|
||||
def get_sign(*args):
|
||||
content = ''.join(args).encode('ascii')
|
||||
sign_key = SECRET_KEY.encode('ascii')
|
||||
sign = hmac.new(sign_key, content, hashlib.sha1).hexdigest()
|
||||
return sign
|
||||
|
||||
get_sign_lambda = lambda *args: hmac.new(
|
||||
'DebugTalk'.encode('ascii'),
|
||||
''.join(args).encode('ascii'),
|
||||
hashlib.sha1).hexdigest()
|
||||
|
||||
def gen_md5(*args):
|
||||
return hashlib.md5("".join(args).encode('utf-8')).hexdigest()
|
||||
|
||||
def sum_status_code(status_code, expect_sum):
|
||||
""" sum status code digits
|
||||
@@ -62,8 +41,6 @@ def get_account():
|
||||
{"username": "user2", "password": "222222"}
|
||||
]
|
||||
|
||||
SECRET_KEY = "DebugTalk"
|
||||
|
||||
def gen_random_string(str_len):
|
||||
random_char_list = []
|
||||
for _ in range(str_len):
|
||||
|
||||
@@ -4,7 +4,6 @@ import unittest
|
||||
|
||||
import requests
|
||||
from httprunner import context, exceptions, loader, parser, response, runner
|
||||
from httprunner.utils import gen_md5
|
||||
from tests.base import ApiServerUnittest
|
||||
|
||||
|
||||
@@ -120,6 +119,7 @@ class TestContext(ApiServerUnittest):
|
||||
{"authorization": "${gen_md5($TOKEN, $data, $random)}"}
|
||||
]
|
||||
from tests import debugtalk
|
||||
from tests.debugtalk import gen_md5
|
||||
self.context.import_module_items(debugtalk)
|
||||
self.context.bind_variables(variables)
|
||||
context_variables = self.context.testcase_variables_mapping
|
||||
|
||||
@@ -2,7 +2,8 @@ import os
|
||||
import shutil
|
||||
|
||||
from httprunner import HttpRunner
|
||||
from tests.base import HTTPBIN_SERVER, ApiServerUnittest
|
||||
from tests.api_server import HTTPBIN_SERVER
|
||||
from tests.base import ApiServerUnittest
|
||||
|
||||
|
||||
class TestHttpRunner(ApiServerUnittest):
|
||||
@@ -22,7 +23,7 @@ class TestHttpRunner(ApiServerUnittest):
|
||||
'output': ['token']
|
||||
},
|
||||
'api': {},
|
||||
'testcases': [
|
||||
'teststeps': [
|
||||
{
|
||||
'name': '/api/get-token',
|
||||
'request': {
|
||||
@@ -113,7 +114,7 @@ class TestHttpRunner(ApiServerUnittest):
|
||||
testsets = [
|
||||
{
|
||||
"name": "post data",
|
||||
"testcases": [
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "post data",
|
||||
"request": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from httprunner import exceptions, loader, validator
|
||||
from httprunner import exceptions, loader, task, validator
|
||||
|
||||
|
||||
class TestFileLoader(unittest.TestCase):
|
||||
@@ -197,17 +197,17 @@ class TestModuleLoader(unittest.TestCase):
|
||||
from httprunner import utils
|
||||
module_mapping = loader.load_python_module(utils)
|
||||
|
||||
gen_md5 = loader.get_module_item(module_mapping, "functions", "gen_md5")
|
||||
self.assertTrue(validator.is_function(("gen_md5", gen_md5)))
|
||||
self.assertEqual(gen_md5("abc"), "900150983cd24fb0d6963f7d28e17f72")
|
||||
get_uniform_comparator = loader.get_module_item(
|
||||
module_mapping, "functions", "get_uniform_comparator")
|
||||
self.assertTrue(validator.is_function(("get_uniform_comparator", get_uniform_comparator)))
|
||||
self.assertEqual(get_uniform_comparator("=="), "equals")
|
||||
|
||||
with self.assertRaises(exceptions.FunctionNotFound):
|
||||
loader.get_module_item(module_mapping, "functions", "gen_md4")
|
||||
|
||||
def test_get_module_item_variables(self):
|
||||
from httprunner import utils
|
||||
module_mapping = loader.load_python_module(utils)
|
||||
|
||||
from tests import debugtalk
|
||||
module_mapping = loader.load_python_module(debugtalk)
|
||||
|
||||
SECRET_KEY = loader.get_module_item(module_mapping, "variables", "SECRET_KEY")
|
||||
self.assertTrue(validator.is_variable(("SECRET_KEY", SECRET_KEY)))
|
||||
@@ -219,60 +219,34 @@ class TestModuleLoader(unittest.TestCase):
|
||||
|
||||
class TestSuiteLoader(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
loader.overall_def_dict = {
|
||||
"api": {},
|
||||
"suite": {}
|
||||
}
|
||||
|
||||
def test_load_test_dependencies(self):
|
||||
loader._load_test_dependencies()
|
||||
overall_def_dict = loader.overall_def_dict
|
||||
self.assertIn("get_token", overall_def_dict["api"])
|
||||
self.assertIn("create_and_check", overall_def_dict["suite"])
|
||||
|
||||
def test_load_api_file(self):
|
||||
loader._load_api_file("tests/api/basic.yml")
|
||||
overall_api_def_dict = loader.overall_def_dict["api"]
|
||||
self.assertIn("get_token",overall_api_def_dict)
|
||||
self.assertEqual("/api/get-token", overall_api_def_dict["get_token"]["request"]["url"])
|
||||
self.assertIn("$user_agent", overall_api_def_dict["get_token"]["function_meta"]["args"])
|
||||
self.assertEqual(len(overall_api_def_dict["get_token"]["validate"]), 3)
|
||||
|
||||
def test_load_test_file_suite(self):
|
||||
loader._load_api_file("tests/api/basic.yml")
|
||||
testset = loader._load_test_file("tests/suite/create_and_get.yml")
|
||||
self.assertEqual(testset["config"]["name"], "create user and check result.")
|
||||
self.assertEqual(len(testset["testcases"]), 3)
|
||||
self.assertEqual(testset["testcases"][0]["name"], "make sure user $uid does not exist")
|
||||
self.assertEqual(testset["testcases"][0]["request"]["url"], "/api/users/$uid")
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
project_dir = os.path.join(os.getcwd(), "tests")
|
||||
loader.load_project_tests(project_dir)
|
||||
|
||||
def test_load_test_file_testcase(self):
|
||||
loader._load_test_dependencies()
|
||||
testset = loader._load_test_file("tests/testcases/smoketest.yml")
|
||||
self.assertEqual(testset["config"]["name"], "smoketest")
|
||||
self.assertEqual(testset["config"]["path"], "tests/testcases/smoketest.yml")
|
||||
self.assertIn("device_sn", testset["config"]["variables"][0])
|
||||
self.assertEqual(len(testset["testcases"]), 8)
|
||||
self.assertEqual(testset["testcases"][0]["name"], "get token")
|
||||
testcase = loader._load_test_file("tests/testcases/smoketest.yml")
|
||||
self.assertEqual(testcase["config"]["name"], "smoketest")
|
||||
self.assertEqual(testcase["config"]["path"], "tests/testcases/smoketest.yml")
|
||||
self.assertIn("device_sn", testcase["config"]["variables"][0])
|
||||
self.assertEqual(len(testcase["teststeps"]), 8)
|
||||
self.assertEqual(testcase["teststeps"][0]["name"], "get token")
|
||||
|
||||
def test_get_block_by_name(self):
|
||||
loader._load_test_dependencies()
|
||||
ref_call = "get_user($uid, $token)"
|
||||
block = loader._get_block_by_name(ref_call, "api")
|
||||
block = loader._get_block_by_name(ref_call, "def-api")
|
||||
self.assertEqual(block["request"]["url"], "/api/users/$uid")
|
||||
self.assertEqual(block["function_meta"]["func_name"], "get_user")
|
||||
self.assertEqual(block["function_meta"]["args"], ['$uid', '$token'])
|
||||
|
||||
def test_get_block_by_name_args_mismatch(self):
|
||||
loader._load_test_dependencies()
|
||||
ref_call = "get_user($uid, $token, $var)"
|
||||
with self.assertRaises(exceptions.ParamsError):
|
||||
loader._get_block_by_name(ref_call, "api")
|
||||
loader._get_block_by_name(ref_call, "def-api")
|
||||
|
||||
def test_override_block(self):
|
||||
loader._load_test_dependencies()
|
||||
def_block = loader._get_block_by_name("get_token($user_agent, $device_sn, $os_platform, $app_version)", "api")
|
||||
def_block = loader._get_block_by_name(
|
||||
"get_token($user_agent, $device_sn, $os_platform, $app_version)", "def-api")
|
||||
test_block = {
|
||||
"name": "override block",
|
||||
"variables": [
|
||||
@@ -286,28 +260,26 @@ class TestSuiteLoader(unittest.TestCase):
|
||||
]
|
||||
}
|
||||
|
||||
loader._override_block(def_block, test_block)
|
||||
loader._extend_block(test_block, def_block)
|
||||
self.assertEqual(test_block["name"], "override block")
|
||||
self.assertIn({'check': 'status_code', 'expect': 201, 'comparator': 'eq'}, test_block["validate"])
|
||||
self.assertIn({'check': 'content.token', 'comparator': 'len_eq', 'expect': 32}, test_block["validate"])
|
||||
|
||||
def test_get_test_definition_api(self):
|
||||
loader._load_test_dependencies()
|
||||
api_def = loader._get_test_definition("get_headers", "api")
|
||||
api_def = loader._get_test_definition("get_headers", "def-api")
|
||||
self.assertEqual(api_def["request"]["url"], "/headers")
|
||||
self.assertEqual(len(api_def["setup_hooks"]), 2)
|
||||
self.assertEqual(len(api_def["teardown_hooks"]), 1)
|
||||
|
||||
with self.assertRaises(exceptions.ApiNotFound):
|
||||
loader._get_test_definition("get_token_XXX", "api")
|
||||
loader._get_test_definition("get_token_XXX", "def-api")
|
||||
|
||||
def test_get_test_definition_suite(self):
|
||||
loader._load_test_dependencies()
|
||||
api_def = loader._get_test_definition("create_and_check", "suite")
|
||||
api_def = loader._get_test_definition("create_and_check", "def-testcase")
|
||||
self.assertEqual(api_def["config"]["name"], "create user and check result.")
|
||||
|
||||
with self.assertRaises(exceptions.TestcaseNotFound):
|
||||
loader._get_test_definition("create_and_check_XXX", "suite")
|
||||
loader._get_test_definition("create_and_check_XXX", "def-testcase")
|
||||
|
||||
def test_merge_validator(self):
|
||||
def_validators = [
|
||||
@@ -376,7 +348,7 @@ class TestSuiteLoader(unittest.TestCase):
|
||||
self.assertEqual(len(testset_list), 1)
|
||||
self.assertIn("path", testset_list[0]["config"])
|
||||
self.assertEqual(testset_list[0]["config"]["path"], path)
|
||||
self.assertEqual(len(testset_list[0]["testcases"]), 3)
|
||||
self.assertEqual(len(testset_list[0]["teststeps"]), 3)
|
||||
testsets_list.extend(testset_list)
|
||||
|
||||
# relative file path
|
||||
@@ -385,7 +357,7 @@ class TestSuiteLoader(unittest.TestCase):
|
||||
self.assertEqual(len(testset_list), 1)
|
||||
self.assertIn("path", testset_list[0]["config"])
|
||||
self.assertIn(path, testset_list[0]["config"]["path"])
|
||||
self.assertEqual(len(testset_list[0]["testcases"]), 3)
|
||||
self.assertEqual(len(testset_list[0]["teststeps"]), 3)
|
||||
testsets_list.extend(testset_list)
|
||||
|
||||
# list/set container with file(s)
|
||||
@@ -395,20 +367,19 @@ class TestSuiteLoader(unittest.TestCase):
|
||||
]
|
||||
testset_list = loader.load_testcases(path)
|
||||
self.assertEqual(len(testset_list), 2)
|
||||
self.assertEqual(len(testset_list[0]["testcases"]), 3)
|
||||
self.assertEqual(len(testset_list[1]["testcases"]), 3)
|
||||
self.assertEqual(len(testset_list[0]["teststeps"]), 3)
|
||||
self.assertEqual(len(testset_list[1]["teststeps"]), 3)
|
||||
testsets_list.extend(testset_list)
|
||||
self.assertEqual(len(testsets_list), 4)
|
||||
|
||||
for testset in testsets_list:
|
||||
for test in testset["testcases"]:
|
||||
for test in testset["teststeps"]:
|
||||
self.assertIn('name', test)
|
||||
self.assertIn('request', test)
|
||||
self.assertIn('url', test['request'])
|
||||
self.assertIn('method', test['request'])
|
||||
|
||||
def test_load_testcases_by_path_folder(self):
|
||||
loader._load_test_dependencies()
|
||||
# absolute folder path
|
||||
path = os.path.join(os.getcwd(), 'tests/data')
|
||||
testset_list_1 = loader.load_testcases(path)
|
||||
@@ -447,12 +418,74 @@ class TestSuiteLoader(unittest.TestCase):
|
||||
loader.load_testcases(path)
|
||||
|
||||
def test_load_testcases_by_path_layered(self):
|
||||
loader._load_test_dependencies()
|
||||
path = os.path.join(
|
||||
os.getcwd(), 'tests/data/demo_testset_layer.yml')
|
||||
testsets_list = loader.load_testcases(path)
|
||||
self.assertIn("variables", testsets_list[0]["config"])
|
||||
self.assertIn("request", testsets_list[0]["config"])
|
||||
self.assertIn("request", testsets_list[0]["testcases"][0])
|
||||
self.assertIn("url", testsets_list[0]["testcases"][0]["request"])
|
||||
self.assertIn("validate", testsets_list[0]["testcases"][0])
|
||||
self.assertIn("request", testsets_list[0]["teststeps"][0])
|
||||
self.assertIn("url", testsets_list[0]["teststeps"][0]["request"])
|
||||
self.assertIn("validate", testsets_list[0]["teststeps"][0])
|
||||
|
||||
def test_load_folder_content(self):
|
||||
path = os.path.join(os.getcwd(), "tests", "api")
|
||||
items_mapping = loader.load_folder_content(path)
|
||||
file_path = os.path.join(os.getcwd(), "tests", "api", "basic.yml")
|
||||
self.assertIn(file_path, items_mapping)
|
||||
self.assertIsInstance(items_mapping[file_path], list)
|
||||
|
||||
def test_load_api_folder(self):
|
||||
path = os.path.join(os.getcwd(), "tests", "api")
|
||||
api_definition_mapping = loader.load_api_folder(path)
|
||||
self.assertIn("get_token", api_definition_mapping)
|
||||
self.assertIn("request", api_definition_mapping["get_token"])
|
||||
self.assertIn("function_meta", api_definition_mapping["get_token"])
|
||||
|
||||
def test_load_testcases_folder(self):
|
||||
path = os.path.join(os.getcwd(), "tests", "suite")
|
||||
testcases_definition_mapping = loader.load_test_folder(path)
|
||||
|
||||
self.assertIn("setup_and_reset", testcases_definition_mapping)
|
||||
self.assertIn("create_and_check", testcases_definition_mapping)
|
||||
self.assertEqual(
|
||||
testcases_definition_mapping["setup_and_reset"]["config"]["name"],
|
||||
"setup and reset all."
|
||||
)
|
||||
self.assertEqual(
|
||||
testcases_definition_mapping["setup_and_reset"]["function_meta"]["func_name"],
|
||||
"setup_and_reset"
|
||||
)
|
||||
|
||||
def test_load_testsuites_folder(self):
|
||||
path = os.path.join(os.getcwd(), "tests", "testcases")
|
||||
testsuites_definition_mapping = loader.load_test_folder(path)
|
||||
|
||||
testsute_path = os.path.join(os.getcwd(), "tests", "testcases", "smoketest.yml")
|
||||
self.assertIn(
|
||||
testsute_path,
|
||||
testsuites_definition_mapping
|
||||
)
|
||||
self.assertEqual(
|
||||
testsuites_definition_mapping[testsute_path]["config"]["name"],
|
||||
"smoketest"
|
||||
)
|
||||
|
||||
def test_load_project_tests(self):
|
||||
project_dir = os.path.join(os.getcwd(), "tests")
|
||||
project_tests = loader.load_project_tests(project_dir)
|
||||
self.assertEqual(project_tests["debugtalk"]["variables"]["SECRET_KEY"], "DebugTalk")
|
||||
self.assertIn("get_token", project_tests["def-api"])
|
||||
self.assertIn("setup_and_reset", project_tests["def-testcase"])
|
||||
|
||||
def test_loader(self):
|
||||
hrunner = task.HttpRunner(dot_env_path="tests/data/test.env")
|
||||
self.assertEqual(hrunner.project_mapping["env"]["PROJECT_KEY"], "ABCDEFGH")
|
||||
self.assertIn("debugtalk", hrunner.project_mapping)
|
||||
self.assertIn("setup_and_reset", hrunner.project_mapping["def-testcase"])
|
||||
self.assertEqual(
|
||||
hrunner.project_mapping["debugtalk"]["variables"]["SECRET_KEY"],
|
||||
"DebugTalk"
|
||||
)
|
||||
self.assertIn("get_sign", hrunner.project_mapping["debugtalk"]["functions"])
|
||||
self.assertIn("get_token", hrunner.project_mapping["def-api"])
|
||||
self.assertIn("setup_and_reset", hrunner.project_mapping["def-testcase"])
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import requests
|
||||
from httprunner import built_in, exceptions, loader, response
|
||||
from httprunner.compat import basestring, bytes
|
||||
from tests.base import HTTPBIN_SERVER, ApiServerUnittest
|
||||
from tests.api_server import HTTPBIN_SERVER
|
||||
from tests.base import ApiServerUnittest
|
||||
|
||||
|
||||
class TestResponse(ApiServerUnittest):
|
||||
|
||||
@@ -3,7 +3,8 @@ import time
|
||||
|
||||
from httprunner import HttpRunner, exceptions, loader, runner
|
||||
from httprunner.utils import deep_update_dict
|
||||
from tests.base import HTTPBIN_SERVER, ApiServerUnittest
|
||||
from tests.api_server import HTTPBIN_SERVER
|
||||
from tests.base import ApiServerUnittest
|
||||
|
||||
|
||||
class TestRunner(ApiServerUnittest):
|
||||
@@ -169,7 +170,7 @@ class TestRunner(ApiServerUnittest):
|
||||
"config": {
|
||||
'path': 'tests/httpbin/hooks.yml',
|
||||
},
|
||||
"testcases": [
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "test teardown hooks",
|
||||
"request": {
|
||||
@@ -205,7 +206,7 @@ class TestRunner(ApiServerUnittest):
|
||||
"config": {
|
||||
'path': 'tests/httpbin/hooks.yml',
|
||||
},
|
||||
"testcases": [
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "test teardown hooks",
|
||||
"request": {
|
||||
@@ -235,7 +236,7 @@ class TestRunner(ApiServerUnittest):
|
||||
"config": {
|
||||
'path': 'tests/httpbin/hooks.yml',
|
||||
},
|
||||
"testcases": [
|
||||
"teststeps": [
|
||||
{
|
||||
"name": "test teardown hooks",
|
||||
"request": {
|
||||
@@ -383,7 +384,7 @@ class TestRunner(ApiServerUnittest):
|
||||
testsets = loader.load_testcases(testcase_file_path)
|
||||
testset = testsets[0]
|
||||
config_dict_headers = testset["config"]["request"]["headers"]
|
||||
test_dict_headers = testset["testcases"][0]["request"]["headers"]
|
||||
test_dict_headers = testset["teststeps"][0]["request"]["headers"]
|
||||
headers = deep_update_dict(
|
||||
config_dict_headers,
|
||||
test_dict_headers
|
||||
|
||||
@@ -37,7 +37,7 @@ class TestTask(ApiServerUnittest):
|
||||
'output': ['token']
|
||||
},
|
||||
'api': {},
|
||||
'testcases': [
|
||||
'teststeps': [
|
||||
{
|
||||
'name': '/api/get-token',
|
||||
'request': {
|
||||
|
||||
Reference in New Issue
Block a user