mirror of
https://github.com/httprunner/httprunner.git
synced 2026-07-07 07:21:23 +08:00
init v3: make http request without validation
This commit is contained in:
0
examples/__init__.py
Normal file
0
examples/__init__.py
Normal file
0
examples/postman_echo/__init__.py
Normal file
0
examples/postman_echo/__init__.py
Normal file
0
examples/postman_echo/request_methods/__init__.py
Normal file
0
examples/postman_echo/request_methods/__init__.py
Normal file
79
examples/postman_echo/request_methods/hardcode.py
Normal file
79
examples/postman_echo/request_methods/hardcode.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
from httprunner.v3.runner import TestCaseRunner
|
||||||
|
from httprunner.v3.schema import TestsConfig, TestStep
|
||||||
|
|
||||||
|
|
||||||
|
class TestCaseRequestMethodsHardcode(TestCaseRunner):
|
||||||
|
config = TestsConfig(**{
|
||||||
|
"name": "request methods testcase in hardcode",
|
||||||
|
"base_url": "https://postman-echo.com",
|
||||||
|
"verify": False
|
||||||
|
})
|
||||||
|
|
||||||
|
teststeps = [
|
||||||
|
TestStep(**{
|
||||||
|
"name": "get with params",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "/get",
|
||||||
|
"params": {
|
||||||
|
"foo1": "bar1",
|
||||||
|
"foo2": "bar2"
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"User-Agent": "HttpRunner/3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{"eq": ["status_code", 200]}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
TestStep(**{
|
||||||
|
"name": "post raw text",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "/post",
|
||||||
|
"data": "This is expected to be sent back as part of response body.",
|
||||||
|
"headers": {
|
||||||
|
"User-Agent": "HttpRunner/3.0",
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{"eq": ["status_code", 200]}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
TestStep(**{
|
||||||
|
"name": "post form data",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "/post",
|
||||||
|
"data": "foo1=bar1&foo2=bar2",
|
||||||
|
"headers": {
|
||||||
|
"User-Agent": "HttpRunner/3.0",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{"eq": ["status_code", 200]}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
TestStep(**{
|
||||||
|
"name": "put request",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"url": "/put",
|
||||||
|
"data": "This is expected to be sent back as part of response body.",
|
||||||
|
"headers": {
|
||||||
|
"User-Agent": "HttpRunner/3.0",
|
||||||
|
"Content-Type": "text/plain"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{"eq": ["status_code", 200]}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
TestCaseRequestMethodsHardcode().run()
|
||||||
0
httprunner/v3/__init__.py
Normal file
0
httprunner/v3/__init__.py
Normal file
81
httprunner/v3/exceptions/__init__.py
Normal file
81
httprunner/v3/exceptions/__init__.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
""" failure type exceptions
|
||||||
|
these exceptions will mark test as failure
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MyBaseFailure(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ParseTestsFailure(MyBaseFailure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationFailure(MyBaseFailure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractFailure(MyBaseFailure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SetupHooksFailure(MyBaseFailure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TeardownHooksFailure(MyBaseFailure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
""" error type exceptions
|
||||||
|
these exceptions will mark test as error
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MyBaseError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FileFormatError(MyBaseError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ParamsError(MyBaseError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundError(MyBaseError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FileNotFound(FileNotFoundError, NotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionNotFound(NotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class VariableNotFound(NotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EnvNotFound(NotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CSVNotFound(NotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ApiNotFound(NotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestcaseNotFound(NotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SummaryEmpty(MyBaseError):
|
||||||
|
""" test result summary data is empty
|
||||||
|
"""
|
||||||
178
httprunner/v3/parser/__init__.py
Normal file
178
httprunner/v3/parser/__init__.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import re
|
||||||
|
from typing import Any, Set
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from httprunner.v3.exceptions import ParamsError
|
||||||
|
|
||||||
|
absolute_http_url_regexp = re.compile(r"^https?://", re.I)
|
||||||
|
|
||||||
|
# use $$ to escape $ notation
|
||||||
|
dolloar_regex_compile = re.compile(r"\$\$")
|
||||||
|
# variable notation, e.g. ${var} or $var
|
||||||
|
variable_regex_compile = re.compile(r"\$\{(\w+)\}|\$(\w+)")
|
||||||
|
# function notation, e.g. ${func1($var_1, $var_3)}
|
||||||
|
function_regex_compile = re.compile(r"\$\{(\w+)\(([\$\w\.\-/\s=,]*)\)\}")
|
||||||
|
|
||||||
|
|
||||||
|
def build_url(base_url, path):
|
||||||
|
""" prepend url with base_url unless it's already an absolute URL """
|
||||||
|
if absolute_http_url_regexp.match(path):
|
||||||
|
return path
|
||||||
|
elif base_url:
|
||||||
|
return "{}/{}".format(base_url.rstrip("/"), path.lstrip("/"))
|
||||||
|
else:
|
||||||
|
raise ParamsError("base url missed!")
|
||||||
|
|
||||||
|
|
||||||
|
def regex_findall_variables(content):
|
||||||
|
""" extract all variable names from content, which is in format $variable
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content (str): string content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: variables list extracted from string content
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> regex_findall_variables("$variable")
|
||||||
|
["variable"]
|
||||||
|
|
||||||
|
>>> regex_findall_variables("/blog/$postid")
|
||||||
|
["postid"]
|
||||||
|
|
||||||
|
>>> regex_findall_variables("/$var1/$var2")
|
||||||
|
["var1", "var2"]
|
||||||
|
|
||||||
|
>>> regex_findall_variables("abc")
|
||||||
|
[]
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
vars_list = []
|
||||||
|
for var_tuple in variable_regex_compile.findall(content):
|
||||||
|
vars_list.append(
|
||||||
|
var_tuple[0] or var_tuple[1]
|
||||||
|
)
|
||||||
|
return vars_list
|
||||||
|
except TypeError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def extract_variables(content: Any) -> Set:
|
||||||
|
""" extract all variables in content recursively.
|
||||||
|
"""
|
||||||
|
if isinstance(content, (list, set, tuple)):
|
||||||
|
variables = set()
|
||||||
|
for item in content:
|
||||||
|
variables = variables | extract_variables(item)
|
||||||
|
return variables
|
||||||
|
|
||||||
|
elif isinstance(content, dict):
|
||||||
|
variables = set()
|
||||||
|
for key, value in content.items():
|
||||||
|
variables = variables | extract_variables(value)
|
||||||
|
return variables
|
||||||
|
|
||||||
|
elif isinstance(content, str):
|
||||||
|
return set(regex_findall_variables(content))
|
||||||
|
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_string_variables(content, variables_mapping):
|
||||||
|
""" parse string content with variables mapping.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content (str): string content to be parsed.
|
||||||
|
variables_mapping (dict): variables mapping.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: parsed string content.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> content = "/api/users/$uid"
|
||||||
|
>>> variables_mapping = {"$uid": 1000}
|
||||||
|
>>> parse_string_variables(content, variables_mapping)
|
||||||
|
"/api/users/1000"
|
||||||
|
|
||||||
|
"""
|
||||||
|
variables_list = extract_variables(content)
|
||||||
|
for variable_name in variables_list:
|
||||||
|
variable_value = variables_mapping[variable_name]
|
||||||
|
|
||||||
|
# TODO: replace variable label from $var to {{var}}
|
||||||
|
if "${}".format(variable_name) == content:
|
||||||
|
# content is a variable
|
||||||
|
content = variable_value
|
||||||
|
else:
|
||||||
|
# content contains one or several variables
|
||||||
|
if not isinstance(variable_value, str):
|
||||||
|
variable_value = str(variable_value)
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
"${}".format(variable_name),
|
||||||
|
variable_value, 1
|
||||||
|
)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def parse_content(content: Any, variables_mapping: Dict[str, Any] = None, functions_mapping=None):
|
||||||
|
""" parse content with evaluated variables mapping.
|
||||||
|
Notice: variables_mapping should not contain any variable or function.
|
||||||
|
"""
|
||||||
|
# TODO: refactor type check
|
||||||
|
if content is None or isinstance(content, (int, float, bool)):
|
||||||
|
return content
|
||||||
|
|
||||||
|
elif isinstance(content, str):
|
||||||
|
# content is in string format here
|
||||||
|
variables_mapping = variables_mapping or {}
|
||||||
|
functions_mapping = functions_mapping or {}
|
||||||
|
content = content.strip()
|
||||||
|
|
||||||
|
# replace functions with evaluated value
|
||||||
|
# Notice: _eval_content_functions must be called before _eval_content_variables
|
||||||
|
# content = parse_string_functions(content, variables_mapping, functions_mapping)
|
||||||
|
|
||||||
|
# replace variables with binding value
|
||||||
|
content = parse_string_variables(content, variables_mapping)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
elif isinstance(content, (list, set, tuple)):
|
||||||
|
return [
|
||||||
|
parse_content(item, variables_mapping)
|
||||||
|
for item in content
|
||||||
|
]
|
||||||
|
|
||||||
|
elif isinstance(content, dict):
|
||||||
|
parsed_content = {}
|
||||||
|
for key, value in content.items():
|
||||||
|
parsed_key = parse_content(key, variables_mapping)
|
||||||
|
parsed_value = parse_content(value, variables_mapping)
|
||||||
|
parsed_content[parsed_key] = parsed_value
|
||||||
|
|
||||||
|
return parsed_content
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def parse_variables_mapping(variables_mapping: Dict[str, Any]):
|
||||||
|
|
||||||
|
parsed_variables: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
while len(parsed_variables) != len(variables_mapping):
|
||||||
|
for var_name in variables_mapping:
|
||||||
|
|
||||||
|
var_value = variables_mapping[var_name]
|
||||||
|
# variables = extract_variables(var_value)
|
||||||
|
|
||||||
|
if var_name in parsed_variables:
|
||||||
|
continue
|
||||||
|
|
||||||
|
parsed_value = parse_content(var_value, parsed_variables)
|
||||||
|
parsed_variables[var_name] = parsed_value
|
||||||
|
|
||||||
|
return parsed_variables
|
||||||
38
httprunner/v3/runner/__init__.py
Normal file
38
httprunner/v3/runner/__init__.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from httprunner.v3.parser import build_url
|
||||||
|
from httprunner.v3.schema import TestsConfig, TestStep
|
||||||
|
|
||||||
|
|
||||||
|
class TestCaseRunner(object):
|
||||||
|
|
||||||
|
config: TestsConfig = {}
|
||||||
|
teststeps: List[TestStep] = []
|
||||||
|
session: requests.Session = None
|
||||||
|
|
||||||
|
def with_session(self, s: requests.Session) -> "TestCaseRunner":
|
||||||
|
self.session = s
|
||||||
|
return self
|
||||||
|
|
||||||
|
def with_variables(self, **variables) -> "TestCaseRunner":
|
||||||
|
self.config.variables.update(variables)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def run_step(self, step):
|
||||||
|
request_dict = step.request.dict()
|
||||||
|
|
||||||
|
method = request_dict.pop("method")
|
||||||
|
url_path = request_dict.pop("url")
|
||||||
|
url = build_url(self.config.base_url, url_path)
|
||||||
|
|
||||||
|
request_dict["json"] = request_dict.pop("req_json", {})
|
||||||
|
|
||||||
|
session = self.session or requests.Session()
|
||||||
|
resp = session.request(method, url, **request_dict)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for step in self.teststeps:
|
||||||
|
step.variables.update(self.config.variables)
|
||||||
|
self.run_step(step)
|
||||||
60
httprunner/v3/schema/__init__.py
Normal file
60
httprunner/v3/schema/__init__.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
from typing import Dict, List, Text, Union
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from pydantic import HttpUrl
|
||||||
|
|
||||||
|
Name = Text
|
||||||
|
Url = Text
|
||||||
|
BaseUrl = Union[HttpUrl, Text]
|
||||||
|
Variables = Dict[Text, Any]
|
||||||
|
Headers = Dict[Text, Text]
|
||||||
|
Verify = bool
|
||||||
|
Hook = List[Text]
|
||||||
|
Export = List[Text]
|
||||||
|
Validate = List[Dict]
|
||||||
|
Env = Dict[Text, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class MethodEnum(Text, Enum):
|
||||||
|
GET = 'GET'
|
||||||
|
POST = 'POST'
|
||||||
|
PUT = "PUT"
|
||||||
|
DELETE = "DELETE"
|
||||||
|
HEAD = "HEAD"
|
||||||
|
OPTIONS = "OPTIONS"
|
||||||
|
PATCH = "PATCH"
|
||||||
|
CONNECT = "CONNECT"
|
||||||
|
TRACE = "TRACE"
|
||||||
|
|
||||||
|
|
||||||
|
class TestsConfig(BaseModel):
|
||||||
|
name: Name
|
||||||
|
verify: Verify = False
|
||||||
|
base_url: BaseUrl = ""
|
||||||
|
variables: Variables = {}
|
||||||
|
setup_hooks: Hook = []
|
||||||
|
teardown_hooks: Hook = []
|
||||||
|
export: Export = []
|
||||||
|
|
||||||
|
|
||||||
|
class Request(BaseModel):
|
||||||
|
method: MethodEnum = MethodEnum.GET
|
||||||
|
url: Url
|
||||||
|
params: Dict[Text, Text] = {}
|
||||||
|
headers: Headers = {}
|
||||||
|
req_json: Dict = Field({}, alias="json")
|
||||||
|
data: Union[Text, Dict[Text, Any]] = ""
|
||||||
|
cookies: Dict[Text, Text] = {}
|
||||||
|
timeout: int = 120
|
||||||
|
allow_redirects: bool = True
|
||||||
|
verify: Verify = False
|
||||||
|
|
||||||
|
|
||||||
|
class TestStep(BaseModel):
|
||||||
|
name: Name
|
||||||
|
request: Request
|
||||||
|
variables: Variables = {}
|
||||||
|
extract: Union[Dict[Text, Text], List[Text]] = {}
|
||||||
|
validation: Validate = Field([], alias="validate")
|
||||||
Reference in New Issue
Block a user