From e32e89c37974a38038250f6edc4c95b0e8e4041f Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 30 Jul 2018 00:02:05 +0800 Subject: [PATCH] relocate test suite loader --- httprunner/context.py | 4 +- httprunner/loader.py | 255 +++++++++++++++++- httprunner/locusts.py | 6 +- httprunner/parser.py | 111 ++++++++ httprunner/task.py | 8 +- httprunner/testcase.py | 549 +-------------------------------------- httprunner/utils.py | 205 ++++++++++++++- tests/test_httprunner.py | 11 - tests/test_loader.py | 326 +++++++++++++++++++++++ tests/test_parser.py | 68 +++++ tests/test_runner.py | 3 +- tests/test_task.py | 5 +- tests/test_testcase.py | 342 +----------------------- tests/test_utils.py | 84 ++++++ 14 files changed, 1061 insertions(+), 916 deletions(-) create mode 100644 httprunner/parser.py create mode 100644 tests/test_loader.py create mode 100644 tests/test_parser.py diff --git a/httprunner/context.py b/httprunner/context.py index b4be7af5..64900560 100644 --- a/httprunner/context.py +++ b/httprunner/context.py @@ -5,7 +5,7 @@ import os import re import sys -from httprunner import built_in, exceptions, logger, testcase, utils +from httprunner import built_in, exceptions, logger, parser, testcase, utils from httprunner.compat import OrderedDict @@ -251,7 +251,7 @@ class Context(object): for validator in validators: # evaluate validators with context variable mapping. evaluated_validator = self.eval_check_item( - testcase.parse_validator(validator), + parser.parse_validator(validator), resp_obj ) diff --git a/httprunner/loader.py b/httprunner/loader.py index 4366a7de..93593d18 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -4,8 +4,7 @@ import json import os import yaml -from httprunner import exceptions, logger - +from httprunner import exceptions, logger, parser, utils ############################################################################### ## file loader @@ -154,4 +153,254 @@ def load_dot_env_file(path): variable, value = line.split("=") variable = variable.strip() os.environ[variable] = value.strip() - logger.log_debug("Loaded variable: {}".format(variable)) \ No newline at end of file + logger.log_debug("Loaded variable: {}".format(variable)) + + +############################################################################### +## suite 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 suite file + @param file_path: absolute valid file path + file_path should be in format below: + [ + { + "config": { + "name": "", + "def": "suite_order()", + "request": {} + } + }, + { + "test": { + "name": "add product to cart", + "api": "api_add_cart()", + "validate": [] + } + }, + { + "test": { + "name": "checkout cart", + "request": {}, + "validate": [] + } + } + ] + @return testset dict + { + "config": {}, + "testcases": [testcase11, testcase12] + } + """ + testset = { + "config": { + "path": file_path + }, + "testcases": [] # TODO: rename to tests + } + for item in load_file(file_path): + if not isinstance(item, dict) or len(item) != 1: + raise exceptions.FileFormatError("Testcase format error: {}".format(file_path)) + + key, test_block = item.popitem() + if not isinstance(test_block, dict): + raise exceptions.FileFormatError("Testcase format error: {}".format(file_path)) + + if key == "config": + testset["config"].update(test_block) + + elif key == "test": + if "api" in test_block: + ref_call = test_block["api"] + def_block = _get_block_by_name(ref_call, "api") + utils._override_block(def_block, test_block) + testset["testcases"].append(test_block) + elif "suite" in test_block: + ref_call = test_block["suite"] + block = _get_block_by_name(ref_call, "suite") + testset["testcases"].extend(block["testcases"]) + else: + testset["testcases"].append(test_block) + + else: + logger.log_warning( + "unexpected block key: {}. block key should only be 'config' or 'test'.".format(key) + ) + + return testset + + +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" + """ + 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", []) + + if len(call_args) != len(def_args): + raise exceptions.ParamsError("call args mismatch defined args!") + + args_mapping = {} + for index, item in enumerate(def_args): + if call_args[index] == item: + continue + + args_mapping[item] = call_args[index] + + if args_mapping: + block = utils.substitute_variables_with_mapping(block, args_mapping) + + return block + + +def _get_test_definition(name, ref_type): + """ get expected api or suite. + @params: + name: api or suite name + ref_type: "api" or "suite" + @return + expected api info if found, otherwise raise ApiNotFound exception + """ + block = overall_def_dict.get(ref_type, {}).get(name) + + if not block: + err_msg = "{} not found!".format(name) + if ref_type == "api": + raise exceptions.ApiNotFound(err_msg) + else: + # ref_type == "suite": + raise exceptions.SuiteNotFound(err_msg) + + return block + + +def load_testsets_by_path(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 testcase sets list, each testset is corresponding to a file + [ + testset_dict_1, + testset_dict_2 + ] + """ + if isinstance(path, (list, set)): + testsets = [] + + for file_path in set(path): + testset = load_testsets_by_path(file_path) + if not testset: + continue + testsets.extend(testset) + + return testsets + + if not os.path.isabs(path): + path = os.path.join(os.getcwd(), path) + + if path in testcases_cache_mapping: + return testcases_cache_mapping[path] + + if os.path.isdir(path): + files_list = load_folder_files(path) + testcases_list = load_testsets_by_path(files_list) + + elif os.path.isfile(path): + try: + testset = load_test_file(path) + if testset["testcases"] or testset["api"]: + testcases_list = [testset] + else: + testcases_list = [] + except exceptions.FileFormatError: + testcases_list = [] + + else: + logger.log_error(u"file not found: {}".format(path)) + testcases_list = [] + + testcases_cache_mapping[path] = testcases_list + return testcases_list diff --git a/httprunner/locusts.py b/httprunner/locusts.py index 28e58403..d7ddedea 100644 --- a/httprunner/locusts.py +++ b/httprunner/locusts.py @@ -6,7 +6,7 @@ import os import sys from httprunner.logger import color_print -from httprunner.testcase import TestcaseLoader +from httprunner import loader from locust.main import main @@ -40,8 +40,8 @@ def gen_locustfile(testcase_file_path): "templates", "locustfile_template" ) - TestcaseLoader.load_test_dependencies() - testset = TestcaseLoader.load_test_file(testcase_file_path) + loader.load_test_dependencies() + testset = loader.load_test_file(testcase_file_path) host = testset.get("config", {}).get("request", {}).get("base_url", "") with io.open(template_path, encoding='utf-8') as template: diff --git a/httprunner/parser.py b/httprunner/parser.py new file mode 100644 index 00000000..d66111f8 --- /dev/null +++ b/httprunner/parser.py @@ -0,0 +1,111 @@ +import ast +import re + +from httprunner import exceptions + +function_regexp_compile = re.compile(r"^([\w_]+)\(([\$\w\.\-_ =,]*)\)$") + + +def parse_string_value(str_value): + """ parse string to number if possible + e.g. "123" => 123 + "12.2" => 12.3 + "abc" => "abc" + "$var" => "$var" + """ + try: + return ast.literal_eval(str_value) + except ValueError: + return str_value + except SyntaxError: + # e.g. $var, ${func} + return str_value + + +def parse_function(content): + """ parse function name and args from string content. + @param (str) content + @return (dict) function name and args + + e.g. func() => {'func_name': 'func', 'args': [], 'kwargs': {}} + func(5) => {'func_name': 'func', 'args': [5], 'kwargs': {}} + func(1, 2) => {'func_name': 'func', 'args': [1, 2], 'kwargs': {}} + func(a=1, b=2) => {'func_name': 'func', 'args': [], 'kwargs': {'a': 1, 'b': 2}} + func(1, 2, a=3, b=4) => {'func_name': 'func', 'args': [1, 2], 'kwargs': {'a':3, 'b':4}} + """ + matched = function_regexp_compile.match(content) + if not matched: + raise exceptions.FunctionNotFound("{} not found!".format(content)) + + function_meta = { + "func_name": matched.group(1), + "args": [], + "kwargs": {} + } + + args_str = matched.group(2).strip() + if args_str == "": + return function_meta + + args_list = args_str.split(',') + for arg in args_list: + arg = arg.strip() + if '=' in arg: + key, value = arg.split('=') + function_meta["kwargs"][key.strip()] = parse_string_value(value.strip()) + else: + function_meta["args"].append(parse_string_value(arg)) + + return function_meta + + +def parse_validator(validator): + """ parse validator, validator maybe in two format + @param (dict) validator + format1: this is kept for compatiblity with the previous versions. + {"check": "status_code", "comparator": "eq", "expect": 201} + {"check": "$resp_body_success", "comparator": "eq", "expect": True} + format2: recommended new version + {'eq': ['status_code', 201]} + {'eq': ['$resp_body_success', True]} + @return (dict) validator info + { + "check": "status_code", + "expect": 201, + "comparator": "eq" + } + """ + if not isinstance(validator, dict): + raise exceptions.ParamsError("invalid validator: {}".format(validator)) + + if "check" in validator and len(validator) > 1: + # format1 + check_item = validator.get("check") + + if "expect" in validator: + expect_value = validator.get("expect") + elif "expected" in validator: + expect_value = validator.get("expected") + else: + raise exceptions.ParamsError("invalid validator: {}".format(validator)) + + comparator = validator.get("comparator", "eq") + + elif len(validator) == 1: + # format2 + comparator = list(validator.keys())[0] + compare_values = validator[comparator] + + if not isinstance(compare_values, list) or len(compare_values) != 2: + raise exceptions.ParamsError("invalid validator: {}".format(validator)) + + check_item, expect_value = compare_values + + else: + raise exceptions.ParamsError("invalid validator: {}".format(validator)) + + return { + "check": check_item, + "expect": expect_value, + "comparator": comparator + } diff --git a/httprunner/task.py b/httprunner/task.py index 010fb3a6..1353463b 100644 --- a/httprunner/task.py +++ b/httprunner/task.py @@ -8,8 +8,6 @@ from httprunner import exceptions, loader, logger, runner, testcase, utils from httprunner.compat import is_py3 from httprunner.report import (HtmlTestResult, get_platform, get_summary, render_html_report) -from httprunner.testcase import TestcaseLoader -from httprunner.utils import print_output class TestCase(unittest.TestCase): @@ -180,8 +178,8 @@ def init_test_suites(path_or_testsets, mapping=None, http_client_session=None): passed in variables mapping, it will override variables in config block """ if not testcase.is_testsets(path_or_testsets): - TestcaseLoader.load_test_dependencies() - testsets = TestcaseLoader.load_testsets_by_path(path_or_testsets) + loader.load_test_dependencies() + testsets = loader.load_testsets_by_path(path_or_testsets) else: testsets = path_or_testsets @@ -268,7 +266,7 @@ class HttpRunner(object): test_suite_summary["name"] = test_suite.config.get("name") test_suite_summary["base_url"] = test_suite.config.get("request", {}).get("base_url", "") test_suite_summary["output"] = test_suite.output - print_output(test_suite_summary["output"]) + utils.print_output(test_suite_summary["output"]) accumulate_stat(self.summary["stat"], test_suite_summary["stat"]) accumulate_stat(self.summary["time"], test_suite_summary["time"]) diff --git a/httprunner/testcase.py b/httprunner/testcase.py index 140b7355..85bf9c99 100644 --- a/httprunner/testcase.py +++ b/httprunner/testcase.py @@ -1,7 +1,5 @@ # encoding: utf-8 -import ast -import collections import io import itertools import json @@ -9,13 +7,12 @@ import os import random import re -from httprunner import exceptions, loader, logger, utils +from httprunner import exceptions, loader, logger, parser, utils from httprunner.compat import (OrderedDict, basestring, builtin_str, numeric_types, str) variable_regexp = r"\$([\w_]+)" function_regexp = r"\$\{([\w_]+\([\$\w\.\-_ =,]*\))\}" -function_regexp_compile = re.compile(r"^([\w_]+)\(([\$\w\.\-_ =,]*)\)$") def extract_variables(content): @@ -49,493 +46,6 @@ def extract_functions(content): except TypeError: return [] -def parse_string_value(str_value): - """ parse string to number if possible - e.g. "123" => 123 - "12.2" => 12.3 - "abc" => "abc" - "$var" => "$var" - """ - try: - return ast.literal_eval(str_value) - except ValueError: - return str_value - except SyntaxError: - # e.g. $var, ${func} - return str_value - -def parse_function(content): - """ parse function name and args from string content. - @param (str) content - @return (dict) function name and args - - e.g. func() => {'func_name': 'func', 'args': [], 'kwargs': {}} - func(5) => {'func_name': 'func', 'args': [5], 'kwargs': {}} - func(1, 2) => {'func_name': 'func', 'args': [1, 2], 'kwargs': {}} - func(a=1, b=2) => {'func_name': 'func', 'args': [], 'kwargs': {'a': 1, 'b': 2}} - func(1, 2, a=3, b=4) => {'func_name': 'func', 'args': [1, 2], 'kwargs': {'a':3, 'b':4}} - """ - matched = function_regexp_compile.match(content) - if not matched: - raise exceptions.FunctionNotFound("{} not found!".format(content)) - - function_meta = { - "func_name": matched.group(1), - "args": [], - "kwargs": {} - } - - args_str = matched.group(2).strip() - if args_str == "": - return function_meta - - args_list = args_str.split(',') - for arg in args_list: - arg = arg.strip() - if '=' in arg: - key, value = arg.split('=') - function_meta["kwargs"][key.strip()] = parse_string_value(value.strip()) - else: - function_meta["args"].append(parse_string_value(arg)) - - return function_meta - - -class TestcaseLoader(object): - - overall_def_dict = { - "api": {}, - "suite": {} - } - testcases_cache_mapping = {} - - @staticmethod - 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 loader.load_folder_files(api_def_folder): - TestcaseLoader.load_api_file(test_file) - - # load suite definitions - suite_def_folder = os.path.join(os.getcwd(), "tests", "suite") - for suite_file in loader.load_folder_files(suite_def_folder): - suite = TestcaseLoader.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 = parse_function(call_func) - suite["function_meta"] = function_meta - TestcaseLoader.overall_def_dict["suite"][function_meta["func_name"]] = suite - - @staticmethod - 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 = loader.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 = parse_function(api_def) - func_name = function_meta["func_name"] - - if func_name in TestcaseLoader.overall_def_dict["api"]: - logger.log_warning("API definition duplicated: {}".format(func_name)) - - api_dict["function_meta"] = function_meta - TestcaseLoader.overall_def_dict["api"][func_name] = api_dict - - @staticmethod - def load_test_file(file_path): - """ load testcase file or suite file - @param file_path: absolute valid file path - file_path should be in format below: - [ - { - "config": { - "name": "", - "def": "suite_order()", - "request": {} - } - }, - { - "test": { - "name": "add product to cart", - "api": "api_add_cart()", - "validate": [] - } - }, - { - "test": { - "name": "checkout cart", - "request": {}, - "validate": [] - } - } - ] - @return testset dict - { - "config": {}, - "testcases": [testcase11, testcase12] - } - """ - testset = { - "config": { - "path": file_path - }, - "testcases": [] # TODO: rename to tests - } - for item in loader.load_file(file_path): - if not isinstance(item, dict) or len(item) != 1: - raise exceptions.FileFormatError("Testcase format error: {}".format(file_path)) - - key, test_block = item.popitem() - if not isinstance(test_block, dict): - raise exceptions.FileFormatError("Testcase format error: {}".format(file_path)) - - if key == "config": - testset["config"].update(test_block) - - elif key == "test": - if "api" in test_block: - ref_call = test_block["api"] - def_block = TestcaseLoader._get_block_by_name(ref_call, "api") - TestcaseLoader._override_block(def_block, test_block) - testset["testcases"].append(test_block) - elif "suite" in test_block: - ref_call = test_block["suite"] - block = TestcaseLoader._get_block_by_name(ref_call, "suite") - testset["testcases"].extend(block["testcases"]) - else: - testset["testcases"].append(test_block) - - else: - logger.log_warning( - "unexpected block key: {}. block key should only be 'config' or 'test'.".format(key) - ) - - return testset - - @staticmethod - 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" - """ - function_meta = parse_function(ref_call) - func_name = function_meta["func_name"] - call_args = function_meta["args"] - block = TestcaseLoader._get_test_definition(func_name, ref_type) - def_args = block.get("function_meta").get("args", []) - - if len(call_args) != len(def_args): - raise exceptions.ParamsError("call args mismatch defined args!") - - args_mapping = {} - for index, item in enumerate(def_args): - if call_args[index] == item: - continue - - args_mapping[item] = call_args[index] - - if args_mapping: - block = substitute_variables_with_mapping(block, args_mapping) - - return block - - @staticmethod - def _get_test_definition(name, ref_type): - """ get expected api or suite. - @params: - name: api or suite name - ref_type: "api" or "suite" - @return - expected api info if found, otherwise raise ApiNotFound exception - """ - block = TestcaseLoader.overall_def_dict.get(ref_type, {}).get(name) - - if not block: - err_msg = "{} not found!".format(name) - if ref_type == "api": - raise exceptions.ApiNotFound(err_msg) - else: - # ref_type == "suite": - raise exceptions.SuiteNotFound(err_msg) - - return block - - @staticmethod - 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_validators = def_block.get("validate") or def_block.get("validators", []) - current_validators = current_block.get("validate") or current_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", []) - - current_block.update(def_block) - current_block["validate"] = _merge_validator( - def_validators, - current_validators - ) - current_block["extract"] = _merge_extractor( - def_extrators, - current_extractors - ) - - @staticmethod - def load_testsets_by_path(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 testcase sets list, each testset is corresponding to a file - [ - testset_dict_1, - testset_dict_2 - ] - """ - if isinstance(path, (list, set)): - testsets = [] - - for file_path in set(path): - testset = TestcaseLoader.load_testsets_by_path(file_path) - if not testset: - continue - testsets.extend(testset) - - return testsets - - if not os.path.isabs(path): - path = os.path.join(os.getcwd(), path) - - if path in TestcaseLoader.testcases_cache_mapping: - return TestcaseLoader.testcases_cache_mapping[path] - - if os.path.isdir(path): - files_list = loader.load_folder_files(path) - testcases_list = TestcaseLoader.load_testsets_by_path(files_list) - - elif os.path.isfile(path): - try: - testset = TestcaseLoader.load_test_file(path) - if testset["testcases"] or testset["api"]: - testcases_list = [testset] - else: - testcases_list = [] - except exceptions.FileFormatError: - testcases_list = [] - - else: - logger.log_error(u"file not found: {}".format(path)) - testcases_list = [] - - TestcaseLoader.testcases_cache_mapping[path] = testcases_list - return testcases_list - -def parse_validator(validator): - """ parse validator, validator maybe in two format - @param (dict) validator - format1: this is kept for compatiblity with the previous versions. - {"check": "status_code", "comparator": "eq", "expect": 201} - {"check": "$resp_body_success", "comparator": "eq", "expect": True} - format2: recommended new version - {'eq': ['status_code', 201]} - {'eq': ['$resp_body_success', True]} - @return (dict) validator info - { - "check": "status_code", - "expect": 201, - "comparator": "eq" - } - """ - if not isinstance(validator, dict): - raise exceptions.ParamsError("invalid validator: {}".format(validator)) - - if "check" in validator and len(validator) > 1: - # format1 - check_item = validator.get("check") - - if "expect" in validator: - expect_value = validator.get("expect") - elif "expected" in validator: - expect_value = validator.get("expected") - else: - raise exceptions.ParamsError("invalid validator: {}".format(validator)) - - comparator = validator.get("comparator", "eq") - - elif len(validator) == 1: - # format2 - comparator = list(validator.keys())[0] - compare_values = validator[comparator] - - if not isinstance(compare_values, list) or len(compare_values) != 2: - raise exceptions.ParamsError("invalid validator: {}".format(validator)) - - check_item, expect_value = compare_values - - else: - raise exceptions.ParamsError("invalid validator: {}".format(validator)) - - return { - "check": check_item, - "expect": expect_value, - "comparator": comparator - } - -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"} - } - """ - validators_mapping = {} - - for validator in validators: - validator = parse_validator(validator) - - if not isinstance(validator["check"], collections.Hashable): - check = json.dumps(validator["check"]) - else: - check = validator["check"] - - key = (check, validator["comparator"]) - validators_mapping[key] = validator - - 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"} - ] - """ - if not def_validators: - return current_validators - - elif not current_validators: - return def_validators - - else: - api_validators_mapping = _get_validators_mapping(def_validators) - test_validators_mapping = _get_validators_mapping(current_validators) - - api_validators_mapping.update(test_validators_mapping) - return list(api_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"} - ] - """ - if not def_extrators: - return current_extractors - - elif not current_extractors: - return def_extrators - - else: - extractor_dict = OrderedDict() - for api_extrator in def_extrators: - if len(api_extrator) != 1: - logger.log_warning("incorrect extractor: {}".format(api_extrator)) - continue - - var_name = list(api_extrator.keys())[0] - extractor_dict[var_name] = api_extrator[var_name] - - for test_extrator in current_extractors: - if len(test_extrator) != 1: - logger.log_warning("incorrect extractor: {}".format(test_extrator)) - continue - - var_name = list(test_extrator.keys())[0] - extractor_dict[var_name] = test_extrator[var_name] - - extractor_list = [] - for key, value in extractor_dict.items(): - extractor_list.append({key: value}) - - return extractor_list - def is_testset(data_structure): """ check if data_structure is a testset @@ -577,61 +87,6 @@ def is_testsets(data_structure): return True -def substitute_variables_with_mapping(content, mapping): - """ substitute variables in content with mapping - e.g. - @params - content = { - 'request': { - 'url': '/api/users/$uid', - 'headers': {'token': '$token'} - } - } - mapping = {"$uid": 1000} - @return - { - 'request': { - 'url': '/api/users/1000', - 'headers': {'token': '$token'} - } - } - """ - # TODO: refactor type check - if isinstance(content, bool): - return content - - if isinstance(content, (numeric_types, type)): - return content - - if not content: - return content - - if isinstance(content, (list, set, tuple)): - return [ - substitute_variables_with_mapping(item, mapping) - for item in content - ] - - if isinstance(content, dict): - substituted_data = {} - for key, value in content.items(): - eval_key = substitute_variables_with_mapping(key, mapping) - eval_value = substitute_variables_with_mapping(value, mapping) - substituted_data[eval_key] = eval_value - - return substituted_data - - # content is in string format here - for var, value in mapping.items(): - if content == var: - # content is a variable - content = value - else: - if not isinstance(value, str): - value = builtin_str(value) - content = content.replace(var, value) - - return content def gen_cartesian_product(*args): """ generate cartesian product for lists @@ -801,7 +256,7 @@ class TestcaseParser(object): def _eval_content_functions(self, content): functions_list = extract_functions(content) for func_content in functions_list: - function_meta = parse_function(func_content) + function_meta = parser.parse_function(func_content) func_name = function_meta['func_name'] args = function_meta.get('args', []) diff --git a/httprunner/utils.py b/httprunner/utils.py index 24c16d1f..1d32ecb5 100644 --- a/httprunner/utils.py +++ b/httprunner/utils.py @@ -1,5 +1,6 @@ # encoding: utf-8 +import collections import copy import hashlib import hmac @@ -13,8 +14,9 @@ import string import types from datetime import datetime -from httprunner import exceptions, logger -from httprunner.compat import OrderedDict, basestring, is_py2, is_py3, str +from httprunner import exceptions, logger, parser +from httprunner.compat import (OrderedDict, basestring, builtin_str, is_py2, + is_py3, numeric_types, str) from requests.structures import CaseInsensitiveDict SECRET_KEY = "DebugTalk" @@ -84,6 +86,205 @@ def query_json(json_content, query, delimiter='.'): return json_content + +def substitute_variables_with_mapping(content, mapping): + """ substitute variables in content with mapping + e.g. + @params + content = { + 'request': { + 'url': '/api/users/$uid', + 'headers': {'token': '$token'} + } + } + mapping = {"$uid": 1000} + @return + { + 'request': { + 'url': '/api/users/1000', + 'headers': {'token': '$token'} + } + } + """ + # TODO: refactor type check + if isinstance(content, bool): + return content + + if isinstance(content, (numeric_types, type)): + return content + + if not content: + return content + + if isinstance(content, (list, set, tuple)): + return [ + substitute_variables_with_mapping(item, mapping) + for item in content + ] + + if isinstance(content, dict): + substituted_data = {} + for key, value in content.items(): + eval_key = substitute_variables_with_mapping(key, mapping) + eval_value = substitute_variables_with_mapping(value, mapping) + substituted_data[eval_key] = eval_value + + return substituted_data + + # content is in string format here + for var, value in mapping.items(): + if content == var: + # content is a variable + content = value + else: + if not isinstance(value, str): + value = builtin_str(value) + content = content.replace(var, value) + + return content + + +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"} + } + """ + validators_mapping = {} + + for validator in validators: + validator = parser.parse_validator(validator) + + if not isinstance(validator["check"], collections.Hashable): + check = json.dumps(validator["check"]) + else: + check = validator["check"] + + key = (check, validator["comparator"]) + validators_mapping[key] = validator + + 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"} + ] + """ + if not def_validators: + return current_validators + + elif not current_validators: + return def_validators + + else: + api_validators_mapping = _get_validators_mapping(def_validators) + test_validators_mapping = _get_validators_mapping(current_validators) + + api_validators_mapping.update(test_validators_mapping) + return list(api_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"} + ] + """ + if not def_extrators: + return current_extractors + + elif not current_extractors: + return def_extrators + + else: + extractor_dict = OrderedDict() + for api_extrator in def_extrators: + if len(api_extrator) != 1: + logger.log_warning("incorrect extractor: {}".format(api_extrator)) + continue + + var_name = list(api_extrator.keys())[0] + extractor_dict[var_name] = api_extrator[var_name] + + for test_extrator in current_extractors: + if len(test_extrator) != 1: + logger.log_warning("incorrect extractor: {}".format(test_extrator)) + continue + + var_name = list(test_extrator.keys())[0] + extractor_dict[var_name] = test_extrator[var_name] + + extractor_list = [] + for key, value in extractor_dict.items(): + extractor_list.append({key: value}) + + return extractor_list + + +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_validators = def_block.get("validate") or def_block.get("validators", []) + current_validators = current_block.get("validate") or current_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", []) + + current_block.update(def_block) + current_block["validate"] = _merge_validator( + def_validators, + current_validators + ) + current_block["extract"] = _merge_extractor( + def_extrators, + current_extractors + ) + + def get_uniform_comparator(comparator): """ convert comparator alias to uniform name """ diff --git a/tests/test_httprunner.py b/tests/test_httprunner.py index 2b5ad47c..8828bb6b 100644 --- a/tests/test_httprunner.py +++ b/tests/test_httprunner.py @@ -2,7 +2,6 @@ import os import shutil from httprunner import HttpRunner -from httprunner.exceptions import FileNotFound from tests.base import ApiServerUnittest @@ -155,13 +154,3 @@ class TestHttpRunner(ApiServerUnittest): summary = runner.summary self.assertTrue(summary["success"]) self.assertEqual(summary["stat"]["testsRun"], 8) - - def test_load_env_path(self): - self.assertNotIn("PROJECT_KEY", os.environ) - HttpRunner(dot_env_path="tests/data/test.env").run(self.testset_path) - self.assertIn("PROJECT_KEY", os.environ) - self.assertEqual(os.environ["UserName"], "debugtalk") - - def test_load_env_path_not_exist(self): - with self.assertRaises(FileNotFound): - HttpRunner(dot_env_path="not_exist.env").run(self.testset_path) diff --git a/tests/test_loader.py b/tests/test_loader.py new file mode 100644 index 00000000..855a2bfe --- /dev/null +++ b/tests/test_loader.py @@ -0,0 +1,326 @@ +import os +import unittest + +from httprunner import exceptions, loader, utils + + +class TestFileLoader(unittest.TestCase): + + def test_load_yaml_file_file_format_error(self): + yaml_tmp_file = "tests/data/tmp.yml" + # create empty yaml file + with open(yaml_tmp_file, 'w') as f: + f.write("") + + with self.assertRaises(exceptions.FileFormatError): + loader.load_yaml_file(yaml_tmp_file) + + os.remove(yaml_tmp_file) + + # create invalid format yaml file + with open(yaml_tmp_file, 'w') as f: + f.write("abc") + + with self.assertRaises(exceptions.FileFormatError): + loader.load_yaml_file(yaml_tmp_file) + + os.remove(yaml_tmp_file) + + + def test_load_json_file_file_format_error(self): + json_tmp_file = "tests/data/tmp.json" + # create empty file + with open(json_tmp_file, 'w') as f: + f.write("") + + with self.assertRaises(exceptions.FileFormatError): + loader.load_json_file(json_tmp_file) + + os.remove(json_tmp_file) + + # create empty json file + with open(json_tmp_file, 'w') as f: + f.write("{}") + + with self.assertRaises(exceptions.FileFormatError): + loader.load_json_file(json_tmp_file) + + os.remove(json_tmp_file) + + # create invalid format json file + with open(json_tmp_file, 'w') as f: + f.write("abc") + + with self.assertRaises(exceptions.FileFormatError): + loader.load_json_file(json_tmp_file) + + os.remove(json_tmp_file) + + def test_load_testcases_bad_filepath(self): + testcase_file_path = os.path.join(os.getcwd(), 'tests/data/demo') + with self.assertRaises(exceptions.FileNotFound): + loader.load_file(testcase_file_path) + + def test_load_json_testcases(self): + testcase_file_path = os.path.join( + os.getcwd(), 'tests/data/demo_testset_hardcode.json') + testcases = loader.load_file(testcase_file_path) + self.assertEqual(len(testcases), 3) + test = testcases[0]["test"] + self.assertIn('name', test) + self.assertIn('request', test) + self.assertIn('url', test['request']) + self.assertIn('method', test['request']) + + def test_load_yaml_testcases(self): + testcase_file_path = os.path.join( + os.getcwd(), 'tests/data/demo_testset_hardcode.yml') + testcases = loader.load_file(testcase_file_path) + self.assertEqual(len(testcases), 3) + test = testcases[0]["test"] + self.assertIn('name', test) + self.assertIn('request', test) + self.assertIn('url', test['request']) + self.assertIn('method', test['request']) + + def test_load_csv_file_one_parameter(self): + csv_file_path = os.path.join( + os.getcwd(), 'tests/data/user_agent.csv') + csv_content = loader.load_file(csv_file_path) + self.assertEqual( + csv_content, + [ + {'user_agent': 'iOS/10.1'}, + {'user_agent': 'iOS/10.2'}, + {'user_agent': 'iOS/10.3'} + ] + ) + + def test_load_csv_file_multiple_parameters(self): + csv_file_path = os.path.join( + os.getcwd(), 'tests/data/account.csv') + csv_content = loader.load_file(csv_file_path) + self.assertEqual( + csv_content, + [ + {'username': 'test1', 'password': '111111'}, + {'username': 'test2', 'password': '222222'}, + {'username': 'test3', 'password': '333333'} + ] + ) + + def test_load_folder_files(self): + folder = os.path.join(os.getcwd(), 'tests') + file1 = os.path.join(os.getcwd(), 'tests', 'test_utils.py') + file2 = os.path.join(os.getcwd(), 'tests', 'data', 'demo_binds.yml') + + files = loader.load_folder_files(folder, recursive=False) + self.assertNotIn(file2, files) + + files = loader.load_folder_files(folder) + self.assertIn(file2, files) + self.assertNotIn(file1, files) + + files = loader.load_folder_files(folder) + api_file = os.path.join(os.getcwd(), 'tests', 'api', 'basic.yml') + self.assertIn(api_file, files) + + files = loader.load_folder_files("not_existed_foulder", recursive=False) + self.assertEqual([], files) + + files = loader.load_folder_files(file2, recursive=False) + self.assertEqual([], files) + + def test_load_dot_env_file(self): + self.assertNotIn("PROJECT_KEY", os.environ) + loader.load_dot_env_file("tests/data/test.env") + self.assertIn("PROJECT_KEY", os.environ) + self.assertEqual(os.environ["UserName"], "debugtalk") + + def test_load_env_path_not_exist(self): + with self.assertRaises(exceptions.FileNotFound): + loader.load_dot_env_file("not_exist.env") + + +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") + + 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") + + 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") + 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") + + 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") + test_block = { + "name": "override block", + "variables": [ + {"var": 123} + ], + 'request': { + 'url': '/api/get-token', 'method': 'POST', 'headers': {'user_agent': '$user_agent', 'device_sn': '$device_sn', 'os_platform': '$os_platform', 'app_version': '$app_version'}, 'json': {'sign': '${get_sign($user_agent, $device_sn, $os_platform, $app_version)}'}}, + 'validate': [ + {'eq': ['status_code', 201]}, + {'len_eq': ['content.token', 32]} + ] + } + + utils._override_block(def_block, test_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") + 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") + + def test_get_test_definition_suite(self): + loader.load_test_dependencies() + api_def = loader._get_test_definition("create_and_check", "suite") + self.assertEqual(api_def["config"]["name"], "create user and check result.") + + with self.assertRaises(exceptions.SuiteNotFound): + loader._get_test_definition("create_and_check_XXX", "suite") + + def test_load_testcases_by_path_files(self): + testsets_list = [] + + # absolute file path + path = os.path.join( + os.getcwd(), 'tests/data/demo_testset_hardcode.json') + testset_list = loader.load_testsets_by_path(path) + 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) + testsets_list.extend(testset_list) + + # relative file path + path = 'tests/data/demo_testset_hardcode.yml' + testset_list = loader.load_testsets_by_path(path) + 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) + testsets_list.extend(testset_list) + + # list/set container with file(s) + path = [ + os.path.join(os.getcwd(), 'tests/data/demo_testset_hardcode.json'), + 'tests/data/demo_testset_hardcode.yml' + ] + testset_list = loader.load_testsets_by_path(path) + self.assertEqual(len(testset_list), 2) + self.assertEqual(len(testset_list[0]["testcases"]), 3) + self.assertEqual(len(testset_list[1]["testcases"]), 3) + testsets_list.extend(testset_list) + self.assertEqual(len(testsets_list), 4) + + for testset in testsets_list: + for test in testset["testcases"]: + 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_testsets_by_path(path) + self.assertGreater(len(testset_list_1), 4) + + # relative folder path + path = 'tests/data/' + testset_list_2 = loader.load_testsets_by_path(path) + self.assertEqual(len(testset_list_1), len(testset_list_2)) + + # list/set container with file(s) + path = [ + os.path.join(os.getcwd(), 'tests/data'), + 'tests/data/' + ] + testset_list_3 = loader.load_testsets_by_path(path) + self.assertEqual(len(testset_list_3), 2 * len(testset_list_1)) + + def test_load_testcases_by_path_not_exist(self): + # absolute folder path + path = os.path.join(os.getcwd(), 'tests/data_not_exist') + testset_list_1 = loader.load_testsets_by_path(path) + self.assertEqual(testset_list_1, []) + + # relative folder path + path = 'tests/data_not_exist' + testset_list_2 = loader.load_testsets_by_path(path) + self.assertEqual(testset_list_2, []) + + # list/set container with file(s) + path = [ + os.path.join(os.getcwd(), 'tests/data_not_exist'), + 'tests/data_not_exist/' + ] + testset_list_3 = loader.load_testsets_by_path(path) + self.assertEqual(testset_list_3, []) + + 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_testsets_by_path(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]) diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 00000000..e4369e34 --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,68 @@ +import os +import unittest +from httprunner import parser, exceptions + + +class TestParser(unittest.TestCase): + + def test_parse_string_value(self): + self.assertEqual(parser.parse_string_value("123"), 123) + self.assertEqual(parser.parse_string_value("12.3"), 12.3) + self.assertEqual(parser.parse_string_value("a123"), "a123") + self.assertEqual(parser.parse_string_value("$var"), "$var") + self.assertEqual(parser.parse_string_value("${func}"), "${func}") + + def test_parse_function(self): + self.assertEqual( + parser.parse_function("func()"), + {'func_name': 'func', 'args': [], 'kwargs': {}} + ) + self.assertEqual( + parser.parse_function("func(5)"), + {'func_name': 'func', 'args': [5], 'kwargs': {}} + ) + self.assertEqual( + parser.parse_function("func(1, 2)"), + {'func_name': 'func', 'args': [1, 2], 'kwargs': {}} + ) + self.assertEqual( + parser.parse_function("func(a=1, b=2)"), + {'func_name': 'func', 'args': [], 'kwargs': {'a': 1, 'b': 2}} + ) + self.assertEqual( + parser.parse_function("func(a= 1, b =2)"), + {'func_name': 'func', 'args': [], 'kwargs': {'a': 1, 'b': 2}} + ) + self.assertEqual( + parser.parse_function("func(1, 2, a=3, b=4)"), + {'func_name': 'func', 'args': [1, 2], 'kwargs': {'a': 3, 'b': 4}} + ) + self.assertEqual( + parser.parse_function("func($request, 123)"), + {'func_name': 'func', 'args': ["$request", 123], 'kwargs': {}} + ) + self.assertEqual( + parser.parse_function("func( )"), + {'func_name': 'func', 'args': [], 'kwargs': {}} + ) + self.assertEqual( + parser.parse_function("func(hello world, a=3, b=4)"), + {'func_name': 'func', 'args': ["hello world"], 'kwargs': {'a': 3, 'b': 4}} + ) + self.assertEqual( + parser.parse_function("func($request, 12 3)"), + {'func_name': 'func', 'args': ["$request", '12 3'], 'kwargs': {}} + ) + + def test_parse_validator(self): + validator = {"check": "status_code", "comparator": "eq", "expect": 201} + self.assertEqual( + parser.parse_validator(validator), + {"check": "status_code", "comparator": "eq", "expect": 201} + ) + + validator = {'eq': ['status_code', 201]} + self.assertEqual( + parser.parse_validator(validator), + {"check": "status_code", "comparator": "eq", "expect": 201} + ) diff --git a/tests/test_runner.py b/tests/test_runner.py index 95552512..4830d570 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -2,7 +2,6 @@ import os import time from httprunner import HttpRunner, exceptions, loader, runner -from httprunner.testcase import TestcaseLoader from httprunner.utils import deep_update_dict from tests.base import ApiServerUnittest @@ -380,7 +379,7 @@ class TestRunner(ApiServerUnittest): def test_run_testcase_with_empty_header(self): testcase_file_path = os.path.join( os.getcwd(), 'tests/data/test_bugfix.yml') - testsets = TestcaseLoader.load_testsets_by_path(testcase_file_path) + testsets = loader.load_testsets_by_path(testcase_file_path) testset = testsets[0] config_dict_headers = testset["config"]["request"]["headers"] test_dict_headers = testset["testcases"][0]["request"]["headers"] diff --git a/tests/test_task.py b/tests/test_task.py index 7e49d6a4..3620de85 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -1,7 +1,6 @@ import os -from httprunner import task -from httprunner.testcase import TestcaseLoader +from httprunner import loader, task from tests.base import ApiServerUnittest @@ -17,7 +16,7 @@ class TestTask(ApiServerUnittest): def test_create_suite(self): testcase_file_path = os.path.join(os.getcwd(), 'tests/data/demo_testset_variables.yml') - testset = TestcaseLoader.load_test_file(testcase_file_path) + testset = loader.load_test_file(testcase_file_path) suite = task.TestSuite(testset) self.assertEqual(suite.countTestCases(), 3) for testcase in suite: diff --git a/tests/test_testcase.py b/tests/test_testcase.py index b7ef5191..5f53ec1c 100644 --- a/tests/test_testcase.py +++ b/tests/test_testcase.py @@ -2,193 +2,7 @@ import os import time import unittest -from httprunner import testcase -from httprunner.exceptions import ApiNotFound, ParamsError, SuiteNotFound -from httprunner.testcase import TestcaseLoader - - -class TestTestcaseLoader(unittest.TestCase): - - def setUp(self): - TestcaseLoader.overall_def_dict = { - "api": {}, - "suite": {} - } - - def test_load_test_dependencies(self): - TestcaseLoader.load_test_dependencies() - overall_def_dict = TestcaseLoader.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): - TestcaseLoader.load_api_file("tests/api/basic.yml") - overall_api_def_dict = TestcaseLoader.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): - TestcaseLoader.load_api_file("tests/api/basic.yml") - testset = TestcaseLoader.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") - - def test_load_test_file_testcase(self): - TestcaseLoader.load_test_dependencies() - testset = TestcaseLoader.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") - - def test_get_block_by_name(self): - TestcaseLoader.load_test_dependencies() - ref_call = "get_user($uid, $token)" - block = TestcaseLoader._get_block_by_name(ref_call, "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): - TestcaseLoader.load_test_dependencies() - ref_call = "get_user($uid, $token, $var)" - with self.assertRaises(ParamsError): - TestcaseLoader._get_block_by_name(ref_call, "api") - - def test_get_test_definition_api(self): - TestcaseLoader.load_test_dependencies() - api_def = TestcaseLoader._get_test_definition("get_headers", "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(ApiNotFound): - TestcaseLoader._get_test_definition("get_token_XXX", "api") - - def test_get_test_definition_suite(self): - TestcaseLoader.load_test_dependencies() - api_def = TestcaseLoader._get_test_definition("create_and_check", "suite") - self.assertEqual(api_def["config"]["name"], "create user and check result.") - - with self.assertRaises(SuiteNotFound): - TestcaseLoader._get_test_definition("create_and_check_XXX", "suite") - - def test_override_block(self): - TestcaseLoader.load_test_dependencies() - def_block = TestcaseLoader._get_block_by_name("get_token($user_agent, $device_sn, $os_platform, $app_version)", "api") - test_block = { - "name": "override block", - "variables": [ - {"var": 123} - ], - 'request': { - 'url': '/api/get-token', 'method': 'POST', 'headers': {'user_agent': '$user_agent', 'device_sn': '$device_sn', 'os_platform': '$os_platform', 'app_version': '$app_version'}, 'json': {'sign': '${get_sign($user_agent, $device_sn, $os_platform, $app_version)}'}}, - 'validate': [ - {'eq': ['status_code', 201]}, - {'len_eq': ['content.token', 32]} - ] - } - - TestcaseLoader._override_block(def_block, test_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_load_testcases_by_path_files(self): - testsets_list = [] - - # absolute file path - path = os.path.join( - os.getcwd(), 'tests/data/demo_testset_hardcode.json') - testset_list = TestcaseLoader.load_testsets_by_path(path) - 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) - testsets_list.extend(testset_list) - - # relative file path - path = 'tests/data/demo_testset_hardcode.yml' - testset_list = TestcaseLoader.load_testsets_by_path(path) - 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) - testsets_list.extend(testset_list) - - # list/set container with file(s) - path = [ - os.path.join(os.getcwd(), 'tests/data/demo_testset_hardcode.json'), - 'tests/data/demo_testset_hardcode.yml' - ] - testset_list = TestcaseLoader.load_testsets_by_path(path) - self.assertEqual(len(testset_list), 2) - self.assertEqual(len(testset_list[0]["testcases"]), 3) - self.assertEqual(len(testset_list[1]["testcases"]), 3) - testsets_list.extend(testset_list) - self.assertEqual(len(testsets_list), 4) - - for testset in testsets_list: - for test in testset["testcases"]: - 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): - TestcaseLoader.load_test_dependencies() - # absolute folder path - path = os.path.join(os.getcwd(), 'tests/data') - testset_list_1 = TestcaseLoader.load_testsets_by_path(path) - self.assertGreater(len(testset_list_1), 4) - - # relative folder path - path = 'tests/data/' - testset_list_2 = TestcaseLoader.load_testsets_by_path(path) - self.assertEqual(len(testset_list_1), len(testset_list_2)) - - # list/set container with file(s) - path = [ - os.path.join(os.getcwd(), 'tests/data'), - 'tests/data/' - ] - testset_list_3 = TestcaseLoader.load_testsets_by_path(path) - self.assertEqual(len(testset_list_3), 2 * len(testset_list_1)) - - def test_load_testcases_by_path_not_exist(self): - # absolute folder path - path = os.path.join(os.getcwd(), 'tests/data_not_exist') - testset_list_1 = TestcaseLoader.load_testsets_by_path(path) - self.assertEqual(testset_list_1, []) - - # relative folder path - path = 'tests/data_not_exist' - testset_list_2 = TestcaseLoader.load_testsets_by_path(path) - self.assertEqual(testset_list_2, []) - - # list/set container with file(s) - path = [ - os.path.join(os.getcwd(), 'tests/data_not_exist'), - 'tests/data_not_exist/' - ] - testset_list_3 = TestcaseLoader.load_testsets_by_path(path) - self.assertEqual(testset_list_3, []) - - def test_load_testcases_by_path_layered(self): - TestcaseLoader.load_test_dependencies() - path = os.path.join( - os.getcwd(), 'tests/data/demo_testset_layer.yml') - testsets_list = TestcaseLoader.load_testsets_by_path(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]) +from httprunner import exceptions, loader, testcase class TestcaseParserUnittest(unittest.TestCase): @@ -414,62 +228,13 @@ class TestcaseParserUnittest(unittest.TestCase): def test_eval_content_variables_search_upward(self): testcase_parser = testcase.TestcaseParser() - with self.assertRaises(ParamsError): + with self.assertRaises(exceptions.ParamsError): testcase_parser._eval_content_variables("/api/$SECRET_KEY") testcase_parser.file_path = "tests/data/demo_testset_hardcode.yml" content = testcase_parser._eval_content_variables("/api/$SECRET_KEY") self.assertEqual(content, "/api/DebugTalk") - def test_parse_string_value(self): - self.assertEqual(testcase.parse_string_value("123"), 123) - self.assertEqual(testcase.parse_string_value("12.3"), 12.3) - self.assertEqual(testcase.parse_string_value("a123"), "a123") - self.assertEqual(testcase.parse_string_value("$var"), "$var") - self.assertEqual(testcase.parse_string_value("${func}"), "${func}") - - def test_parse_function(self): - self.assertEqual( - testcase.parse_function("func()"), - {'func_name': 'func', 'args': [], 'kwargs': {}} - ) - self.assertEqual( - testcase.parse_function("func(5)"), - {'func_name': 'func', 'args': [5], 'kwargs': {}} - ) - self.assertEqual( - testcase.parse_function("func(1, 2)"), - {'func_name': 'func', 'args': [1, 2], 'kwargs': {}} - ) - self.assertEqual( - testcase.parse_function("func(a=1, b=2)"), - {'func_name': 'func', 'args': [], 'kwargs': {'a': 1, 'b': 2}} - ) - self.assertEqual( - testcase.parse_function("func(a= 1, b =2)"), - {'func_name': 'func', 'args': [], 'kwargs': {'a': 1, 'b': 2}} - ) - self.assertEqual( - testcase.parse_function("func(1, 2, a=3, b=4)"), - {'func_name': 'func', 'args': [1, 2], 'kwargs': {'a': 3, 'b': 4}} - ) - self.assertEqual( - testcase.parse_function("func($request, 123)"), - {'func_name': 'func', 'args': ["$request", 123], 'kwargs': {}} - ) - self.assertEqual( - testcase.parse_function("func( )"), - {'func_name': 'func', 'args': [], 'kwargs': {}} - ) - self.assertEqual( - testcase.parse_function("func(hello world, a=3, b=4)"), - {'func_name': 'func', 'args': ["hello world"], 'kwargs': {'a': 3, 'b': 4}} - ) - self.assertEqual( - testcase.parse_function("func($request, 12 3)"), - {'func_name': 'func', 'args': ["$request", '12 3'], 'kwargs': {}} - ) - def test_parse_content_with_bindings_variables(self): variables = { @@ -486,7 +251,7 @@ class TestcaseParserUnittest(unittest.TestCase): "123str_value1/456" ) - with self.assertRaises(ParamsError): + with self.assertRaises(exceptions.ParamsError): testcase_parser.eval_content_with_bindings("$str_3") self.assertEqual( @@ -596,7 +361,7 @@ class TestcaseParserUnittest(unittest.TestCase): def test_eval_content_functions_search_upward(self): testcase_parser = testcase.TestcaseParser() - with self.assertRaises(ParamsError): + with self.assertRaises(exceptions.ParamsError): testcase_parser._eval_content_functions("/api/${gen_md5(abc)}") testcase_parser.file_path = "tests/data/demo_testset_hardcode.yml" @@ -649,105 +414,6 @@ class TestcaseParserUnittest(unittest.TestCase): 3 ) - - def test_substitute_variables_with_mapping(self): - content = { - 'request': { - 'url': '/api/users/$uid', - 'method': "$method", - 'headers': {'token': '$token'}, - 'data': { - "null": None, - "true": True, - "false": False, - "empty_str": "" - } - } - } - mapping = { - "$uid": 1000, - "$method": "POST" - } - result = testcase.substitute_variables_with_mapping(content, mapping) - self.assertEqual("/api/users/1000", result["request"]["url"]) - self.assertEqual("$token", result["request"]["headers"]["token"]) - self.assertEqual("POST", result["request"]["method"]) - self.assertIsNone(result["request"]["data"]["null"]) - self.assertTrue(result["request"]["data"]["true"]) - self.assertFalse(result["request"]["data"]["false"]) - self.assertEqual("", result["request"]["data"]["empty_str"]) - - - def test_parse_validator(self): - validator = {"check": "status_code", "comparator": "eq", "expect": 201} - self.assertEqual( - testcase.parse_validator(validator), - {"check": "status_code", "comparator": "eq", "expect": 201} - ) - - validator = {'eq': ['status_code', 201]} - self.assertEqual( - testcase.parse_validator(validator), - {"check": "status_code", "comparator": "eq", "expect": 201} - ) - - def test_merge_validator(self): - def_validators = [ - {'eq': ['v1', 200]}, - {"check": "s2", "expect": 16, "comparator": "len_eq"} - ] - current_validators = [ - {"check": "v1", "expect": 201}, - {'len_eq': ['s3', 12]} - ] - - merged_validators = testcase._merge_validator(def_validators, current_validators) - self.assertIn( - {"check": "v1", "expect": 201, "comparator": "eq"}, - merged_validators - ) - self.assertIn( - {"check": "s2", "expect": 16, "comparator": "len_eq"}, - merged_validators - ) - self.assertIn( - {"check": "s3", "expect": 12, "comparator": "len_eq"}, - merged_validators - ) - - def test_merge_validator_with_dict(self): - def_validators = [ - {'eq': ["a", {"v": 1}]}, - {'eq': [{"b": 1}, 200]} - ] - current_validators = [ - {'len_eq': ['s3', 12]}, - {'eq': [{"b": 1}, 201]} - ] - - merged_validators = testcase._merge_validator(def_validators, current_validators) - self.assertEqual(len(merged_validators), 3) - self.assertIn({'check': {'b': 1}, 'expect': 201, 'comparator': 'eq'}, merged_validators) - self.assertNotIn({'check': {'b': 1}, 'expect': 200, 'comparator': 'eq'}, merged_validators) - - def test_merge_extractor(self): - api_extrators = [{"var1": "val1"}, {"var2": "val2"}] - current_extractors = [{"var1": "val111"}, {"var3": "val3"}] - - merged_extractors = testcase._merge_extractor(api_extrators, current_extractors) - self.assertIn( - {"var1": "val111"}, - merged_extractors - ) - self.assertIn( - {"var2": "val2"}, - merged_extractors - ) - self.assertIn( - {"var3": "val3"}, - merged_extractors - ) - def test_is_testsets(self): data_structure = "path/to/file" self.assertFalse(testcase.is_testsets(data_structure)) diff --git a/tests/test_utils.py b/tests/test_utils.py index e2febcb7..bfeef62e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -61,6 +61,90 @@ class TestUtils(ApiServerUnittest): result = utils.query_json(json_content, query) self.assertEqual(result, "L") + def test_substitute_variables_with_mapping(self): + content = { + 'request': { + 'url': '/api/users/$uid', + 'method': "$method", + 'headers': {'token': '$token'}, + 'data': { + "null": None, + "true": True, + "false": False, + "empty_str": "" + } + } + } + mapping = { + "$uid": 1000, + "$method": "POST" + } + result = utils.substitute_variables_with_mapping(content, mapping) + self.assertEqual("/api/users/1000", result["request"]["url"]) + self.assertEqual("$token", result["request"]["headers"]["token"]) + self.assertEqual("POST", result["request"]["method"]) + self.assertIsNone(result["request"]["data"]["null"]) + self.assertTrue(result["request"]["data"]["true"]) + self.assertFalse(result["request"]["data"]["false"]) + self.assertEqual("", result["request"]["data"]["empty_str"]) + + def test_merge_validator(self): + def_validators = [ + {'eq': ['v1', 200]}, + {"check": "s2", "expect": 16, "comparator": "len_eq"} + ] + current_validators = [ + {"check": "v1", "expect": 201}, + {'len_eq': ['s3', 12]} + ] + + merged_validators = utils._merge_validator(def_validators, current_validators) + self.assertIn( + {"check": "v1", "expect": 201, "comparator": "eq"}, + merged_validators + ) + self.assertIn( + {"check": "s2", "expect": 16, "comparator": "len_eq"}, + merged_validators + ) + self.assertIn( + {"check": "s3", "expect": 12, "comparator": "len_eq"}, + merged_validators + ) + + def test_merge_validator_with_dict(self): + def_validators = [ + {'eq': ["a", {"v": 1}]}, + {'eq': [{"b": 1}, 200]} + ] + current_validators = [ + {'len_eq': ['s3', 12]}, + {'eq': [{"b": 1}, 201]} + ] + + merged_validators = utils._merge_validator(def_validators, current_validators) + self.assertEqual(len(merged_validators), 3) + self.assertIn({'check': {'b': 1}, 'expect': 201, 'comparator': 'eq'}, merged_validators) + self.assertNotIn({'check': {'b': 1}, 'expect': 200, 'comparator': 'eq'}, merged_validators) + + def test_merge_extractor(self): + api_extrators = [{"var1": "val1"}, {"var2": "val2"}] + current_extractors = [{"var1": "val111"}, {"var3": "val3"}] + + merged_extractors = utils._merge_extractor(api_extrators, current_extractors) + self.assertIn( + {"var1": "val111"}, + merged_extractors + ) + self.assertIn( + {"var2": "val2"}, + merged_extractors + ) + self.assertIn( + {"var3": "val3"}, + merged_extractors + ) + def test_get_uniform_comparator(self): self.assertEqual(utils.get_uniform_comparator("eq"), "equals") self.assertEqual(utils.get_uniform_comparator("=="), "equals")