mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-11 18:11:21 +08:00
Merge pull request #902 from httprunner/v3
## 3.0.4 (2020-05-19) **Added** - feat: make testsuite and run testsuite - feat: testcase/testsuite config support getting variables by function - feat: har2case with request cookies - feat: log request/response headers and body with indent **Fixed** - fix: extract response cookies - fix: handle errors when no valid testcases generated **Changed** - change: har2case do not ignore request headers, except for header startswith :
This commit is contained in:
@@ -1,5 +1,23 @@
|
||||
# Release History
|
||||
|
||||
## 3.0.4 (2020-05-19)
|
||||
|
||||
**Added**
|
||||
|
||||
- feat: make testsuite and run testsuite
|
||||
- feat: testcase/testsuite config support getting variables by function
|
||||
- feat: har2case with request cookies
|
||||
- feat: log request/response headers and body with indent
|
||||
|
||||
**Fixed**
|
||||
|
||||
- fix: extract response cookies
|
||||
- fix: handle errors when no valid testcases generated
|
||||
|
||||
**Changed**
|
||||
|
||||
- change: har2case do not ignore request headers, except for header startswith :
|
||||
|
||||
## 3.0.3 (2020-05-17)
|
||||
|
||||
**Fixed**
|
||||
|
||||
@@ -19,7 +19,7 @@ teststeps:
|
||||
method: GET
|
||||
validate:
|
||||
- eq: ["status_code", 200]
|
||||
# - startswith: [body.user-agent, "python-requests"]
|
||||
- startswith: [body."user-agent", "python-requests"]
|
||||
|
||||
-
|
||||
name: get without params
|
||||
@@ -58,7 +58,7 @@ teststeps:
|
||||
method: GET
|
||||
validate:
|
||||
- eq: ["status_code", 200]
|
||||
# - eq: [cookies.name, "value"]
|
||||
- eq: [body.cookies.name, "value"]
|
||||
|
||||
-
|
||||
name: extract cookie
|
||||
@@ -67,7 +67,7 @@ teststeps:
|
||||
method: GET
|
||||
validate:
|
||||
- eq: ["status_code", 200]
|
||||
# - eq: [cookies.name, "value"]
|
||||
- eq: [body.cookies.name, "value"]
|
||||
|
||||
-
|
||||
name: post data
|
||||
|
||||
@@ -27,7 +27,10 @@ class TestCaseBasic(HttpRunner):
|
||||
**{
|
||||
"name": "user-agent",
|
||||
"request": {"url": "/user-agent", "method": "GET"},
|
||||
"validate": [{"eq": ["status_code", 200]}],
|
||||
"validate": [
|
||||
{"eq": ["status_code", 200]},
|
||||
{"startswith": ['body."user-agent"', "python-requests"]},
|
||||
],
|
||||
}
|
||||
),
|
||||
TStep(
|
||||
@@ -61,14 +64,20 @@ class TestCaseBasic(HttpRunner):
|
||||
**{
|
||||
"name": "set cookie",
|
||||
"request": {"url": "/cookies/set?name=value", "method": "GET"},
|
||||
"validate": [{"eq": ["status_code", 200]}],
|
||||
"validate": [
|
||||
{"eq": ["status_code", 200]},
|
||||
{"eq": ["body.cookies.name", "value"]},
|
||||
],
|
||||
}
|
||||
),
|
||||
TStep(
|
||||
**{
|
||||
"name": "extract cookie",
|
||||
"request": {"url": "/cookies", "method": "GET"},
|
||||
"validate": [{"eq": ["status_code", 200]}],
|
||||
"validate": [
|
||||
{"eq": ["status_code", 200]},
|
||||
{"eq": ["body.cookies.name", "value"]},
|
||||
],
|
||||
}
|
||||
),
|
||||
TStep(
|
||||
|
||||
@@ -32,5 +32,6 @@ teststeps:
|
||||
- ${alter_response($response)}
|
||||
validate:
|
||||
- eq: ["status_code", 200]
|
||||
# - eq: ["headers.content-type", "html/text"]
|
||||
# TODO: implement hooks
|
||||
# - eq: [body.headers."Content-Type", "html/text"]
|
||||
- eq: [body.headers.Host, "httpbin.org"]
|
||||
|
||||
@@ -7,3 +7,9 @@ def get_httprunner_version():
|
||||
|
||||
def sum_two(m, n):
|
||||
return m + n
|
||||
|
||||
|
||||
def get_variables():
|
||||
return {
|
||||
"foo1": "session_bar1"
|
||||
}
|
||||
|
||||
15
examples/postman_echo/request_methods/demo_testsuite.yml
Normal file
15
examples/postman_echo/request_methods/demo_testsuite.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
config:
|
||||
name: "demo testsuite"
|
||||
variables: ${get_variables()}
|
||||
|
||||
testcases:
|
||||
-
|
||||
name: request with functions
|
||||
testcase: request_methods/request_with_functions.yml
|
||||
variables:
|
||||
var1: testsuite_val1
|
||||
-
|
||||
name: request with referenced testcase
|
||||
testcase: request_methods/request_with_testcase_reference.yml
|
||||
variables:
|
||||
var2: testsuite_val2
|
||||
@@ -0,0 +1,89 @@
|
||||
# NOTICE: Generated By HttpRunner. DO'NOT EDIT!
|
||||
# FROM: examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions.yml
|
||||
from httprunner import HttpRunner, TConfig, TStep
|
||||
|
||||
|
||||
class TestCaseRequestWithFunctions(HttpRunner):
|
||||
config = TConfig(
|
||||
**{
|
||||
"name": "request with functions",
|
||||
"variables": {"foo1": "session_bar1", "var1": "testsuite_val1"},
|
||||
"base_url": "https://postman-echo.com",
|
||||
"verify": False,
|
||||
"path": "examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py",
|
||||
}
|
||||
)
|
||||
|
||||
teststeps = [
|
||||
TStep(
|
||||
**{
|
||||
"name": "get with params",
|
||||
"variables": {
|
||||
"foo1": "bar1",
|
||||
"foo2": "session_bar2",
|
||||
"sum_v": "${sum_two(1, 2)}",
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/get",
|
||||
"params": {"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"},
|
||||
"headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"},
|
||||
},
|
||||
"extract": {"session_foo2": "body.args.foo2"},
|
||||
"validate": [
|
||||
{"eq": ["status_code", 200]},
|
||||
{"eq": ["body.args.foo1", "session_bar1"]},
|
||||
{"eq": ["body.args.sum_v", 3]},
|
||||
{"eq": ["body.args.foo2", "session_bar2"]},
|
||||
],
|
||||
}
|
||||
),
|
||||
TStep(
|
||||
**{
|
||||
"name": "post raw text",
|
||||
"variables": {"foo1": "hello world", "foo3": "$session_foo2"},
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/post",
|
||||
"headers": {
|
||||
"User-Agent": "HttpRunner/${get_httprunner_version()}",
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
"data": "This is expected to be sent back as part of response body: $foo1-$foo3.",
|
||||
},
|
||||
"validate": [
|
||||
{"eq": ["status_code", 200]},
|
||||
{
|
||||
"eq": [
|
||||
"body.data",
|
||||
"This is expected to be sent back as part of response body: session_bar1-session_bar2.",
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
),
|
||||
TStep(
|
||||
**{
|
||||
"name": "post form data",
|
||||
"variables": {"foo1": "bar1", "foo2": "bar2"},
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "/post",
|
||||
"headers": {
|
||||
"User-Agent": "HttpRunner/${get_httprunner_version()}",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
"data": "foo1=$foo1&foo2=$foo2",
|
||||
},
|
||||
"validate": [
|
||||
{"eq": ["status_code", 200]},
|
||||
{"eq": ["body.form.foo1", "session_bar1"]},
|
||||
{"eq": ["body.form.foo2", "bar2"]},
|
||||
],
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
TestCaseRequestWithFunctions().test_start()
|
||||
@@ -0,0 +1,29 @@
|
||||
# NOTICE: Generated By HttpRunner. DO'NOT EDIT!
|
||||
# FROM: examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference.yml
|
||||
from httprunner import HttpRunner, TConfig, TStep
|
||||
|
||||
|
||||
class TestCaseRequestWithTestcaseReference(HttpRunner):
|
||||
config = TConfig(
|
||||
**{
|
||||
"name": "request with referenced testcase",
|
||||
"variables": {"foo1": "session_bar1", "var2": "testsuite_val2"},
|
||||
"base_url": "https://postman-echo.com",
|
||||
"verify": False,
|
||||
"path": "examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py",
|
||||
}
|
||||
)
|
||||
|
||||
teststeps = [
|
||||
TStep(
|
||||
**{
|
||||
"name": "request with functions",
|
||||
"variables": {"foo1": "override_bar1"},
|
||||
"testcase": "request_methods/request_with_functions.yml",
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
TestCaseRequestWithTestcaseReference().test_start()
|
||||
@@ -10,6 +10,7 @@ class TestCaseHardcode(HttpRunner):
|
||||
"base_url": "https://postman-echo.com",
|
||||
"verify": False,
|
||||
"path": "examples/postman_echo/request_methods/hardcode_test.py",
|
||||
"variables": {},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ config:
|
||||
|
||||
teststeps:
|
||||
-
|
||||
name: request with variables
|
||||
name: request with functions
|
||||
variables:
|
||||
foo1: override_bar1
|
||||
testcase: request_methods/request_with_variables.yml
|
||||
testcase: request_methods/request_with_functions.yml
|
||||
|
||||
@@ -17,9 +17,9 @@ class TestCaseRequestWithTestcaseReference(HttpRunner):
|
||||
teststeps = [
|
||||
TStep(
|
||||
**{
|
||||
"name": "request with variables",
|
||||
"name": "request with functions",
|
||||
"variables": {"foo1": "override_bar1"},
|
||||
"testcase": "request_methods/request_with_variables.yml",
|
||||
"testcase": "request_methods/request_with_functions.yml",
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
config:
|
||||
name: "request methods testcase with variables"
|
||||
variables:
|
||||
foo1: session_bar1
|
||||
variables: ${get_variables()}
|
||||
base_url: "https://postman-echo.com"
|
||||
verify: False
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = "3.0.3"
|
||||
__version__ = "3.0.4"
|
||||
__description__ = "One-stop solution for HTTP(S) testing."
|
||||
|
||||
from httprunner.runner import HttpRunner
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from loguru import logger
|
||||
|
||||
from httprunner import __description__, __version__, exceptions
|
||||
from httprunner.ext.har2case import init_har2case_parser, main_har2case
|
||||
@@ -19,25 +20,30 @@ def init_parser_run(subparsers):
|
||||
|
||||
def main_run(extra_args):
|
||||
tests_path_list = []
|
||||
for index, item in enumerate(extra_args):
|
||||
extra_args_new = []
|
||||
for item in extra_args:
|
||||
if not os.path.exists(item):
|
||||
# item is not file/folder path
|
||||
continue
|
||||
elif os.path.isfile(item):
|
||||
# replace YAML/JSON file path with generated python file
|
||||
extra_args[index], _ = convert_testcase_path(item)
|
||||
|
||||
tests_path_list.append(item)
|
||||
extra_args_new.append(item)
|
||||
else:
|
||||
# item is file/folder path
|
||||
tests_path_list.append(item)
|
||||
|
||||
if len(tests_path_list) == 0:
|
||||
# has not specified any testcase path
|
||||
raise exceptions.ParamsError("Missed testcase path")
|
||||
logger.error(f"No valid testcase path in cli arguments: {extra_args}")
|
||||
sys.exit(1)
|
||||
|
||||
main_make(tests_path_list)
|
||||
testcase_path_list = main_make(tests_path_list)
|
||||
if not testcase_path_list:
|
||||
logger.error("No valid testcases found, exit 1.")
|
||||
sys.exit(1)
|
||||
|
||||
if "-s" not in extra_args:
|
||||
extra_args.insert(0, "-s")
|
||||
pytest.main(extra_args)
|
||||
extra_args_new.extend(testcase_path_list)
|
||||
if "-s" not in extra_args_new:
|
||||
extra_args_new.insert(0, "-s")
|
||||
|
||||
pytest.main(extra_args_new)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import time
|
||||
|
||||
import requests
|
||||
@@ -32,12 +33,19 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData:
|
||||
def log_print(req_or_resp, r_type):
|
||||
msg = f"\n================== {r_type} details ==================\n"
|
||||
for key, value in req_or_resp.dict().items():
|
||||
msg += "{:<16} : {}\n".format(key, repr(value))
|
||||
if isinstance(value, dict):
|
||||
value = json.dumps(value, indent=4)
|
||||
|
||||
msg += "{:<8} : {}\n".format(key, value)
|
||||
logger.debug(msg)
|
||||
|
||||
# record actual request info
|
||||
request_headers = dict(resp_obj.request.headers)
|
||||
request_body = resp_obj.request.body
|
||||
try:
|
||||
request_body = json.loads(request_body)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
if request_body:
|
||||
request_content_type = lower_dict_keys(request_headers).get("content-type")
|
||||
@@ -195,10 +203,6 @@ class HttpSession(requests.Session):
|
||||
Safe mode has been removed from requests 1.x.
|
||||
"""
|
||||
try:
|
||||
msg = "processed request:\n"
|
||||
msg += f"> {method} {url}\n"
|
||||
msg += f"> kwargs: {kwargs}"
|
||||
logger.debug(msg)
|
||||
return requests.Session.request(self, method, url, **kwargs)
|
||||
except (MissingSchema, InvalidSchema, InvalidURL):
|
||||
raise
|
||||
|
||||
@@ -44,6 +44,10 @@ class TestCaseFormatError(MyBaseError):
|
||||
pass
|
||||
|
||||
|
||||
class TestSuiteFormatError(MyBaseError):
|
||||
pass
|
||||
|
||||
|
||||
class ParamsError(MyBaseError):
|
||||
pass
|
||||
|
||||
|
||||
@@ -14,24 +14,6 @@ except ImportError:
|
||||
JSONDecodeError = ValueError
|
||||
|
||||
|
||||
IGNORE_REQUEST_HEADERS = [
|
||||
"host",
|
||||
"accept",
|
||||
"content-length",
|
||||
"connection",
|
||||
"accept-encoding",
|
||||
"accept-language",
|
||||
"origin",
|
||||
"cache-control",
|
||||
"pragma",
|
||||
"upgrade-insecure-requests",
|
||||
":authority",
|
||||
":method",
|
||||
":scheme",
|
||||
":path",
|
||||
]
|
||||
|
||||
|
||||
class HarParser(object):
|
||||
def __init__(self, har_file_path, filter_str=None, exclude_str=None):
|
||||
self.har_file_path = har_file_path
|
||||
@@ -93,6 +75,14 @@ class HarParser(object):
|
||||
|
||||
teststep_dict["request"]["method"] = method
|
||||
|
||||
def __make_request_cookies(self, teststep_dict, entry_json):
|
||||
cookies = {}
|
||||
for cookie in entry_json["request"].get("cookies", []):
|
||||
cookies[cookie["name"]] = cookie["value"]
|
||||
|
||||
if cookies:
|
||||
teststep_dict["request"]["cookies"] = cookies
|
||||
|
||||
def __make_request_headers(self, teststep_dict, entry_json):
|
||||
""" parse HAR entry request headers, and make teststep headers.
|
||||
header in IGNORE_REQUEST_HEADERS will be ignored.
|
||||
@@ -119,7 +109,7 @@ class HarParser(object):
|
||||
"""
|
||||
teststep_headers = {}
|
||||
for header in entry_json["request"].get("headers", []):
|
||||
if header["name"].lower() in IGNORE_REQUEST_HEADERS:
|
||||
if header["name"] == "cookie" or header["name"].startswith(":"):
|
||||
continue
|
||||
|
||||
teststep_headers[header["name"]] = header["value"]
|
||||
@@ -288,6 +278,7 @@ class HarParser(object):
|
||||
|
||||
self.__make_request_url(teststep_dict, entry_json)
|
||||
self.__make_request_method(teststep_dict, entry_json)
|
||||
self.__make_request_cookies(teststep_dict, entry_json)
|
||||
self.__make_request_headers(teststep_dict, entry_json)
|
||||
self._make_request_data(teststep_dict, entry_json)
|
||||
self._make_validate(teststep_dict, entry_json)
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Union, Text, List, Tuple
|
||||
from typing import Union, Text, List, Tuple, Dict
|
||||
|
||||
import jinja2
|
||||
from loguru import logger
|
||||
|
||||
from httprunner import exceptions
|
||||
from httprunner.exceptions import TestCaseFormatError
|
||||
from httprunner.loader import load_testcase_file, load_folder_files
|
||||
from httprunner.loader import (
|
||||
load_folder_files,
|
||||
load_test_file,
|
||||
load_testcase,
|
||||
load_testsuite,
|
||||
load_project_meta,
|
||||
)
|
||||
from httprunner.parser import parse_data
|
||||
|
||||
__TMPL__ = """# NOTICE: Generated By HttpRunner. DO'NOT EDIT!
|
||||
# FROM: {{ testcase_path }}
|
||||
@@ -39,7 +45,9 @@ def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]:
|
||||
|
||||
file_suffix = file_suffix.lower()
|
||||
if file_suffix not in [".json", ".yml", ".yaml"]:
|
||||
raise exceptions.ParamsError("")
|
||||
raise exceptions.ParamsError(
|
||||
"testcase file should have .yaml/.yml/.json suffix"
|
||||
)
|
||||
|
||||
file_name = raw_file_name.replace(" ", "_").replace(".", "_").replace("-", "_")
|
||||
testcase_dir = os.path.dirname(testcase_path)
|
||||
@@ -51,18 +59,40 @@ def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]:
|
||||
return testcase_python_path, name_in_title_case
|
||||
|
||||
|
||||
def make_testcase(testcase_path: str) -> Union[str, None]:
|
||||
logger.info(f"start to make testcase: {testcase_path}")
|
||||
def format_pytest_with_black(python_paths: List[Text]):
|
||||
logger.info("format pytest cases with black ...")
|
||||
try:
|
||||
testcase, _ = load_testcase_file(testcase_path)
|
||||
except TestCaseFormatError:
|
||||
return None
|
||||
subprocess.run(["black", *python_paths])
|
||||
except subprocess.CalledProcessError as ex:
|
||||
logger.error(ex)
|
||||
|
||||
|
||||
def make_testcase(testcase: Dict) -> Union[str, None]:
|
||||
"""convert valid testcase dict to pytest file path"""
|
||||
try:
|
||||
# validate testcase format
|
||||
load_testcase(testcase)
|
||||
except exceptions.TestCaseFormatError as ex:
|
||||
logger.error(f"TestCaseFormatError: {ex}")
|
||||
raise
|
||||
|
||||
testcase_path = testcase["config"]["path"]
|
||||
logger.info(f"start to make testcase: {testcase_path}")
|
||||
|
||||
template = jinja2.Template(__TMPL__)
|
||||
|
||||
testcase_python_path, name_in_title_case = convert_testcase_path(testcase_path)
|
||||
|
||||
config = testcase["config"]
|
||||
|
||||
config.setdefault("variables", {})
|
||||
if isinstance(config["variables"], Text):
|
||||
# get variables by function, e.g. ${get_variables()}
|
||||
project_meta = load_project_meta(testcase_path)
|
||||
config["variables"] = parse_data(
|
||||
config["variables"], {}, project_meta.functions
|
||||
)
|
||||
|
||||
config["path"] = testcase_python_path
|
||||
data = {
|
||||
"testcase_path": testcase_path,
|
||||
@@ -79,42 +109,119 @@ def make_testcase(testcase_path: str) -> Union[str, None]:
|
||||
return testcase_python_path
|
||||
|
||||
|
||||
def format_with_black(tests_path: Text):
|
||||
logger.info("format testcases with black ...")
|
||||
tests_path, _ = convert_testcase_path(tests_path)
|
||||
|
||||
def make_testsuite(testsuite: Dict) -> List[Text]:
|
||||
"""convert valid testsuite dict to pytest folder with testcases"""
|
||||
try:
|
||||
subprocess.run(["black", tests_path])
|
||||
except subprocess.CalledProcessError as ex:
|
||||
logger.error(ex)
|
||||
# validate testcase format
|
||||
load_testsuite(testsuite)
|
||||
except exceptions.TestSuiteFormatError as ex:
|
||||
logger.error(f"TestSuiteFormatError: {ex}")
|
||||
raise
|
||||
|
||||
config = testsuite["config"]
|
||||
testsuite_path = config["path"]
|
||||
project_meta = load_project_meta(testsuite_path)
|
||||
project_working_directory = project_meta.PWD
|
||||
|
||||
testsuite_variables = config.get("variables", {})
|
||||
if isinstance(testsuite_variables, Text):
|
||||
# get variables by function, e.g. ${get_variables()}
|
||||
testsuite_variables = parse_data(
|
||||
testsuite_variables, {}, project_meta.functions
|
||||
)
|
||||
|
||||
logger.info(f"start to make testsuite: {testsuite_path}")
|
||||
|
||||
# create directory with testsuite file name, put its testcases under this directory
|
||||
testsuite_dir = testsuite_path.replace(".", "_")
|
||||
os.makedirs(testsuite_dir, exist_ok=True)
|
||||
|
||||
testcase_files = []
|
||||
|
||||
for testcase in testsuite["testcases"]:
|
||||
# get referenced testcase content
|
||||
testcase_file = testcase["testcase"]
|
||||
testcase_path = os.path.join(project_working_directory, testcase_file)
|
||||
testcase_dict = load_test_file(testcase_path)
|
||||
testcase_dict.setdefault("config", {})
|
||||
testcase_dict["config"]["path"] = os.path.join(
|
||||
testsuite_dir, os.path.basename(testcase_path)
|
||||
)
|
||||
|
||||
# override testcase name
|
||||
testcase_dict["config"]["name"] = testcase["name"]
|
||||
# override base_url
|
||||
base_url = testsuite["config"].get("base_url") or testcase.get("base_url")
|
||||
if base_url:
|
||||
testcase_dict["config"]["base_url"] = base_url
|
||||
# override variables
|
||||
testcase_dict["config"].setdefault("variables", {})
|
||||
testcase_dict["config"]["variables"].update(testcase.get("variables", {}))
|
||||
testcase_dict["config"]["variables"].update(testsuite_variables)
|
||||
|
||||
# make testcase
|
||||
testcase_path = make_testcase(testcase_dict)
|
||||
testcase_files.append(testcase_path)
|
||||
|
||||
return testcase_files
|
||||
|
||||
|
||||
def make(tests_path: Text) -> List:
|
||||
testcases = []
|
||||
def __make(tests_path: Text) -> List:
|
||||
test_files = []
|
||||
if os.path.isdir(tests_path):
|
||||
files_list = load_folder_files(tests_path)
|
||||
testcases.extend(files_list)
|
||||
test_files.extend(files_list)
|
||||
elif os.path.isfile(tests_path):
|
||||
testcases.append(tests_path)
|
||||
test_files.append(tests_path)
|
||||
else:
|
||||
raise exceptions.TestcaseNotFound(f"Invalid tests path: {tests_path}")
|
||||
|
||||
testcase_path_list = []
|
||||
for testcase_path in testcases:
|
||||
testcase_path = make_testcase(testcase_path)
|
||||
if not testcase_path:
|
||||
for test_file in test_files:
|
||||
try:
|
||||
test_content = load_test_file(test_file)
|
||||
test_content.setdefault("config", {})["path"] = test_file
|
||||
except (exceptions.FileNotFound, exceptions.FileFormatError) as ex:
|
||||
logger.warning(ex)
|
||||
continue
|
||||
testcase_path_list.append(testcase_path)
|
||||
|
||||
format_with_black(tests_path)
|
||||
# testcase
|
||||
if "teststeps" in test_content:
|
||||
try:
|
||||
testcase_file = make_testcase(test_content)
|
||||
except exceptions.TestCaseFormatError:
|
||||
continue
|
||||
|
||||
testcase_path_list.append(testcase_file)
|
||||
|
||||
# testsuite
|
||||
elif "testcases" in test_content:
|
||||
try:
|
||||
testcase_files = make_testsuite(test_content)
|
||||
except exceptions.TestSuiteFormatError:
|
||||
continue
|
||||
|
||||
testcase_path_list.extend(testcase_files)
|
||||
|
||||
# invalid format
|
||||
else:
|
||||
raise exceptions.FileFormatError(
|
||||
f"test file is neither testcase nor testsuite: {test_file}"
|
||||
)
|
||||
|
||||
if not testcase_path_list:
|
||||
logger.warning(f"No valid testcase generated on {tests_path}")
|
||||
return []
|
||||
|
||||
return testcase_path_list
|
||||
|
||||
|
||||
def main_make(tests_paths: List[Text]) -> List:
|
||||
testcase_path_list = []
|
||||
for tests_path in tests_paths:
|
||||
testcase_path_list.extend(make(tests_path))
|
||||
testcase_path_list.extend(__make(tests_path))
|
||||
|
||||
format_pytest_with_black(testcase_path_list)
|
||||
return testcase_path_list
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import unittest
|
||||
from httprunner.ext.make import make_testcase, main_make, convert_testcase_path
|
||||
|
||||
from httprunner.ext.make import main_make, convert_testcase_path
|
||||
|
||||
|
||||
class TestLoader(unittest.TestCase):
|
||||
def test_make_testcase(self):
|
||||
path = "examples/postman_echo/request_methods/request_with_variables.yml"
|
||||
testcase_python_path = make_testcase(path)
|
||||
path = ["examples/postman_echo/request_methods/request_with_variables.yml"]
|
||||
testcase_python_list = main_make(path)
|
||||
self.assertEqual(
|
||||
testcase_python_path,
|
||||
testcase_python_list[0],
|
||||
"examples/postman_echo/request_methods/request_with_variables_test.py",
|
||||
)
|
||||
|
||||
@@ -21,42 +22,47 @@ class TestLoader(unittest.TestCase):
|
||||
|
||||
def test_convert_testcase_path(self):
|
||||
self.assertEqual(
|
||||
convert_testcase_path("mubu.login.yml")[0],
|
||||
"mubu_login_test.py"
|
||||
convert_testcase_path("mubu.login.yml")[0], "mubu_login_test.py"
|
||||
)
|
||||
self.assertEqual(
|
||||
convert_testcase_path("/path/to/mubu.login.yml")[0],
|
||||
"/path/to/mubu_login_test.py"
|
||||
"/path/to/mubu_login_test.py",
|
||||
)
|
||||
self.assertEqual(
|
||||
convert_testcase_path("/path/to 2/mubu.login.yml")[0],
|
||||
"/path/to 2/mubu_login_test.py"
|
||||
"/path/to 2/mubu_login_test.py",
|
||||
)
|
||||
self.assertEqual(
|
||||
convert_testcase_path("/path/to 2/mubu.login.yml")[1],
|
||||
"MubuLogin"
|
||||
convert_testcase_path("/path/to 2/mubu.login.yml")[1], "MubuLogin"
|
||||
)
|
||||
self.assertEqual(
|
||||
convert_testcase_path("mubu login.yml")[0],
|
||||
"mubu_login_test.py"
|
||||
convert_testcase_path("mubu login.yml")[0], "mubu_login_test.py"
|
||||
)
|
||||
self.assertEqual(
|
||||
convert_testcase_path("/path/to 2/mubu login.yml")[1],
|
||||
"MubuLogin"
|
||||
convert_testcase_path("/path/to 2/mubu login.yml")[1], "MubuLogin"
|
||||
)
|
||||
self.assertEqual(
|
||||
convert_testcase_path("/path/to 2/mubu-login.yml")[0],
|
||||
"/path/to 2/mubu_login_test.py"
|
||||
"/path/to 2/mubu_login_test.py",
|
||||
)
|
||||
self.assertEqual(
|
||||
convert_testcase_path("/path/to 2/mubu-login.yml")[1],
|
||||
"MubuLogin"
|
||||
convert_testcase_path("/path/to 2/mubu-login.yml")[1], "MubuLogin"
|
||||
)
|
||||
self.assertEqual(
|
||||
convert_testcase_path("/path/to 2/幕布login.yml")[0],
|
||||
"/path/to 2/幕布login_test.py"
|
||||
"/path/to 2/幕布login_test.py",
|
||||
)
|
||||
self.assertEqual(
|
||||
convert_testcase_path("/path/to/幕布login.yml")[1],
|
||||
"幕布Login"
|
||||
self.assertEqual(convert_testcase_path("/path/to/幕布login.yml")[1], "幕布Login")
|
||||
|
||||
def test_make_testsuite(self):
|
||||
path = ["examples/postman_echo/request_methods/demo_testsuite.yml"]
|
||||
testcase_python_list = main_make(path)
|
||||
self.assertEqual(len(testcase_python_list), 2)
|
||||
self.assertIn(
|
||||
"examples/postman_echo/request_methods/demo_testsuite_yml/request_with_functions_test.py",
|
||||
testcase_python_list,
|
||||
)
|
||||
self.assertIn(
|
||||
"examples/postman_echo/request_methods/demo_testsuite_yml/request_with_testcase_reference_test.py",
|
||||
testcase_python_list,
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from pydantic import ValidationError
|
||||
|
||||
from httprunner import builtin, utils
|
||||
from httprunner import exceptions
|
||||
from httprunner.schema import TestCase, ProjectMeta
|
||||
from httprunner.schema import TestCase, ProjectMeta, TestSuite
|
||||
|
||||
try:
|
||||
# PyYAML version >= 5.1
|
||||
@@ -34,7 +34,8 @@ def _load_yaml_file(yaml_file: Text) -> Dict:
|
||||
try:
|
||||
yaml_content = yaml.load(stream)
|
||||
except yaml.YAMLError as ex:
|
||||
logger.error(str(ex))
|
||||
err_msg = f"YAMLError:\nfile: {yaml_file}\nerror: {ex}"
|
||||
logger.error(err_msg)
|
||||
raise exceptions.FileFormatError
|
||||
|
||||
return yaml_content
|
||||
@@ -46,42 +47,65 @@ def _load_json_file(json_file: Text) -> Dict:
|
||||
with io.open(json_file, encoding="utf-8") as data_file:
|
||||
try:
|
||||
json_content = json.load(data_file)
|
||||
except json.JSONDecodeError:
|
||||
err_msg = f"JSONDecodeError: JSON file format error: {json_file}"
|
||||
except json.JSONDecodeError as ex:
|
||||
err_msg = f"JSONDecodeError:\nfile: {json_file}\nerror: {ex}"
|
||||
logger.error(err_msg)
|
||||
raise exceptions.FileFormatError(err_msg)
|
||||
|
||||
return json_content
|
||||
|
||||
|
||||
def load_testcase_file(testcase_file: Text) -> Tuple[Dict, TestCase]:
|
||||
"""load testcase file and validate with pydantic model"""
|
||||
if not os.path.isfile(testcase_file):
|
||||
raise exceptions.FileNotFound(f"testcase file not exists: {testcase_file}")
|
||||
def load_test_file(test_file: Text) -> Dict:
|
||||
"""load testcase/testsuite file content"""
|
||||
if not os.path.isfile(test_file):
|
||||
raise exceptions.FileNotFound(f"test file not exists: {test_file}")
|
||||
|
||||
file_suffix = os.path.splitext(testcase_file)[1].lower()
|
||||
file_suffix = os.path.splitext(test_file)[1].lower()
|
||||
if file_suffix == ".json":
|
||||
testcase_content = _load_json_file(testcase_file)
|
||||
test_file_content = _load_json_file(test_file)
|
||||
elif file_suffix in [".yaml", ".yml"]:
|
||||
testcase_content = _load_yaml_file(testcase_file)
|
||||
test_file_content = _load_yaml_file(test_file)
|
||||
else:
|
||||
# '' or other suffix
|
||||
raise exceptions.FileFormatError(
|
||||
f"testcase file should be YAML/JSON format, invalid testcase file: {testcase_file}"
|
||||
f"testcase/testsuite file should be YAML/JSON format, invalid format file: {test_file}"
|
||||
)
|
||||
|
||||
return test_file_content
|
||||
|
||||
|
||||
def load_testcase(testcase: Dict) -> TestCase:
|
||||
path = testcase["config"]["path"]
|
||||
try:
|
||||
# validate with pydantic TestCase model
|
||||
testcase_obj = TestCase.parse_obj(testcase_content)
|
||||
testcase_obj = TestCase.parse_obj(testcase)
|
||||
except ValidationError as ex:
|
||||
err_msg = f"Invalid testcase format: {testcase_file}"
|
||||
logger.error(f"{err_msg}\n{ex}")
|
||||
err_msg = f"TestCase ValidationError:\nfile: {path}\nerror: {ex}"
|
||||
logger.error(err_msg)
|
||||
raise exceptions.TestCaseFormatError(err_msg)
|
||||
|
||||
testcase_content["config"]["path"] = testcase_file
|
||||
testcase_obj.config.path = testcase_file
|
||||
return testcase_obj
|
||||
|
||||
return testcase_content, testcase_obj
|
||||
|
||||
def load_testcase_file(testcase_file: Text) -> TestCase:
|
||||
"""load testcase file and validate with pydantic model"""
|
||||
testcase_content = load_test_file(testcase_file)
|
||||
testcase_content.setdefault("config", {})["path"] = testcase_file
|
||||
testcase_obj = load_testcase(testcase_content)
|
||||
return testcase_obj
|
||||
|
||||
|
||||
def load_testsuite(testsuite: Dict) -> TestSuite:
|
||||
path = testsuite["config"]["path"]
|
||||
try:
|
||||
# validate with pydantic TestCase model
|
||||
testsuite_obj = TestSuite.parse_obj(testsuite)
|
||||
except ValidationError as ex:
|
||||
err_msg = f"TestSuite ValidationError:\nfile: {path}\nerror: {ex}"
|
||||
logger.error(err_msg)
|
||||
raise exceptions.TestSuiteFormatError(err_msg)
|
||||
|
||||
return testsuite_obj
|
||||
|
||||
|
||||
def load_dot_env_file(dot_env_path: Text) -> Dict:
|
||||
@@ -349,6 +373,14 @@ def init_project_working_directory(test_path: Text) -> Tuple[Text, Text]:
|
||||
return debugtalk_path, project_working_directory
|
||||
|
||||
|
||||
def get_project_working_directory(test_path: Text) -> Text:
|
||||
global project_working_directory
|
||||
if not project_working_directory:
|
||||
init_project_working_directory(test_path)
|
||||
|
||||
return project_working_directory
|
||||
|
||||
|
||||
def load_debugtalk_functions() -> Dict[Text, Callable]:
|
||||
""" load project debugtalk.py module functions
|
||||
debugtalk.py should be located in project working directory.
|
||||
|
||||
@@ -7,14 +7,10 @@ from httprunner import exceptions, loader
|
||||
class TestLoader(unittest.TestCase):
|
||||
def test_load_testcase_file(self):
|
||||
path = "examples/postman_echo/request_methods/request_with_variables.yml"
|
||||
testcase_json, testcase_obj = loader.load_testcase_file(path)
|
||||
self.assertEqual(
|
||||
testcase_json["config"]["name"], "request methods testcase with variables"
|
||||
)
|
||||
testcase_obj = loader.load_testcase_file(path)
|
||||
self.assertEqual(
|
||||
testcase_obj.config.name, "request methods testcase with variables"
|
||||
)
|
||||
self.assertEqual(len(testcase_json["teststeps"]), 3)
|
||||
self.assertEqual(len(testcase_obj.teststeps), 3)
|
||||
|
||||
def test_load_json_file_file_format_error(self):
|
||||
|
||||
@@ -124,6 +124,7 @@ class ResponseObject(object):
|
||||
self.resp_obj_meta = {
|
||||
"status_code": resp_obj.status_code,
|
||||
"headers": resp_obj.headers,
|
||||
"cookies": dict(resp_obj.cookies),
|
||||
"body": body,
|
||||
}
|
||||
self.validation_results: Dict = {}
|
||||
@@ -163,6 +164,8 @@ class ResponseObject(object):
|
||||
|
||||
# check item
|
||||
check_item = u_validator["check"]
|
||||
# TODO: validate variable or function
|
||||
# check_item = parse_data(check_item, variables_mapping, functions_mapping)
|
||||
check_value = jmespath.search(check_item, self.resp_obj_meta)
|
||||
check_value = parse_string_value(check_value)
|
||||
|
||||
|
||||
@@ -65,12 +65,8 @@ class HttpRunner(object):
|
||||
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["json"] = parsed_request_dict.pop("req_json", {})
|
||||
|
||||
logger.info(f"{method} {url}")
|
||||
logger.debug(f"request kwargs(raw): {parsed_request_dict}")
|
||||
|
||||
# request
|
||||
self.__session = self.__session or HttpSession()
|
||||
resp = self.__session.request(method, url, **parsed_request_dict)
|
||||
@@ -148,7 +144,7 @@ class HttpRunner(object):
|
||||
|
||||
def __run_step(self, step: TStep):
|
||||
"""run teststep, teststep maybe a request or referenced testcase"""
|
||||
logger.info(f"run step: {step.name}")
|
||||
logger.info(f"run step begin: {step.name} >>>>>>")
|
||||
|
||||
if step.request:
|
||||
step_data = self.__run_step_request(step)
|
||||
@@ -160,6 +156,7 @@ class HttpRunner(object):
|
||||
)
|
||||
|
||||
self.__step_datas.append(step_data)
|
||||
logger.info(f"run step end: {step.name} <<<<<<\n")
|
||||
return step_data.export
|
||||
|
||||
def run(self, testcase: TestCase):
|
||||
@@ -209,7 +206,7 @@ class HttpRunner(object):
|
||||
if not os.path.isfile(path):
|
||||
raise exceptions.ParamsError(f"Invalid testcase path: {path}")
|
||||
|
||||
_, testcase_obj = load_testcase_file(path)
|
||||
testcase_obj = load_testcase_file(path)
|
||||
return self.run(testcase_obj)
|
||||
|
||||
def get_step_datas(self) -> List[StepData]:
|
||||
|
||||
@@ -9,11 +9,11 @@ class TestHttpRunner(unittest.TestCase):
|
||||
|
||||
def test_run_testcase_by_path_request_only(self):
|
||||
self.runner.run_path(
|
||||
"examples/postman_echo/request_methods/request_with_variables.yml"
|
||||
"examples/postman_echo/request_methods/request_with_functions.yml"
|
||||
)
|
||||
result = self.runner.get_summary()
|
||||
self.assertTrue(result.success)
|
||||
self.assertEqual(result.name, "request methods testcase with variables")
|
||||
self.assertEqual(result.name, "request methods testcase with functions")
|
||||
self.assertEqual(result.step_datas[0].name, "get with params")
|
||||
self.assertEqual(len(result.step_datas), 3)
|
||||
|
||||
@@ -24,5 +24,5 @@ class TestHttpRunner(unittest.TestCase):
|
||||
result = self.runner.get_summary()
|
||||
self.assertTrue(result.success)
|
||||
self.assertEqual(result.name, "request methods testcase: reference testcase")
|
||||
self.assertEqual(result.step_datas[0].name, "request with variables")
|
||||
self.assertEqual(result.step_datas[0].name, "request with functions")
|
||||
self.assertEqual(len(result.step_datas), 1)
|
||||
|
||||
@@ -36,7 +36,8 @@ class TConfig(BaseModel):
|
||||
name: Name
|
||||
verify: Verify = False
|
||||
base_url: BaseUrl = ""
|
||||
variables: VariablesMapping = {}
|
||||
# Text: prepare variables in debugtalk.py, ${get_variable()}
|
||||
variables: Union[VariablesMapping, Text] = {}
|
||||
setup_hooks: Hook = []
|
||||
teardown_hooks: Hook = []
|
||||
export: Export = []
|
||||
@@ -160,6 +161,18 @@ class PlatformInfo(BaseModel):
|
||||
platform: Text
|
||||
|
||||
|
||||
class TestCaseRef(BaseModel):
|
||||
name: Text
|
||||
base_url: Text = ""
|
||||
testcase: Text
|
||||
variables: VariablesMapping = {}
|
||||
|
||||
|
||||
class TestSuite(BaseModel):
|
||||
config: TConfig
|
||||
testcases: List[TestCaseRef]
|
||||
|
||||
|
||||
class Stat(BaseModel):
|
||||
total: int = 0
|
||||
success: int = 0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "httprunner"
|
||||
version = "3.0.3"
|
||||
version = "3.0.4"
|
||||
description = "One-stop solution for HTTP(S) testing."
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
Reference in New Issue
Block a user