mirror of
https://github.com/httprunner/httprunner.git
synced 2026-07-10 06:51:44 +08:00
refactor: make step extensible to support implementing new protocols and test types for python version
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
## v4.0.0-alpha
|
## v4.0.0-alpha
|
||||||
|
|
||||||
- refactor: merge [hrp] into httprunner v4, which will include golang and python dual engine
|
- refactor: merge [hrp] into httprunner v4, which will include golang and python dual engine
|
||||||
|
- refactor: redesign `IStep` to make step extensible to support implementing new protocols and test types
|
||||||
|
|
||||||
**go version**
|
**go version**
|
||||||
|
|
||||||
@@ -11,7 +12,6 @@
|
|||||||
- change: integrate [sentry sdk][sentry sdk] for panic reporting and analysis
|
- change: integrate [sentry sdk][sentry sdk] for panic reporting and analysis
|
||||||
- change: lock funplugin version when creating scaffold project
|
- change: lock funplugin version when creating scaffold project
|
||||||
- fix: call referenced api/testcase with relative path
|
- fix: call referenced api/testcase with relative path
|
||||||
- refactor: redesign `IStep` to make step extensible to support implementing new protocols and test types
|
|
||||||
|
|
||||||
**python version**
|
**python version**
|
||||||
|
|
||||||
|
|||||||
@@ -257,6 +257,12 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// override step variables
|
||||||
|
stepVariables, err := r.MergeStepVariables(step.Variables)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
sessionData := newSessionData()
|
sessionData := newSessionData()
|
||||||
parser := r.GetParser()
|
parser := r.GetParser()
|
||||||
config := r.GetConfig()
|
config := r.GetConfig()
|
||||||
@@ -264,12 +270,6 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
|
|||||||
rb := newRequestBuilder(parser, config, step.Request)
|
rb := newRequestBuilder(parser, config, step.Request)
|
||||||
rb.req.Method = string(step.Request.Method)
|
rb.req.Method = string(step.Request.Method)
|
||||||
|
|
||||||
// override step variables
|
|
||||||
stepVariables, err := r.MergeStepVariables(step.Variables)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = rb.prepareUrlParams(stepVariables)
|
err = rb.prepareUrlParams(stepVariables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error
|
|||||||
return stepResult, err
|
return stepResult, err
|
||||||
}
|
}
|
||||||
summary := sessionRunner.GetSummary()
|
summary := sessionRunner.GetSummary()
|
||||||
stepResult.Data = summary
|
stepResult.Data = summary.Records
|
||||||
// export testcase export variables
|
// export testcase export variables
|
||||||
stepResult.ExportVars = sessionRunner.summary.InOut.ExportVars
|
stepResult.ExportVars = summary.InOut.ExportVars
|
||||||
stepResult.Success = true
|
stepResult.Success = true
|
||||||
|
|
||||||
// update extracted variables
|
// update extracted variables
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
__version__ = "4.0.0-alpha"
|
__version__ = "4.0.0-alpha"
|
||||||
__description__ = "One-stop solution for HTTP(S) testing."
|
__description__ = "One-stop solution for HTTP(S) testing."
|
||||||
|
|
||||||
# import firstly for monkey patch if needed
|
from httprunner.config import Config
|
||||||
from httprunner.parser import parse_parameters as Parameters
|
from httprunner.parser import parse_parameters as Parameters
|
||||||
from httprunner.runner import HttpRunner
|
from httprunner.runner import HttpRunner
|
||||||
from httprunner.testcase import Config, Step, RunRequest, RunTestCase
|
from httprunner.step import Step
|
||||||
|
from httprunner.step_request import RunRequest
|
||||||
|
from httprunner.step_testcase import RunTestCase
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"__version__",
|
"__version__",
|
||||||
|
|||||||
60
httprunner/config.py
Normal file
60
httprunner/config.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import inspect
|
||||||
|
from typing import Text
|
||||||
|
|
||||||
|
from httprunner.models import TConfig
|
||||||
|
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
def __init__(self, name: Text):
|
||||||
|
self.__name = name
|
||||||
|
self.__variables = {}
|
||||||
|
self.__base_url = ""
|
||||||
|
self.__verify = False
|
||||||
|
self.__export = []
|
||||||
|
self.__weight = 1
|
||||||
|
|
||||||
|
caller_frame = inspect.stack()[1]
|
||||||
|
self.__path = caller_frame.filename
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> Text:
|
||||||
|
return self.__name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self) -> Text:
|
||||||
|
return self.__path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def weight(self) -> int:
|
||||||
|
return self.__weight
|
||||||
|
|
||||||
|
def variables(self, **variables) -> "Config":
|
||||||
|
self.__variables.update(variables)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def base_url(self, base_url: Text) -> "Config":
|
||||||
|
self.__base_url = base_url
|
||||||
|
return self
|
||||||
|
|
||||||
|
def verify(self, verify: bool) -> "Config":
|
||||||
|
self.__verify = verify
|
||||||
|
return self
|
||||||
|
|
||||||
|
def export(self, *export_var_name: Text) -> "Config":
|
||||||
|
self.__export.extend(export_var_name)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def locust_weight(self, weight: int) -> "Config":
|
||||||
|
self.__weight = weight
|
||||||
|
return self
|
||||||
|
|
||||||
|
def struct(self) -> TConfig:
|
||||||
|
return TConfig(
|
||||||
|
name=self.__name,
|
||||||
|
base_url=self.__base_url,
|
||||||
|
verify=self.__verify,
|
||||||
|
variables=self.__variables,
|
||||||
|
export=list(set(self.__export)),
|
||||||
|
path=self.__path,
|
||||||
|
weight=self.__weight,
|
||||||
|
)
|
||||||
@@ -156,15 +156,35 @@ class SessionData(BaseModel):
|
|||||||
class StepData(BaseModel):
|
class StepData(BaseModel):
|
||||||
"""teststep data, each step maybe corresponding to one request or one testcase"""
|
"""teststep data, each step maybe corresponding to one request or one testcase"""
|
||||||
|
|
||||||
|
name: Text = "" # teststep name
|
||||||
|
step_type: Text = "" # teststep type, request or testcase
|
||||||
success: bool = False
|
success: bool = False
|
||||||
name: Text = "" # teststep name
|
|
||||||
data: Union[SessionData, List['StepData']] = None
|
data: Union[SessionData, List['StepData']] = None
|
||||||
|
elapsed: float = 0.0 # teststep elapsed time
|
||||||
|
content_size: float = 0 # response content size
|
||||||
export_vars: VariablesMapping = {}
|
export_vars: VariablesMapping = {}
|
||||||
|
attachment: Text = "" # teststep attachment
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
StepData.update_forward_refs()
|
StepData.update_forward_refs()
|
||||||
|
|
||||||
|
|
||||||
|
class IStep(object):
|
||||||
|
|
||||||
|
def name(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def type(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def struct(self) -> TStep:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def run(self, runner) -> StepData:
|
||||||
|
# runner: HttpRunner
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class TestCaseSummary(BaseModel):
|
class TestCaseSummary(BaseModel):
|
||||||
name: Text
|
name: Text
|
||||||
success: bool
|
success: bool
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import ast
|
import ast
|
||||||
import builtins
|
import builtins
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
from typing import Any, Set, Text, Callable, List, Dict, Union
|
import re
|
||||||
|
from typing import Any, Callable, Dict, List, Set, Text
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
from httprunner import loader, utils, exceptions
|
from httprunner import exceptions, loader, utils
|
||||||
from httprunner.models import VariablesMapping, FunctionsMapping
|
from httprunner.models import FunctionsMapping, VariablesMapping
|
||||||
|
|
||||||
absolute_http_url_regexp = re.compile(r"^https?://", re.I)
|
absolute_http_url_regexp = re.compile(r"^https?://", re.I)
|
||||||
|
|
||||||
@@ -572,3 +572,21 @@ def parse_parameters(parameters: Dict,) -> List[Dict]:
|
|||||||
parsed_parameters_list.append(parameter_content_list)
|
parsed_parameters_list.append(parameter_content_list)
|
||||||
|
|
||||||
return utils.gen_cartesian_product(*parsed_parameters_list)
|
return utils.gen_cartesian_product(*parsed_parameters_list)
|
||||||
|
|
||||||
|
|
||||||
|
class Parser(object):
|
||||||
|
|
||||||
|
def __init__(self, functions_mapping: FunctionsMapping = None) -> None:
|
||||||
|
self.functions_mapping = functions_mapping
|
||||||
|
|
||||||
|
def parse_string(self, raw_string: Text, variables_mapping: VariablesMapping) -> Any:
|
||||||
|
return parse_string(raw_string, variables_mapping, self.functions_mapping)
|
||||||
|
|
||||||
|
def parse_variables(self, variables_mapping: VariablesMapping) -> VariablesMapping:
|
||||||
|
return parse_variables_mapping(variables_mapping, self.functions_mapping)
|
||||||
|
|
||||||
|
def parse_data(self, raw_data: Any, variables_mapping: VariablesMapping = None) -> Any:
|
||||||
|
return parse_data(raw_data, variables_mapping, self.functions_mapping)
|
||||||
|
|
||||||
|
def get_mapping_function(self, func_name: Text) -> Callable:
|
||||||
|
return get_mapping_function(func_name, self.functions_mapping)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from loguru import logger
|
|||||||
|
|
||||||
from httprunner import exceptions
|
from httprunner import exceptions
|
||||||
from httprunner.exceptions import ValidationFailure, ParamsError
|
from httprunner.exceptions import ValidationFailure, ParamsError
|
||||||
from httprunner.models import VariablesMapping, Validators, FunctionsMapping
|
from httprunner.models import VariablesMapping, Validators
|
||||||
from httprunner.parser import parse_data, parse_string_value, get_mapping_function
|
from httprunner.parser import parse_string_value, Parser
|
||||||
|
|
||||||
|
|
||||||
def get_uniform_comparator(comparator: Text):
|
def get_uniform_comparator(comparator: Text):
|
||||||
@@ -115,7 +115,7 @@ def uniform_validator(validator):
|
|||||||
|
|
||||||
|
|
||||||
class ResponseObject(object):
|
class ResponseObject(object):
|
||||||
def __init__(self, resp_obj: requests.Response):
|
def __init__(self, resp_obj: requests.Response, parser: Parser):
|
||||||
""" initialize with a requests.Response object
|
""" initialize with a requests.Response object
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -123,6 +123,7 @@ class ResponseObject(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
self.resp_obj = resp_obj
|
self.resp_obj = resp_obj
|
||||||
|
self.parser = parser
|
||||||
self.validation_results: Dict = {}
|
self.validation_results: Dict = {}
|
||||||
|
|
||||||
def __getattr__(self, key):
|
def __getattr__(self, key):
|
||||||
@@ -170,7 +171,6 @@ class ResponseObject(object):
|
|||||||
def extract(self,
|
def extract(self,
|
||||||
extractors: Dict[Text, Text],
|
extractors: Dict[Text, Text],
|
||||||
variables_mapping: VariablesMapping = None,
|
variables_mapping: VariablesMapping = None,
|
||||||
functions_mapping: FunctionsMapping = None,
|
|
||||||
) -> Dict[Text, Any]:
|
) -> Dict[Text, Any]:
|
||||||
if not extractors:
|
if not extractors:
|
||||||
return {}
|
return {}
|
||||||
@@ -179,8 +179,8 @@ class ResponseObject(object):
|
|||||||
for key, field in extractors.items():
|
for key, field in extractors.items():
|
||||||
if '$' in field:
|
if '$' in field:
|
||||||
# field contains variable or function
|
# field contains variable or function
|
||||||
field = parse_data(
|
field = self.parser.parse_data(
|
||||||
field, variables_mapping, functions_mapping
|
field, variables_mapping
|
||||||
)
|
)
|
||||||
field_value = self._search_jmespath(field)
|
field_value = self._search_jmespath(field)
|
||||||
extract_mapping[key] = field_value
|
extract_mapping[key] = field_value
|
||||||
@@ -192,11 +192,9 @@ class ResponseObject(object):
|
|||||||
self,
|
self,
|
||||||
validators: Validators,
|
validators: Validators,
|
||||||
variables_mapping: VariablesMapping = None,
|
variables_mapping: VariablesMapping = None,
|
||||||
functions_mapping: FunctionsMapping = None,
|
|
||||||
):
|
):
|
||||||
|
|
||||||
variables_mapping = variables_mapping or {}
|
variables_mapping = variables_mapping or {}
|
||||||
functions_mapping = functions_mapping or {}
|
|
||||||
|
|
||||||
self.validation_results = {}
|
self.validation_results = {}
|
||||||
if not validators:
|
if not validators:
|
||||||
@@ -216,8 +214,8 @@ class ResponseObject(object):
|
|||||||
check_item = u_validator["check"]
|
check_item = u_validator["check"]
|
||||||
if "$" in check_item:
|
if "$" in check_item:
|
||||||
# check_item is variable or function
|
# check_item is variable or function
|
||||||
check_item = parse_data(
|
check_item = self.parser.parse_data(
|
||||||
check_item, variables_mapping, functions_mapping
|
check_item, variables_mapping
|
||||||
)
|
)
|
||||||
check_item = parse_string_value(check_item)
|
check_item = parse_string_value(check_item)
|
||||||
|
|
||||||
@@ -229,17 +227,17 @@ class ResponseObject(object):
|
|||||||
|
|
||||||
# comparator
|
# comparator
|
||||||
assert_method = u_validator["assert"]
|
assert_method = u_validator["assert"]
|
||||||
assert_func = get_mapping_function(assert_method, functions_mapping)
|
assert_func = self.parser.get_mapping_function(assert_method)
|
||||||
|
|
||||||
# expect item
|
# expect item
|
||||||
expect_item = u_validator["expect"]
|
expect_item = u_validator["expect"]
|
||||||
# parse expected value with config/teststep/extracted variables
|
# parse expected value with config/teststep/extracted variables
|
||||||
expect_value = parse_data(expect_item, variables_mapping, functions_mapping)
|
expect_value = self.parser.parse_data(expect_item, variables_mapping)
|
||||||
|
|
||||||
# message
|
# message
|
||||||
message = u_validator["message"]
|
message = u_validator["message"]
|
||||||
# parse message with config/teststep/extracted variables
|
# parse message with config/teststep/extracted variables
|
||||||
message = parse_data(message, variables_mapping, functions_mapping)
|
message = self.parser.parse_data(message, variables_mapping)
|
||||||
|
|
||||||
validate_msg = f"assert {check_item} {assert_method} {expect_value}({type(expect_value).__name__})"
|
validate_msg = f"assert {check_item} {assert_method} {expect_value}({type(expect_value).__name__})"
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Dict, Text
|
from typing import Dict, List, Text
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import allure
|
import allure
|
||||||
@@ -13,41 +13,29 @@ except ModuleNotFoundError:
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from httprunner import utils, exceptions
|
|
||||||
from httprunner.client import HttpSession
|
from httprunner.client import HttpSession
|
||||||
from httprunner.exceptions import ValidationFailure, ParamsError
|
from httprunner.config import Config
|
||||||
from httprunner.ext.uploader import prepare_upload_step
|
from httprunner.exceptions import ParamsError
|
||||||
from httprunner.loader import load_project_meta, load_testcase_file
|
from httprunner.loader import load_project_meta
|
||||||
from httprunner.parser import build_url, parse_data, parse_variables_mapping
|
from httprunner.models import (ProjectMeta, StepData, TConfig, TestCaseInOut,
|
||||||
from httprunner.response import ResponseObject
|
TestCaseSummary, TestCaseTime, VariablesMapping)
|
||||||
from httprunner.testcase import Config, Step
|
from httprunner.parser import Parser
|
||||||
from httprunner.utils import merge_variables
|
from httprunner.utils import merge_variables
|
||||||
from httprunner.models import (
|
|
||||||
TConfig,
|
|
||||||
TStep,
|
|
||||||
VariablesMapping,
|
|
||||||
StepData,
|
|
||||||
TestCaseSummary,
|
|
||||||
TestCaseTime,
|
|
||||||
TestCaseInOut,
|
|
||||||
ProjectMeta,
|
|
||||||
TestCase,
|
|
||||||
Hooks,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class HttpRunner(object):
|
class HttpRunner(object):
|
||||||
config: Config
|
config: Config
|
||||||
teststeps: List[Step]
|
teststeps: List[object] # list of Step
|
||||||
|
|
||||||
|
parser: Parser = None
|
||||||
|
session: HttpSession = None
|
||||||
|
case_id: Text = ""
|
||||||
|
root_dir: Text = ""
|
||||||
|
|
||||||
success: bool = False # indicate testcase execution result
|
|
||||||
__config: TConfig
|
__config: TConfig
|
||||||
__teststeps: List[TStep]
|
|
||||||
__project_meta: ProjectMeta = None
|
__project_meta: ProjectMeta = None
|
||||||
__case_id: Text = ""
|
|
||||||
__export: List[Text] = []
|
__export: List[Text] = []
|
||||||
__step_datas: List[StepData] = []
|
__step_datas: List[StepData] = []
|
||||||
__session: HttpSession = None
|
|
||||||
__session_variables: VariablesMapping = {}
|
__session_variables: VariablesMapping = {}
|
||||||
# time
|
# time
|
||||||
__start_at: float = 0
|
__start_at: float = 0
|
||||||
@@ -55,29 +43,34 @@ class HttpRunner(object):
|
|||||||
# log
|
# log
|
||||||
__log_path: Text = ""
|
__log_path: Text = ""
|
||||||
|
|
||||||
def __init_tests__(self):
|
def __init(self):
|
||||||
self.__config = self.config.perform()
|
self.__config = self.config.struct()
|
||||||
self.__teststeps = []
|
self.__session_variables = {}
|
||||||
for step in self.teststeps:
|
self.__start_at = 0
|
||||||
self.__teststeps.append(step.perform())
|
self.__duration = 0
|
||||||
|
|
||||||
@property
|
self.__project_meta = self.__project_meta or load_project_meta(
|
||||||
def raw_testcase(self) -> TestCase:
|
self.__config.path
|
||||||
if not hasattr(self, "__config"):
|
)
|
||||||
self.__init_tests__()
|
self.case_id = self.case_id or str(uuid.uuid4())
|
||||||
|
self.root_dir = self.root_dir or self.__project_meta.RootDir
|
||||||
|
self.__log_path = os.path.join(
|
||||||
|
self.root_dir, "logs", f"{self.case_id}.run.log"
|
||||||
|
)
|
||||||
|
|
||||||
return TestCase(config=self.__config, teststeps=self.__teststeps)
|
self.__step_datas.clear()
|
||||||
|
self.session = self.session or HttpSession()
|
||||||
def with_project_meta(self, project_meta: ProjectMeta) -> "HttpRunner":
|
self.parser = self.parser or Parser(self.__project_meta.functions)
|
||||||
self.__project_meta = project_meta
|
|
||||||
return self
|
|
||||||
|
|
||||||
def with_session(self, session: HttpSession) -> "HttpRunner":
|
def with_session(self, session: HttpSession) -> "HttpRunner":
|
||||||
self.__session = session
|
self.session = session
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def get_config(self) -> TConfig:
|
||||||
|
return self.__config
|
||||||
|
|
||||||
def with_case_id(self, case_id: Text) -> "HttpRunner":
|
def with_case_id(self, case_id: Text) -> "HttpRunner":
|
||||||
self.__case_id = case_id
|
self.case_id = case_id
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def with_variables(self, variables: VariablesMapping) -> "HttpRunner":
|
def with_variables(self, variables: VariablesMapping) -> "HttpRunner":
|
||||||
@@ -88,302 +81,24 @@ class HttpRunner(object):
|
|||||||
self.__export = export
|
self.__export = export
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __call_hooks(self, hooks: Hooks, step_variables: VariablesMapping, hook_msg: Text):
|
def __parse_config(self, param: Dict = None) -> None:
|
||||||
""" call hook actions.
|
# parse config variables
|
||||||
|
self.__config.variables.update(self.__session_variables)
|
||||||
Args:
|
if param:
|
||||||
hooks (list): each hook in hooks list maybe in two format.
|
self.__config.variables.update(param)
|
||||||
|
self.__config.variables = self.parser.parse_variables(
|
||||||
format1 (str): only call hook functions.
|
self.__config.variables
|
||||||
${func()}
|
|
||||||
format2 (dict): assignment, the value returned by hook function will be assigned to variable.
|
|
||||||
{"var": "${func()}"}
|
|
||||||
|
|
||||||
step_variables: current step variables to call hook, include two special variables
|
|
||||||
|
|
||||||
request: parsed request dict
|
|
||||||
response: ResponseObject for current response
|
|
||||||
|
|
||||||
hook_msg: setup/teardown request/testcase
|
|
||||||
|
|
||||||
"""
|
|
||||||
logger.info(f"call hook actions: {hook_msg}")
|
|
||||||
|
|
||||||
if not isinstance(hooks, List):
|
|
||||||
logger.error(f"Invalid hooks format: {hooks}")
|
|
||||||
return
|
|
||||||
|
|
||||||
for hook in hooks:
|
|
||||||
if isinstance(hook, Text):
|
|
||||||
# format 1: ["${func()}"]
|
|
||||||
logger.debug(f"call hook function: {hook}")
|
|
||||||
parse_data(hook, step_variables, self.__project_meta.functions)
|
|
||||||
elif isinstance(hook, Dict) and len(hook) == 1:
|
|
||||||
# format 2: {"var": "${func()}"}
|
|
||||||
var_name, hook_content = list(hook.items())[0]
|
|
||||||
hook_content_eval = parse_data(
|
|
||||||
hook_content, step_variables, self.__project_meta.functions
|
|
||||||
)
|
|
||||||
logger.debug(
|
|
||||||
f"call hook function: {hook_content}, got value: {hook_content_eval}"
|
|
||||||
)
|
|
||||||
logger.debug(f"assign variable: {var_name} = {hook_content_eval}")
|
|
||||||
step_variables[var_name] = hook_content_eval
|
|
||||||
else:
|
|
||||||
logger.error(f"Invalid hook format: {hook}")
|
|
||||||
|
|
||||||
def __run_step_request(self, step: TStep) -> StepData:
|
|
||||||
"""run teststep: request"""
|
|
||||||
step_data = StepData(name=step.name)
|
|
||||||
|
|
||||||
# parse
|
|
||||||
functions = self.__project_meta.functions
|
|
||||||
prepare_upload_step(step, functions)
|
|
||||||
request_dict = step.request.dict()
|
|
||||||
request_dict.pop("upload", None)
|
|
||||||
parsed_request_dict = parse_data(
|
|
||||||
request_dict, step.variables, functions
|
|
||||||
)
|
|
||||||
parsed_request_dict["headers"].setdefault(
|
|
||||||
"HRUN-Request-ID",
|
|
||||||
f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}",
|
|
||||||
)
|
|
||||||
step.variables["request"] = parsed_request_dict
|
|
||||||
|
|
||||||
# setup hooks
|
|
||||||
if step.setup_hooks:
|
|
||||||
self.__call_hooks(step.setup_hooks, step.variables, "setup request")
|
|
||||||
|
|
||||||
# prepare arguments
|
|
||||||
method = parsed_request_dict.pop("method")
|
|
||||||
url_path = parsed_request_dict.pop("url")
|
|
||||||
url = build_url(self.__config.base_url, url_path)
|
|
||||||
parsed_request_dict["verify"] = self.__config.verify
|
|
||||||
parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {})
|
|
||||||
|
|
||||||
# request
|
|
||||||
resp = self.__session.request(method, url, **parsed_request_dict)
|
|
||||||
resp_obj = ResponseObject(resp)
|
|
||||||
step.variables["response"] = resp_obj
|
|
||||||
|
|
||||||
# teardown hooks
|
|
||||||
if step.teardown_hooks:
|
|
||||||
self.__call_hooks(step.teardown_hooks, step.variables, "teardown request")
|
|
||||||
|
|
||||||
def log_req_resp_details():
|
|
||||||
err_msg = "\n{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)
|
|
||||||
|
|
||||||
# log request
|
|
||||||
err_msg += "====== request details ======\n"
|
|
||||||
err_msg += f"url: {url}\n"
|
|
||||||
err_msg += f"method: {method}\n"
|
|
||||||
headers = parsed_request_dict.pop("headers", {})
|
|
||||||
err_msg += f"headers: {headers}\n"
|
|
||||||
for k, v in parsed_request_dict.items():
|
|
||||||
v = utils.omit_long_data(v)
|
|
||||||
err_msg += f"{k}: {repr(v)}\n"
|
|
||||||
|
|
||||||
err_msg += "\n"
|
|
||||||
|
|
||||||
# log response
|
|
||||||
err_msg += "====== response details ======\n"
|
|
||||||
err_msg += f"status_code: {resp.status_code}\n"
|
|
||||||
err_msg += f"headers: {resp.headers}\n"
|
|
||||||
err_msg += f"body: {repr(resp.text)}\n"
|
|
||||||
logger.error(err_msg)
|
|
||||||
|
|
||||||
# extract
|
|
||||||
extractors = step.extract
|
|
||||||
extract_mapping = resp_obj.extract(extractors, step.variables, functions)
|
|
||||||
step_data.export_vars = extract_mapping
|
|
||||||
|
|
||||||
variables_mapping = step.variables
|
|
||||||
variables_mapping.update(extract_mapping)
|
|
||||||
|
|
||||||
# validate
|
|
||||||
validators = step.validators
|
|
||||||
session_success = False
|
|
||||||
try:
|
|
||||||
resp_obj.validate(
|
|
||||||
validators, variables_mapping, functions
|
|
||||||
)
|
|
||||||
session_success = True
|
|
||||||
except ValidationFailure:
|
|
||||||
session_success = False
|
|
||||||
log_req_resp_details()
|
|
||||||
# log testcase duration before raise ValidationFailure
|
|
||||||
self.__duration = time.time() - self.__start_at
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
self.success = session_success
|
|
||||||
step_data.success = session_success
|
|
||||||
|
|
||||||
if hasattr(self.__session, "data"):
|
|
||||||
# httprunner.client.HttpSession, not locust.clients.HttpSession
|
|
||||||
# save request & response meta data
|
|
||||||
self.__session.data.success = session_success
|
|
||||||
self.__session.data.validators = resp_obj.validation_results
|
|
||||||
|
|
||||||
# save step data
|
|
||||||
step_data.data = self.__session.data
|
|
||||||
|
|
||||||
return step_data
|
|
||||||
|
|
||||||
def __run_step_testcase(self, step: TStep) -> StepData:
|
|
||||||
"""run teststep: referenced testcase"""
|
|
||||||
step_data = StepData(name=step.name)
|
|
||||||
step_variables = step.variables
|
|
||||||
step_export = step.export
|
|
||||||
|
|
||||||
# setup hooks
|
|
||||||
if step.setup_hooks:
|
|
||||||
self.__call_hooks(step.setup_hooks, step_variables, "setup testcase")
|
|
||||||
|
|
||||||
if hasattr(step.testcase, "config") and hasattr(step.testcase, "teststeps"):
|
|
||||||
testcase_cls = step.testcase
|
|
||||||
case_result = (
|
|
||||||
testcase_cls()
|
|
||||||
.with_session(self.__session)
|
|
||||||
.with_case_id(self.__case_id)
|
|
||||||
.with_variables(step_variables)
|
|
||||||
.with_export(step_export)
|
|
||||||
.run()
|
|
||||||
)
|
|
||||||
|
|
||||||
elif isinstance(step.testcase, Text):
|
|
||||||
if os.path.isabs(step.testcase):
|
|
||||||
ref_testcase_path = step.testcase
|
|
||||||
else:
|
|
||||||
ref_testcase_path = os.path.join(
|
|
||||||
self.__project_meta.RootDir, step.testcase
|
|
||||||
)
|
|
||||||
|
|
||||||
case_result = (
|
|
||||||
HttpRunner()
|
|
||||||
.with_session(self.__session)
|
|
||||||
.with_case_id(self.__case_id)
|
|
||||||
.with_variables(step_variables)
|
|
||||||
.with_export(step_export)
|
|
||||||
.run_path(ref_testcase_path)
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise exceptions.ParamsError(
|
|
||||||
f"Invalid teststep referenced testcase: {step.dict()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# teardown hooks
|
|
||||||
if step.teardown_hooks:
|
|
||||||
self.__call_hooks(step.teardown_hooks, step.variables, "teardown testcase")
|
|
||||||
|
|
||||||
step_data.data = case_result.get_step_datas() # list of step data
|
|
||||||
step_data.export_vars = case_result.get_export_variables()
|
|
||||||
step_data.success = case_result.success
|
|
||||||
self.success = case_result.success
|
|
||||||
|
|
||||||
if step_data.export_vars:
|
|
||||||
logger.info(f"export variables: {step_data.export_vars}")
|
|
||||||
|
|
||||||
return step_data
|
|
||||||
|
|
||||||
def __run_step(self, step: TStep) -> Dict:
|
|
||||||
"""run teststep, teststep maybe a request or referenced testcase"""
|
|
||||||
logger.info(f"run step begin: {step.name} >>>>>>")
|
|
||||||
|
|
||||||
if step.request:
|
|
||||||
step_data = self.__run_step_request(step)
|
|
||||||
elif step.testcase:
|
|
||||||
step_data = self.__run_step_testcase(step)
|
|
||||||
else:
|
|
||||||
raise ParamsError(
|
|
||||||
f"teststep is neither a request nor a referenced testcase: {step.dict()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.__step_datas.append(step_data)
|
|
||||||
logger.info(f"run step end: {step.name} <<<<<<\n")
|
|
||||||
return step_data.export_vars
|
|
||||||
|
|
||||||
def __parse_config(self, config: TConfig):
|
|
||||||
config.variables.update(self.__session_variables)
|
|
||||||
config.variables = parse_variables_mapping(
|
|
||||||
config.variables, self.__project_meta.functions
|
|
||||||
)
|
|
||||||
config.name = parse_data(
|
|
||||||
config.name, config.variables, self.__project_meta.functions
|
|
||||||
)
|
|
||||||
config.base_url = parse_data(
|
|
||||||
config.base_url, config.variables, self.__project_meta.functions
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def run_testcase(self, testcase: TestCase) -> "HttpRunner":
|
# parse config name
|
||||||
"""run specified testcase
|
self.__config.name = self.parser.parse_data(
|
||||||
|
self.__config.name, self.__config.variables
|
||||||
Examples:
|
|
||||||
>>> testcase_obj = TestCase(config=TConfig(...), teststeps=[TStep(...)])
|
|
||||||
>>> HttpRunner().with_project_meta(project_meta).run_testcase(testcase_obj)
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.__config = testcase.config
|
|
||||||
self.__teststeps = testcase.teststeps
|
|
||||||
|
|
||||||
# prepare
|
|
||||||
self.__project_meta = self.__project_meta or load_project_meta(
|
|
||||||
self.__config.path
|
|
||||||
)
|
)
|
||||||
self.__parse_config(self.__config)
|
|
||||||
self.__start_at = time.time()
|
|
||||||
self.__step_datas: List[StepData] = []
|
|
||||||
self.__session = self.__session or HttpSession()
|
|
||||||
# save extracted variables of teststeps
|
|
||||||
extracted_variables: VariablesMapping = {}
|
|
||||||
|
|
||||||
# run teststeps
|
# parse config base url
|
||||||
for step in self.__teststeps:
|
self.__config.base_url = self.parser.parse_data(
|
||||||
# override variables
|
self.__config.base_url, self.__config.variables
|
||||||
# step variables > extracted variables from previous steps
|
)
|
||||||
step.variables = merge_variables(step.variables, extracted_variables)
|
|
||||||
# step variables > testcase config variables
|
|
||||||
step.variables = merge_variables(step.variables, self.__config.variables)
|
|
||||||
|
|
||||||
# parse variables
|
|
||||||
step.variables = parse_variables_mapping(
|
|
||||||
step.variables, self.__project_meta.functions
|
|
||||||
)
|
|
||||||
|
|
||||||
# run step
|
|
||||||
if USE_ALLURE:
|
|
||||||
with allure.step(f"step: {step.name}"):
|
|
||||||
extract_mapping = self.__run_step(step)
|
|
||||||
else:
|
|
||||||
extract_mapping = self.__run_step(step)
|
|
||||||
|
|
||||||
# save extracted variables to session variables
|
|
||||||
extracted_variables.update(extract_mapping)
|
|
||||||
|
|
||||||
self.__session_variables.update(extracted_variables)
|
|
||||||
self.__duration = time.time() - self.__start_at
|
|
||||||
return self
|
|
||||||
|
|
||||||
def run_path(self, path: Text) -> "HttpRunner":
|
|
||||||
if not os.path.isfile(path):
|
|
||||||
raise exceptions.ParamsError(f"Invalid testcase path: {path}")
|
|
||||||
|
|
||||||
testcase_obj = load_testcase_file(path)
|
|
||||||
return self.run_testcase(testcase_obj)
|
|
||||||
|
|
||||||
def run(self) -> "HttpRunner":
|
|
||||||
""" run current testcase
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
>>> TestCaseRequestWithFunctions().run()
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.__init_tests__()
|
|
||||||
testcase_obj = TestCase(config=self.__config, teststeps=self.__teststeps)
|
|
||||||
return self.run_testcase(testcase_obj)
|
|
||||||
|
|
||||||
def get_step_datas(self) -> List[StepData]:
|
|
||||||
return self.__step_datas
|
|
||||||
|
|
||||||
def get_export_variables(self) -> Dict:
|
def get_export_variables(self) -> Dict:
|
||||||
# override testcase export vars with step export
|
# override testcase export vars with step export
|
||||||
@@ -403,10 +118,17 @@ class HttpRunner(object):
|
|||||||
"""get testcase result summary"""
|
"""get testcase result summary"""
|
||||||
start_at_timestamp = self.__start_at
|
start_at_timestamp = self.__start_at
|
||||||
start_at_iso_format = datetime.utcfromtimestamp(start_at_timestamp).isoformat()
|
start_at_iso_format = datetime.utcfromtimestamp(start_at_timestamp).isoformat()
|
||||||
|
|
||||||
|
summary_success = True
|
||||||
|
for step_data in self.__step_datas:
|
||||||
|
if not step_data.success:
|
||||||
|
summary_success = False
|
||||||
|
break
|
||||||
|
|
||||||
return TestCaseSummary(
|
return TestCaseSummary(
|
||||||
name=self.__config.name,
|
name=self.__config.name,
|
||||||
success=self.success,
|
success=summary_success,
|
||||||
case_id=self.__case_id,
|
case_id=self.case_id,
|
||||||
time=TestCaseTime(
|
time=TestCaseTime(
|
||||||
start_at=self.__start_at,
|
start_at=self.__start_at,
|
||||||
start_at_iso_format=start_at_iso_format,
|
start_at_iso_format=start_at_iso_format,
|
||||||
@@ -420,40 +142,63 @@ class HttpRunner(object):
|
|||||||
step_datas=self.__step_datas,
|
step_datas=self.__step_datas,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_start(self, param: Dict = None) -> "HttpRunner":
|
def merge_step_variables(self, variables: VariablesMapping) -> VariablesMapping:
|
||||||
|
# override variables
|
||||||
|
# step variables > extracted variables from previous steps
|
||||||
|
variables = merge_variables(variables, self.__session_variables)
|
||||||
|
# step variables > testcase config variables
|
||||||
|
variables = merge_variables(variables, self.__config.variables)
|
||||||
|
|
||||||
|
# parse variables
|
||||||
|
return self.parser.parse_variables(variables)
|
||||||
|
|
||||||
|
def __run_step(self, step) -> Dict:
|
||||||
|
"""run teststep, step maybe any kind that implements IStep interface
|
||||||
|
|
||||||
|
Args:
|
||||||
|
step (Step): teststep
|
||||||
|
|
||||||
|
"""
|
||||||
|
logger.info(f"run step begin: {step.name()} >>>>>>")
|
||||||
|
|
||||||
|
# run step
|
||||||
|
if USE_ALLURE:
|
||||||
|
with allure.step(f"step: {step.name()}"):
|
||||||
|
step_result = step.run(self)
|
||||||
|
else:
|
||||||
|
step_result = step.run(self)
|
||||||
|
|
||||||
|
# save extracted variables to session variables
|
||||||
|
self.__session_variables.update(step_result.export_vars)
|
||||||
|
# update testcase summary
|
||||||
|
self.__step_datas.append(step_result)
|
||||||
|
|
||||||
|
logger.info(f"run step end: {step.name()} <<<<<<\n")
|
||||||
|
|
||||||
|
def test_start(self, param: Dict = None):
|
||||||
"""main entrance, discovered by pytest"""
|
"""main entrance, discovered by pytest"""
|
||||||
self.__init_tests__()
|
self.__init()
|
||||||
self.__project_meta = self.__project_meta or load_project_meta(
|
|
||||||
self.__config.path
|
|
||||||
)
|
|
||||||
self.__case_id = self.__case_id or str(uuid.uuid4())
|
|
||||||
self.__log_path = self.__log_path or os.path.join(
|
|
||||||
self.__project_meta.RootDir, "logs", f"{self.__case_id}.run.log"
|
|
||||||
)
|
|
||||||
log_handler = logger.add(self.__log_path, level="DEBUG")
|
log_handler = logger.add(self.__log_path, level="DEBUG")
|
||||||
|
|
||||||
# parse config name
|
self.__parse_config(param)
|
||||||
config_variables = self.__config.variables
|
|
||||||
if param:
|
|
||||||
config_variables.update(param)
|
|
||||||
config_variables.update(self.__session_variables)
|
|
||||||
self.__config.name = parse_data(
|
|
||||||
self.__config.name, config_variables, self.__project_meta.functions
|
|
||||||
)
|
|
||||||
|
|
||||||
if USE_ALLURE:
|
if USE_ALLURE:
|
||||||
# update allure report meta
|
# update allure report meta
|
||||||
allure.dynamic.title(self.__config.name)
|
allure.dynamic.title(self.__config.name)
|
||||||
allure.dynamic.description(f"TestCase ID: {self.__case_id}")
|
allure.dynamic.description(f"TestCase ID: {self.case_id}")
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Start to run testcase: {self.__config.name}, TestCase ID: {self.__case_id}"
|
f"Start to run testcase: {self.__config.name}, TestCase ID: {self.case_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.__start_at = time.time()
|
||||||
try:
|
try:
|
||||||
return self.run_testcase(
|
# run step in sequential order
|
||||||
TestCase(config=self.__config, teststeps=self.__teststeps)
|
for step in self.teststeps:
|
||||||
)
|
self.__run_step(step)
|
||||||
finally:
|
finally:
|
||||||
logger.remove(log_handler)
|
logger.remove(log_handler)
|
||||||
logger.info(f"generate testcase log: {self.__log_path}")
|
logger.info(f"generate testcase log: {self.__log_path}")
|
||||||
|
|
||||||
|
self.__duration = time.time() - self.__start_at
|
||||||
|
return self
|
||||||
|
|||||||
40
httprunner/step.py
Normal file
40
httprunner/step.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from httprunner.models import StepData, TRequest, TStep, TestCase
|
||||||
|
from httprunner.runner import HttpRunner
|
||||||
|
from httprunner.step_request import RequestWithOptionalArgs, StepRequestExtraction, StepRequestValidation
|
||||||
|
from httprunner.step_testcase import StepRefCase
|
||||||
|
|
||||||
|
|
||||||
|
class Step(object):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
step: Union[
|
||||||
|
StepRequestValidation,
|
||||||
|
StepRequestExtraction,
|
||||||
|
RequestWithOptionalArgs,
|
||||||
|
StepRefCase,
|
||||||
|
],
|
||||||
|
):
|
||||||
|
self.__step = step
|
||||||
|
|
||||||
|
@property
|
||||||
|
def request(self) -> TRequest:
|
||||||
|
return self.__step.struct().request
|
||||||
|
|
||||||
|
@property
|
||||||
|
def testcase(self) -> TestCase:
|
||||||
|
return self.__step.struct().testcase
|
||||||
|
|
||||||
|
def struct(self) -> TStep:
|
||||||
|
return self.__step.struct()
|
||||||
|
|
||||||
|
def name(self) -> str:
|
||||||
|
return self.__step.name()
|
||||||
|
|
||||||
|
def type(self) -> str:
|
||||||
|
return self.__step.type()
|
||||||
|
|
||||||
|
def run(self, runner: HttpRunner) -> StepData:
|
||||||
|
return self.__step.run(runner)
|
||||||
462
httprunner/step_request.py
Normal file
462
httprunner/step_request.py
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
import time
|
||||||
|
from typing import Any, Dict, List, Text, Union
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from httprunner import utils
|
||||||
|
from httprunner.exceptions import ValidationFailure
|
||||||
|
from httprunner.ext.uploader import prepare_upload_step
|
||||||
|
from httprunner.models import (Hooks, IStep, MethodEnum, StepData, TRequest,
|
||||||
|
TStep, VariablesMapping)
|
||||||
|
from httprunner.parser import build_url
|
||||||
|
from httprunner.response import ResponseObject
|
||||||
|
from httprunner.runner import HttpRunner
|
||||||
|
|
||||||
|
|
||||||
|
def call_hooks(runner: HttpRunner, hooks: Hooks, step_variables: VariablesMapping, hook_msg: Text):
|
||||||
|
""" call hook actions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hooks (list): each hook in hooks list maybe in two format.
|
||||||
|
|
||||||
|
format1 (str): only call hook functions.
|
||||||
|
${func()}
|
||||||
|
format2 (dict): assignment, the value returned by hook function will be assigned to variable.
|
||||||
|
{"var": "${func()}"}
|
||||||
|
|
||||||
|
step_variables: current step variables to call hook, include two special variables
|
||||||
|
|
||||||
|
request: parsed request dict
|
||||||
|
response: ResponseObject for current response
|
||||||
|
|
||||||
|
hook_msg: setup/teardown request/testcase
|
||||||
|
|
||||||
|
"""
|
||||||
|
logger.info(f"call hook actions: {hook_msg}")
|
||||||
|
|
||||||
|
if not isinstance(hooks, List):
|
||||||
|
logger.error(f"Invalid hooks format: {hooks}")
|
||||||
|
return
|
||||||
|
|
||||||
|
for hook in hooks:
|
||||||
|
if isinstance(hook, Text):
|
||||||
|
# format 1: ["${func()}"]
|
||||||
|
logger.debug(f"call hook function: {hook}")
|
||||||
|
runner.parser.parse_data(hook, step_variables)
|
||||||
|
elif isinstance(hook, Dict) and len(hook) == 1:
|
||||||
|
# format 2: {"var": "${func()}"}
|
||||||
|
var_name, hook_content = list(hook.items())[0]
|
||||||
|
hook_content_eval = runner.parser.parse_data(
|
||||||
|
hook_content, step_variables
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"call hook function: {hook_content}, got value: {hook_content_eval}"
|
||||||
|
)
|
||||||
|
logger.debug(f"assign variable: {var_name} = {hook_content_eval}")
|
||||||
|
step_variables[var_name] = hook_content_eval
|
||||||
|
else:
|
||||||
|
logger.error(f"Invalid hook format: {hook}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_step_request(runner: HttpRunner, step: TStep) -> StepData:
|
||||||
|
"""run teststep: request"""
|
||||||
|
step_data = StepData(
|
||||||
|
name=step.name,
|
||||||
|
success=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
step.variables = runner.merge_step_variables(step.variables)
|
||||||
|
|
||||||
|
# parse
|
||||||
|
functions = runner.parser.functions_mapping
|
||||||
|
prepare_upload_step(step, functions)
|
||||||
|
request_dict = step.request.dict()
|
||||||
|
request_dict.pop("upload", None)
|
||||||
|
parsed_request_dict = runner.parser.parse_data(
|
||||||
|
request_dict, step.variables
|
||||||
|
)
|
||||||
|
parsed_request_dict["headers"].setdefault(
|
||||||
|
"HRUN-Request-ID",
|
||||||
|
f"HRUN-{runner.case_id}-{str(int(time.time() * 1000))[-6:]}",
|
||||||
|
)
|
||||||
|
step.variables["request"] = parsed_request_dict
|
||||||
|
|
||||||
|
# setup hooks
|
||||||
|
if step.setup_hooks:
|
||||||
|
call_hooks(runner, step.setup_hooks, step.variables, "setup request")
|
||||||
|
|
||||||
|
# prepare arguments
|
||||||
|
config = runner.get_config()
|
||||||
|
method = parsed_request_dict.pop("method")
|
||||||
|
url_path = parsed_request_dict.pop("url")
|
||||||
|
url = build_url(config.base_url, url_path)
|
||||||
|
parsed_request_dict["verify"] = config.verify
|
||||||
|
parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {})
|
||||||
|
|
||||||
|
# request
|
||||||
|
resp = runner.session.request(method, url, **parsed_request_dict)
|
||||||
|
resp_obj = ResponseObject(resp, runner.parser)
|
||||||
|
step.variables["response"] = resp_obj
|
||||||
|
|
||||||
|
# teardown hooks
|
||||||
|
if step.teardown_hooks:
|
||||||
|
call_hooks(runner, step.teardown_hooks, step.variables, "teardown request")
|
||||||
|
|
||||||
|
def log_req_resp_details():
|
||||||
|
err_msg = "\n{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)
|
||||||
|
|
||||||
|
# log request
|
||||||
|
err_msg += "====== request details ======\n"
|
||||||
|
err_msg += f"url: {url}\n"
|
||||||
|
err_msg += f"method: {method}\n"
|
||||||
|
headers = parsed_request_dict.pop("headers", {})
|
||||||
|
err_msg += f"headers: {headers}\n"
|
||||||
|
for k, v in parsed_request_dict.items():
|
||||||
|
v = utils.omit_long_data(v)
|
||||||
|
err_msg += f"{k}: {repr(v)}\n"
|
||||||
|
|
||||||
|
err_msg += "\n"
|
||||||
|
|
||||||
|
# log response
|
||||||
|
err_msg += "====== response details ======\n"
|
||||||
|
err_msg += f"status_code: {resp.status_code}\n"
|
||||||
|
err_msg += f"headers: {resp.headers}\n"
|
||||||
|
err_msg += f"body: {repr(resp.text)}\n"
|
||||||
|
logger.error(err_msg)
|
||||||
|
|
||||||
|
# extract
|
||||||
|
extractors = step.extract
|
||||||
|
extract_mapping = resp_obj.extract(extractors, step.variables)
|
||||||
|
step_data.export_vars = extract_mapping
|
||||||
|
|
||||||
|
variables_mapping = step.variables
|
||||||
|
variables_mapping.update(extract_mapping)
|
||||||
|
|
||||||
|
# validate
|
||||||
|
validators = step.validators
|
||||||
|
try:
|
||||||
|
resp_obj.validate(
|
||||||
|
validators, variables_mapping
|
||||||
|
)
|
||||||
|
step_data.success = True
|
||||||
|
except ValidationFailure:
|
||||||
|
log_req_resp_details()
|
||||||
|
# log testcase duration before raise ValidationFailure
|
||||||
|
step_data.elapsed = time.time() - runner.__start_at
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
session_data = runner.session.data
|
||||||
|
session_data.success = step_data.success
|
||||||
|
session_data.validators = resp_obj.validation_results
|
||||||
|
|
||||||
|
# save step data
|
||||||
|
step_data.data = session_data
|
||||||
|
|
||||||
|
return step_data
|
||||||
|
|
||||||
|
|
||||||
|
class StepRequestValidation(IStep):
|
||||||
|
def __init__(self, step: TStep):
|
||||||
|
self.__step = step
|
||||||
|
|
||||||
|
def assert_equal(
|
||||||
|
self, jmes_path: Text, expected_value: Any, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"equal": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_not_equal(
|
||||||
|
self, jmes_path: Text, expected_value: Any, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"not_equal": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_greater_than(
|
||||||
|
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"greater_than": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_less_than(
|
||||||
|
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"less_than": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_greater_or_equals(
|
||||||
|
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"greater_or_equals": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_less_or_equals(
|
||||||
|
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"less_or_equals": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_length_equal(
|
||||||
|
self, jmes_path: Text, expected_value: int, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"length_equal": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_length_greater_than(
|
||||||
|
self, jmes_path: Text, expected_value: int, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"length_greater_than": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_length_less_than(
|
||||||
|
self, jmes_path: Text, expected_value: int, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"length_less_than": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_length_greater_or_equals(
|
||||||
|
self, jmes_path: Text, expected_value: int, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"length_greater_or_equals": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_length_less_or_equals(
|
||||||
|
self, jmes_path: Text, expected_value: int, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"length_less_or_equals": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_string_equals(
|
||||||
|
self, jmes_path: Text, expected_value: Any, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"string_equals": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_startswith(
|
||||||
|
self, jmes_path: Text, expected_value: Text, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"startswith": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_endswith(
|
||||||
|
self, jmes_path: Text, expected_value: Text, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"endswith": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_regex_match(
|
||||||
|
self, jmes_path: Text, expected_value: Text, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"regex_match": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_contains(
|
||||||
|
self, jmes_path: Text, expected_value: Any, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"contains": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_contained_by(
|
||||||
|
self, jmes_path: Text, expected_value: Any, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"contained_by": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def assert_type_match(
|
||||||
|
self, jmes_path: Text, expected_value: Any, message: Text = ""
|
||||||
|
) -> "StepRequestValidation":
|
||||||
|
self.__step.validators.append(
|
||||||
|
{"type_match": [jmes_path, expected_value, message]}
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def struct(self) -> TStep:
|
||||||
|
return self.__step
|
||||||
|
|
||||||
|
def name(self) -> Text:
|
||||||
|
return self.__step.name
|
||||||
|
|
||||||
|
def type(self) -> Text:
|
||||||
|
return f"request-{self.__step.request.method}"
|
||||||
|
|
||||||
|
def run(self, runner: HttpRunner):
|
||||||
|
return run_step_request(runner, self.__step)
|
||||||
|
|
||||||
|
|
||||||
|
class StepRequestExtraction(IStep):
|
||||||
|
def __init__(self, step: TStep):
|
||||||
|
self.__step = step
|
||||||
|
|
||||||
|
def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepRequestExtraction":
|
||||||
|
self.__step.extract[var_name] = jmes_path
|
||||||
|
return self
|
||||||
|
|
||||||
|
# def with_regex(self):
|
||||||
|
# # TODO: extract response html with regex
|
||||||
|
# pass
|
||||||
|
#
|
||||||
|
# def with_jsonpath(self):
|
||||||
|
# # TODO: extract response json with jsonpath
|
||||||
|
# pass
|
||||||
|
|
||||||
|
def validate(self) -> StepRequestValidation:
|
||||||
|
return StepRequestValidation(self.__step)
|
||||||
|
|
||||||
|
def struct(self) -> TStep:
|
||||||
|
return self.__step
|
||||||
|
|
||||||
|
def name(self) -> Text:
|
||||||
|
return self.__step.name
|
||||||
|
|
||||||
|
def type(self) -> Text:
|
||||||
|
return f"request-{self.__step.request.method}"
|
||||||
|
|
||||||
|
def run(self, runner: HttpRunner):
|
||||||
|
return run_step_request(runner, self.__step)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestWithOptionalArgs(IStep):
|
||||||
|
def __init__(self, step: TStep):
|
||||||
|
self.__step = step
|
||||||
|
|
||||||
|
def with_params(self, **params) -> "RequestWithOptionalArgs":
|
||||||
|
self.__step.request.params.update(params)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def with_headers(self, **headers) -> "RequestWithOptionalArgs":
|
||||||
|
self.__step.request.headers.update(headers)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def with_cookies(self, **cookies) -> "RequestWithOptionalArgs":
|
||||||
|
self.__step.request.cookies.update(cookies)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def with_data(self, data) -> "RequestWithOptionalArgs":
|
||||||
|
self.__step.request.data = data
|
||||||
|
return self
|
||||||
|
|
||||||
|
def with_json(self, req_json) -> "RequestWithOptionalArgs":
|
||||||
|
self.__step.request.req_json = req_json
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs":
|
||||||
|
self.__step.request.timeout = timeout
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_verify(self, verify: bool) -> "RequestWithOptionalArgs":
|
||||||
|
self.__step.request.verify = verify
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_allow_redirects(self, allow_redirects: bool) -> "RequestWithOptionalArgs":
|
||||||
|
self.__step.request.allow_redirects = allow_redirects
|
||||||
|
return self
|
||||||
|
|
||||||
|
def upload(self, **file_info) -> "RequestWithOptionalArgs":
|
||||||
|
self.__step.request.upload.update(file_info)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def teardown_hook(
|
||||||
|
self, hook: Text, assign_var_name: Text = None
|
||||||
|
) -> "RequestWithOptionalArgs":
|
||||||
|
if assign_var_name:
|
||||||
|
self.__step.teardown_hooks.append({assign_var_name: hook})
|
||||||
|
else:
|
||||||
|
self.__step.teardown_hooks.append(hook)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def extract(self) -> StepRequestExtraction:
|
||||||
|
return StepRequestExtraction(self.__step)
|
||||||
|
|
||||||
|
def validate(self) -> StepRequestValidation:
|
||||||
|
return StepRequestValidation(self.__step)
|
||||||
|
|
||||||
|
def struct(self) -> TStep:
|
||||||
|
return self.__step
|
||||||
|
|
||||||
|
def name(self) -> Text:
|
||||||
|
return self.__step.name
|
||||||
|
|
||||||
|
def type(self) -> Text:
|
||||||
|
return f"request-{self.__step.request.method}"
|
||||||
|
|
||||||
|
def run(self, runner: HttpRunner):
|
||||||
|
return run_step_request(runner, self.__step)
|
||||||
|
|
||||||
|
|
||||||
|
class RunRequest(object):
|
||||||
|
def __init__(self, name: Text):
|
||||||
|
self.__step = TStep(name=name)
|
||||||
|
|
||||||
|
def with_variables(self, **variables) -> "RunRequest":
|
||||||
|
self.__step.variables.update(variables)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunRequest":
|
||||||
|
if assign_var_name:
|
||||||
|
self.__step.setup_hooks.append({assign_var_name: hook})
|
||||||
|
else:
|
||||||
|
self.__step.setup_hooks.append(hook)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
|
self.__step.request = TRequest(method=MethodEnum.GET, url=url)
|
||||||
|
return RequestWithOptionalArgs(self.__step)
|
||||||
|
|
||||||
|
def post(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
|
self.__step.request = TRequest(method=MethodEnum.POST, url=url)
|
||||||
|
return RequestWithOptionalArgs(self.__step)
|
||||||
|
|
||||||
|
def put(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
|
self.__step.request = TRequest(method=MethodEnum.PUT, url=url)
|
||||||
|
return RequestWithOptionalArgs(self.__step)
|
||||||
|
|
||||||
|
def head(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
|
self.__step.request = TRequest(method=MethodEnum.HEAD, url=url)
|
||||||
|
return RequestWithOptionalArgs(self.__step)
|
||||||
|
|
||||||
|
def delete(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
|
self.__step.request = TRequest(method=MethodEnum.DELETE, url=url)
|
||||||
|
return RequestWithOptionalArgs(self.__step)
|
||||||
|
|
||||||
|
def options(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
|
self.__step.request = TRequest(method=MethodEnum.OPTIONS, url=url)
|
||||||
|
return RequestWithOptionalArgs(self.__step)
|
||||||
|
|
||||||
|
def patch(self, url: Text) -> RequestWithOptionalArgs:
|
||||||
|
self.__step.request = TRequest(method=MethodEnum.PATCH, url=url)
|
||||||
|
return RequestWithOptionalArgs(self.__step)
|
||||||
110
httprunner/step_testcase.py
Normal file
110
httprunner/step_testcase.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import os
|
||||||
|
from typing import Text, Callable
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from httprunner import exceptions
|
||||||
|
from httprunner.loader import load_testcase_file
|
||||||
|
|
||||||
|
from httprunner.step_request import call_hooks
|
||||||
|
from httprunner.runner import HttpRunner
|
||||||
|
from httprunner.models import (
|
||||||
|
TStep,
|
||||||
|
StepData
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_step_testcase(runner: HttpRunner, step: TStep) -> StepData:
|
||||||
|
"""run teststep: referenced testcase"""
|
||||||
|
step_data = StepData(name=step.name)
|
||||||
|
step_variables = step.variables
|
||||||
|
step_export = step.export
|
||||||
|
|
||||||
|
# setup hooks
|
||||||
|
if step.setup_hooks:
|
||||||
|
call_hooks(runner, step.setup_hooks, step_variables, "setup testcase")
|
||||||
|
|
||||||
|
# TODO: override testcase with current step name/variables/export
|
||||||
|
|
||||||
|
ref_case_runner = HttpRunner()
|
||||||
|
ref_case_runner.config = step.testcase.config
|
||||||
|
ref_case_runner.teststeps = step.testcase.teststeps
|
||||||
|
ref_case_runner.with_session(runner.session) \
|
||||||
|
.with_case_id(runner.case_id) \
|
||||||
|
.with_variables(step_variables) \
|
||||||
|
.with_export(step_export) \
|
||||||
|
.test_start()
|
||||||
|
|
||||||
|
# teardown hooks
|
||||||
|
if step.teardown_hooks:
|
||||||
|
call_hooks(runner, step.teardown_hooks, step.variables, "teardown testcase")
|
||||||
|
|
||||||
|
summary = ref_case_runner.get_summary()
|
||||||
|
step_data.data = summary.step_datas # list of step data
|
||||||
|
step_data.export_vars = summary.in_out.export_vars
|
||||||
|
step_data.success = summary.success
|
||||||
|
|
||||||
|
if step_data.export_vars:
|
||||||
|
logger.info(f"export variables: {step_data.export_vars}")
|
||||||
|
|
||||||
|
return step_data
|
||||||
|
|
||||||
|
|
||||||
|
class StepRefCase(object):
|
||||||
|
def __init__(self, step: TStep):
|
||||||
|
self.__step = step
|
||||||
|
|
||||||
|
def teardown_hook(self, hook: Text, assign_var_name: Text = None) -> "StepRefCase":
|
||||||
|
if assign_var_name:
|
||||||
|
self.__step.teardown_hooks.append({assign_var_name: hook})
|
||||||
|
else:
|
||||||
|
self.__step.teardown_hooks.append(hook)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def export(self, *var_name: Text) -> "StepRefCase":
|
||||||
|
self.__step.export.extend(var_name)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def struct(self) -> TStep:
|
||||||
|
return self.__step
|
||||||
|
|
||||||
|
def name(self) -> Text:
|
||||||
|
return self.__step.name
|
||||||
|
|
||||||
|
def type(self) -> Text:
|
||||||
|
return f"request-{self.__step.request.method}"
|
||||||
|
|
||||||
|
def run(self, runner: HttpRunner):
|
||||||
|
return run_step_testcase(runner, self.__step)
|
||||||
|
|
||||||
|
|
||||||
|
class RunTestCase(object):
|
||||||
|
def __init__(self, name: Text):
|
||||||
|
self.__step = TStep(name=name)
|
||||||
|
|
||||||
|
def with_variables(self, **variables) -> "RunTestCase":
|
||||||
|
self.__step.variables.update(variables)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunTestCase":
|
||||||
|
if assign_var_name:
|
||||||
|
self.__step.setup_hooks.append({assign_var_name: hook})
|
||||||
|
else:
|
||||||
|
self.__step.setup_hooks.append(hook)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def call(self, testcase: Callable) -> StepRefCase:
|
||||||
|
if hasattr(testcase, "config") and hasattr(testcase, "teststeps"):
|
||||||
|
self.__step.testcase = testcase
|
||||||
|
elif isinstance(testcase, Text):
|
||||||
|
if not os.path.isfile(testcase):
|
||||||
|
raise exceptions.ParamsError(f"Invalid testcase path: {testcase}")
|
||||||
|
|
||||||
|
self.__step.testcase = load_testcase_file(testcase)
|
||||||
|
else:
|
||||||
|
raise exceptions.ParamsError(
|
||||||
|
f"Invalid teststep referenced testcase: {testcase}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return StepRefCase(self.__step)
|
||||||
@@ -1,415 +0,0 @@
|
|||||||
import inspect
|
|
||||||
from typing import Text, Any, Union, Callable
|
|
||||||
|
|
||||||
from httprunner.models import (
|
|
||||||
TConfig,
|
|
||||||
TStep,
|
|
||||||
TRequest,
|
|
||||||
MethodEnum,
|
|
||||||
TestCase,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Config(object):
|
|
||||||
def __init__(self, name: Text):
|
|
||||||
self.__name = name
|
|
||||||
self.__variables = {}
|
|
||||||
self.__base_url = ""
|
|
||||||
self.__verify = False
|
|
||||||
self.__export = []
|
|
||||||
self.__weight = 1
|
|
||||||
|
|
||||||
caller_frame = inspect.stack()[1]
|
|
||||||
self.__path = caller_frame.filename
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> Text:
|
|
||||||
return self.__name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self) -> Text:
|
|
||||||
return self.__path
|
|
||||||
|
|
||||||
@property
|
|
||||||
def weight(self) -> int:
|
|
||||||
return self.__weight
|
|
||||||
|
|
||||||
def variables(self, **variables) -> "Config":
|
|
||||||
self.__variables.update(variables)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def base_url(self, base_url: Text) -> "Config":
|
|
||||||
self.__base_url = base_url
|
|
||||||
return self
|
|
||||||
|
|
||||||
def verify(self, verify: bool) -> "Config":
|
|
||||||
self.__verify = verify
|
|
||||||
return self
|
|
||||||
|
|
||||||
def export(self, *export_var_name: Text) -> "Config":
|
|
||||||
self.__export.extend(export_var_name)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def locust_weight(self, weight: int) -> "Config":
|
|
||||||
self.__weight = weight
|
|
||||||
return self
|
|
||||||
|
|
||||||
def perform(self) -> TConfig:
|
|
||||||
return TConfig(
|
|
||||||
name=self.__name,
|
|
||||||
base_url=self.__base_url,
|
|
||||||
verify=self.__verify,
|
|
||||||
variables=self.__variables,
|
|
||||||
export=list(set(self.__export)),
|
|
||||||
path=self.__path,
|
|
||||||
weight=self.__weight,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StepRequestValidation(object):
|
|
||||||
def __init__(self, step_context: TStep):
|
|
||||||
self.__step_context = step_context
|
|
||||||
|
|
||||||
def assert_equal(
|
|
||||||
self, jmes_path: Text, expected_value: Any, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"equal": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_not_equal(
|
|
||||||
self, jmes_path: Text, expected_value: Any, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"not_equal": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_greater_than(
|
|
||||||
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"greater_than": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_less_than(
|
|
||||||
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"less_than": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_greater_or_equals(
|
|
||||||
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"greater_or_equals": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_less_or_equals(
|
|
||||||
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"less_or_equals": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_length_equal(
|
|
||||||
self, jmes_path: Text, expected_value: int, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"length_equal": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_length_greater_than(
|
|
||||||
self, jmes_path: Text, expected_value: int, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"length_greater_than": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_length_less_than(
|
|
||||||
self, jmes_path: Text, expected_value: int, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"length_less_than": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_length_greater_or_equals(
|
|
||||||
self, jmes_path: Text, expected_value: int, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"length_greater_or_equals": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_length_less_or_equals(
|
|
||||||
self, jmes_path: Text, expected_value: int, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"length_less_or_equals": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_string_equals(
|
|
||||||
self, jmes_path: Text, expected_value: Any, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"string_equals": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_startswith(
|
|
||||||
self, jmes_path: Text, expected_value: Text, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"startswith": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_endswith(
|
|
||||||
self, jmes_path: Text, expected_value: Text, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"endswith": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_regex_match(
|
|
||||||
self, jmes_path: Text, expected_value: Text, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"regex_match": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_contains(
|
|
||||||
self, jmes_path: Text, expected_value: Any, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"contains": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_contained_by(
|
|
||||||
self, jmes_path: Text, expected_value: Any, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"contained_by": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def assert_type_match(
|
|
||||||
self, jmes_path: Text, expected_value: Any, message: Text = ""
|
|
||||||
) -> "StepRequestValidation":
|
|
||||||
self.__step_context.validators.append(
|
|
||||||
{"type_match": [jmes_path, expected_value, message]}
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def perform(self) -> TStep:
|
|
||||||
return self.__step_context
|
|
||||||
|
|
||||||
|
|
||||||
class StepRequestExtraction(object):
|
|
||||||
def __init__(self, step_context: TStep):
|
|
||||||
self.__step_context = step_context
|
|
||||||
|
|
||||||
def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepRequestExtraction":
|
|
||||||
self.__step_context.extract[var_name] = jmes_path
|
|
||||||
return self
|
|
||||||
|
|
||||||
# def with_regex(self):
|
|
||||||
# # TODO: extract response html with regex
|
|
||||||
# pass
|
|
||||||
#
|
|
||||||
# def with_jsonpath(self):
|
|
||||||
# # TODO: extract response json with jsonpath
|
|
||||||
# pass
|
|
||||||
|
|
||||||
def validate(self) -> StepRequestValidation:
|
|
||||||
return StepRequestValidation(self.__step_context)
|
|
||||||
|
|
||||||
def perform(self) -> TStep:
|
|
||||||
return self.__step_context
|
|
||||||
|
|
||||||
|
|
||||||
class RequestWithOptionalArgs(object):
|
|
||||||
def __init__(self, step_context: TStep):
|
|
||||||
self.__step_context = step_context
|
|
||||||
|
|
||||||
def with_params(self, **params) -> "RequestWithOptionalArgs":
|
|
||||||
self.__step_context.request.params.update(params)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def with_headers(self, **headers) -> "RequestWithOptionalArgs":
|
|
||||||
self.__step_context.request.headers.update(headers)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def with_cookies(self, **cookies) -> "RequestWithOptionalArgs":
|
|
||||||
self.__step_context.request.cookies.update(cookies)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def with_data(self, data) -> "RequestWithOptionalArgs":
|
|
||||||
self.__step_context.request.data = data
|
|
||||||
return self
|
|
||||||
|
|
||||||
def with_json(self, req_json) -> "RequestWithOptionalArgs":
|
|
||||||
self.__step_context.request.req_json = req_json
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs":
|
|
||||||
self.__step_context.request.timeout = timeout
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_verify(self, verify: bool) -> "RequestWithOptionalArgs":
|
|
||||||
self.__step_context.request.verify = verify
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_allow_redirects(self, allow_redirects: bool) -> "RequestWithOptionalArgs":
|
|
||||||
self.__step_context.request.allow_redirects = allow_redirects
|
|
||||||
return self
|
|
||||||
|
|
||||||
def upload(self, **file_info) -> "RequestWithOptionalArgs":
|
|
||||||
self.__step_context.request.upload.update(file_info)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def teardown_hook(
|
|
||||||
self, hook: Text, assign_var_name: Text = None
|
|
||||||
) -> "RequestWithOptionalArgs":
|
|
||||||
if assign_var_name:
|
|
||||||
self.__step_context.teardown_hooks.append({assign_var_name: hook})
|
|
||||||
else:
|
|
||||||
self.__step_context.teardown_hooks.append(hook)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def extract(self) -> StepRequestExtraction:
|
|
||||||
return StepRequestExtraction(self.__step_context)
|
|
||||||
|
|
||||||
def validate(self) -> StepRequestValidation:
|
|
||||||
return StepRequestValidation(self.__step_context)
|
|
||||||
|
|
||||||
def perform(self) -> TStep:
|
|
||||||
return self.__step_context
|
|
||||||
|
|
||||||
|
|
||||||
class RunRequest(object):
|
|
||||||
def __init__(self, name: Text):
|
|
||||||
self.__step_context = TStep(name=name)
|
|
||||||
|
|
||||||
def with_variables(self, **variables) -> "RunRequest":
|
|
||||||
self.__step_context.variables.update(variables)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunRequest":
|
|
||||||
if assign_var_name:
|
|
||||||
self.__step_context.setup_hooks.append({assign_var_name: hook})
|
|
||||||
else:
|
|
||||||
self.__step_context.setup_hooks.append(hook)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def get(self, url: Text) -> RequestWithOptionalArgs:
|
|
||||||
self.__step_context.request = TRequest(method=MethodEnum.GET, url=url)
|
|
||||||
return RequestWithOptionalArgs(self.__step_context)
|
|
||||||
|
|
||||||
def post(self, url: Text) -> RequestWithOptionalArgs:
|
|
||||||
self.__step_context.request = TRequest(method=MethodEnum.POST, url=url)
|
|
||||||
return RequestWithOptionalArgs(self.__step_context)
|
|
||||||
|
|
||||||
def put(self, url: Text) -> RequestWithOptionalArgs:
|
|
||||||
self.__step_context.request = TRequest(method=MethodEnum.PUT, url=url)
|
|
||||||
return RequestWithOptionalArgs(self.__step_context)
|
|
||||||
|
|
||||||
def head(self, url: Text) -> RequestWithOptionalArgs:
|
|
||||||
self.__step_context.request = TRequest(method=MethodEnum.HEAD, url=url)
|
|
||||||
return RequestWithOptionalArgs(self.__step_context)
|
|
||||||
|
|
||||||
def delete(self, url: Text) -> RequestWithOptionalArgs:
|
|
||||||
self.__step_context.request = TRequest(method=MethodEnum.DELETE, url=url)
|
|
||||||
return RequestWithOptionalArgs(self.__step_context)
|
|
||||||
|
|
||||||
def options(self, url: Text) -> RequestWithOptionalArgs:
|
|
||||||
self.__step_context.request = TRequest(method=MethodEnum.OPTIONS, url=url)
|
|
||||||
return RequestWithOptionalArgs(self.__step_context)
|
|
||||||
|
|
||||||
def patch(self, url: Text) -> RequestWithOptionalArgs:
|
|
||||||
self.__step_context.request = TRequest(method=MethodEnum.PATCH, url=url)
|
|
||||||
return RequestWithOptionalArgs(self.__step_context)
|
|
||||||
|
|
||||||
|
|
||||||
class StepRefCase(object):
|
|
||||||
def __init__(self, step_context: TStep):
|
|
||||||
self.__step_context = step_context
|
|
||||||
|
|
||||||
def teardown_hook(self, hook: Text, assign_var_name: Text = None) -> "StepRefCase":
|
|
||||||
if assign_var_name:
|
|
||||||
self.__step_context.teardown_hooks.append({assign_var_name: hook})
|
|
||||||
else:
|
|
||||||
self.__step_context.teardown_hooks.append(hook)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def export(self, *var_name: Text) -> "StepRefCase":
|
|
||||||
self.__step_context.export.extend(var_name)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def perform(self) -> TStep:
|
|
||||||
return self.__step_context
|
|
||||||
|
|
||||||
|
|
||||||
class RunTestCase(object):
|
|
||||||
def __init__(self, name: Text):
|
|
||||||
self.__step_context = TStep(name=name)
|
|
||||||
|
|
||||||
def with_variables(self, **variables) -> "RunTestCase":
|
|
||||||
self.__step_context.variables.update(variables)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunTestCase":
|
|
||||||
if assign_var_name:
|
|
||||||
self.__step_context.setup_hooks.append({assign_var_name: hook})
|
|
||||||
else:
|
|
||||||
self.__step_context.setup_hooks.append(hook)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def call(self, testcase: Callable) -> StepRefCase:
|
|
||||||
self.__step_context.testcase = testcase
|
|
||||||
return StepRefCase(self.__step_context)
|
|
||||||
|
|
||||||
def perform(self) -> TStep:
|
|
||||||
return self.__step_context
|
|
||||||
|
|
||||||
|
|
||||||
class Step(object):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
step_context: Union[
|
|
||||||
StepRequestValidation,
|
|
||||||
StepRequestExtraction,
|
|
||||||
RequestWithOptionalArgs,
|
|
||||||
RunTestCase,
|
|
||||||
StepRefCase,
|
|
||||||
],
|
|
||||||
):
|
|
||||||
self.__step_context = step_context.perform()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def request(self) -> TRequest:
|
|
||||||
return self.__step_context.request
|
|
||||||
|
|
||||||
@property
|
|
||||||
def testcase(self) -> TestCase:
|
|
||||||
return self.__step_context.testcase
|
|
||||||
|
|
||||||
def perform(self) -> TStep:
|
|
||||||
return self.__step_context
|
|
||||||
Reference in New Issue
Block a user