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:
debugtalk
2020-05-19 20:56:09 +08:00
committed by GitHub
26 changed files with 457 additions and 131 deletions

View File

@@ -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**

View File

@@ -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

View File

@@ -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(

View File

@@ -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"]

View File

@@ -7,3 +7,9 @@ def get_httprunner_version():
def sum_two(m, n):
return m + n
def get_variables():
return {
"foo1": "session_bar1"
}

View 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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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": {},
}
)

View File

@@ -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

View File

@@ -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",
}
),
]

View File

@@ -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

View File

@@ -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

View File

@@ -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():

View File

@@ -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

View File

@@ -44,6 +44,10 @@ class TestCaseFormatError(MyBaseError):
pass
class TestSuiteFormatError(MyBaseError):
pass
class ParamsError(MyBaseError):
pass

View File

@@ -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)

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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.

View File

@@ -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):

View File

@@ -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)

View File

@@ -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]:

View File

@@ -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)

View File

@@ -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

View File

@@ -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"