Merge pull request #809 from httprunner/leo_dev

2.4.8

**Added**

- feat: store parse failed api/testcase/testsuite file path in `logs/xxx.parse_failed.json`
- feat: add exception SummaryEmpty

**Fixed**

- fix: display request & response details in report when extraction failed
- fix: include CHANGELOG in package
This commit is contained in:
debugtalk
2019-12-25 11:42:18 +08:00
committed by GitHub
15 changed files with 261 additions and 144 deletions

View File

@@ -1,5 +1,21 @@
# Release History
## 2.4.8 (2019-12-25)
**Added**
- feat: store parse failed api/testcase/testsuite file path in `logs/xxx.parse_failed.json`
- feat: add exception SummaryEmpty
**Fixed**
- fix: display request & response details in report when extraction failed
- fix: include CHANGELOG in package
**Changed**
- change: use sys.exit(code) in hrun main
## 2.4.7 (2019-12-24)
**Added**

View File

@@ -1,4 +1,4 @@
__version__ = "2.4.7"
__version__ = "2.4.8"
__description__ = "One-stop solution for HTTP(S) testing."
__all__ = ["__version__", "__description__"]

View File

@@ -1,6 +1,5 @@
import sys
from httprunner.cli import main
if __name__ == "__main__":
sys.exit(main())
main()

View File

@@ -195,6 +195,10 @@ class HttpRunner(object):
# parse tests
self.exception_stage = "parse tests"
parsed_testcases = parser.parse_tests(tests_mapping)
parse_failed_testfiles = parser.get_parse_failed_testfiles()
if parse_failed_testfiles:
logger.log_warning("parse failures occurred ...")
utils.dump_logs(parse_failed_testfiles, project_mapping, "parse_failed")
if self.save_tests:
utils.dump_logs(parsed_testcases, project_mapping, "parsed")
@@ -277,6 +281,8 @@ class HttpRunner(object):
path_or_tests:
str: testcase/testsuite file/foler path
dict: valid testcase/testsuite data
dot_env_path (str): specified .env file path.
mapping (dict): if mapping is specified, it will override variables in config block.
Returns:
dict: result summary

View File

@@ -1,7 +1,9 @@
import argparse
import os
import sys
from sentry_sdk import capture_exception
from httprunner import __description__, __version__
from httprunner.api import HttpRunner
from httprunner.compat import is_py2
@@ -64,23 +66,23 @@ def main():
if len(sys.argv) == 1:
# no argument passed
parser.print_help()
return 0
sys.exit(0)
if args.version:
color_print("{}".format(__version__), "GREEN")
return 0
sys.exit(0)
if args.validate:
validate_json_file(args.validate)
return 0
sys.exit(0)
if args.prettify:
prettify_json_file(args.prettify)
return 0
sys.exit(0)
project_name = args.startproject
if project_name:
create_scaffold(project_name)
return 0
sys.exit(0)
runner = HttpRunner(
failfast=args.failfast,
@@ -104,10 +106,10 @@ def main():
except Exception as ex:
color_print("!!!!!!!!!! exception stage: {} !!!!!!!!!!".format(runner.exception_stage), "YELLOW")
capture_exception(ex)
raise
err_code = 1
return err_code
sys.exit(err_code)
if __name__ == '__main__':
sys.exit(main())
main()

View File

@@ -14,6 +14,74 @@ from httprunner.utils import lower_dict_keys, omit_long_data
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def get_req_resp_record(resp_obj):
""" get request and response info from Response() object.
"""
def log_print(req_resp_dict, r_type):
msg = "\n================== {} details ==================\n".format(r_type)
for key, value in req_resp_dict[r_type].items():
msg += "{:<16} : {}\n".format(key, repr(value))
logger.log_debug(msg)
req_resp_dict = {
"request": {},
"response": {}
}
# record actual request info
req_resp_dict["request"]["url"] = resp_obj.request.url
req_resp_dict["request"]["method"] = resp_obj.request.method
req_resp_dict["request"]["headers"] = dict(resp_obj.request.headers)
request_body = resp_obj.request.body
if request_body:
request_content_type = lower_dict_keys(
req_resp_dict["request"]["headers"]
).get("content-type")
if request_content_type and "multipart/form-data" in request_content_type:
# upload file type
req_resp_dict["request"]["body"] = "upload file stream (OMITTED)"
else:
req_resp_dict["request"]["body"] = request_body
# log request details in debug mode
log_print(req_resp_dict, "request")
# record response info
req_resp_dict["response"]["ok"] = resp_obj.ok
req_resp_dict["response"]["url"] = resp_obj.url
req_resp_dict["response"]["status_code"] = resp_obj.status_code
req_resp_dict["response"]["reason"] = resp_obj.reason
req_resp_dict["response"]["cookies"] = resp_obj.cookies or {}
req_resp_dict["response"]["encoding"] = resp_obj.encoding
resp_headers = dict(resp_obj.headers)
req_resp_dict["response"]["headers"] = resp_headers
lower_resp_headers = lower_dict_keys(resp_headers)
content_type = lower_resp_headers.get("content-type", "")
req_resp_dict["response"]["content_type"] = content_type
if "image" in content_type:
# response is image type, record bytes content only
req_resp_dict["response"]["body"] = resp_obj.content
else:
try:
# try to record json data
if isinstance(resp_obj, response.ResponseObject):
req_resp_dict["response"]["body"] = resp_obj.json
else:
req_resp_dict["response"]["body"] = resp_obj.json()
except ValueError:
# only record at most 512 text charactors
resp_text = resp_obj.text
req_resp_dict["response"]["body"] = omit_long_data(resp_text)
# log response details in debug mode
log_print(req_resp_dict, "response")
return req_resp_dict
class ApiResponse(Response):
def raise_for_status(self):
@@ -62,79 +130,12 @@ class HttpSession(requests.Session):
}
}
def get_req_resp_record(self, resp_obj):
""" get request and response info from Response() object.
"""
def log_print(req_resp_dict, r_type):
msg = "\n================== {} details ==================\n".format(r_type)
for key, value in req_resp_dict[r_type].items():
msg += "{:<16} : {}\n".format(key, repr(value))
logger.log_debug(msg)
req_resp_dict = {
"request": {},
"response": {}
}
# record actual request info
req_resp_dict["request"]["url"] = resp_obj.request.url
req_resp_dict["request"]["method"] = resp_obj.request.method
req_resp_dict["request"]["headers"] = dict(resp_obj.request.headers)
request_body = resp_obj.request.body
if request_body:
request_content_type = lower_dict_keys(
req_resp_dict["request"]["headers"]
).get("content-type")
if request_content_type and "multipart/form-data" in request_content_type:
# upload file type
req_resp_dict["request"]["body"] = "upload file stream (OMITTED)"
else:
req_resp_dict["request"]["body"] = request_body
# log request details in debug mode
log_print(req_resp_dict, "request")
# record response info
req_resp_dict["response"]["ok"] = resp_obj.ok
req_resp_dict["response"]["url"] = resp_obj.url
req_resp_dict["response"]["status_code"] = resp_obj.status_code
req_resp_dict["response"]["reason"] = resp_obj.reason
req_resp_dict["response"]["cookies"] = resp_obj.cookies or {}
req_resp_dict["response"]["encoding"] = resp_obj.encoding
resp_headers = dict(resp_obj.headers)
req_resp_dict["response"]["headers"] = resp_headers
lower_resp_headers = lower_dict_keys(resp_headers)
content_type = lower_resp_headers.get("content-type", "")
req_resp_dict["response"]["content_type"] = content_type
if "image" in content_type:
# response is image type, record bytes content only
req_resp_dict["response"]["body"] = resp_obj.content
else:
try:
# try to record json data
if isinstance(resp_obj, response.ResponseObject):
req_resp_dict["response"]["body"] = resp_obj.json
else:
req_resp_dict["response"]["body"] = resp_obj.json()
except ValueError:
# only record at most 512 text charactors
resp_text = resp_obj.text
req_resp_dict["response"]["body"] = omit_long_data(resp_text)
# log response details in debug mode
log_print(req_resp_dict, "response")
return req_resp_dict
def update_last_req_resp_record(self, resp_obj):
"""
update request and response info from Response() object.
"""
self.meta_data["data"].pop()
self.meta_data["data"].append(self.get_req_resp_record(resp_obj))
self.meta_data["data"].append(get_req_resp_record(resp_obj))
def request(self, method, url, name=None, **kwargs):
"""
@@ -207,7 +208,7 @@ class HttpSession(requests.Session):
# record request and response histories, include 30X redirection
response_list = response.history + [response]
self.meta_data["data"] = [
self.get_req_resp_record(resp_obj)
get_req_resp_record(resp_obj)
for resp_obj in response_list
]

View File

@@ -6,18 +6,23 @@ from httprunner.compat import JSONDecodeError, FileNotFoundError
these exceptions will mark test as failure
"""
class MyBaseFailure(Exception):
pass
class ValidationFailure(MyBaseFailure):
pass
class ExtractFailure(MyBaseFailure):
pass
class SetupHooksFailure(MyBaseFailure):
pass
class TeardownHooksFailure(MyBaseFailure):
pass
@@ -26,35 +31,51 @@ class TeardownHooksFailure(MyBaseFailure):
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
"""

View File

@@ -16,6 +16,14 @@ 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=,]*)\)\}")
""" Store parse failed api/testcase/testsuite file path
"""
parse_failed_testfiles = {}
def get_parse_failed_testfiles():
return parse_failed_testfiles
def parse_string_value(str_value):
""" parse string to number if possible
@@ -1145,6 +1153,8 @@ def __prepare_testcase_tests(tests, config, project_mapping, session_variables_s
# 3, testcase_def config => testcase_def test_dict
test_dict = _parse_testcase(test_dict, project_mapping, session_variables_set)
if not test_dict:
continue
elif "api_def" in test_dict:
# test_dict has API reference
@@ -1216,21 +1226,34 @@ def _parse_testcase(testcase, project_mapping, session_variables_set=None):
"""
testcase.setdefault("config", {})
prepared_config = __prepare_config(
testcase["config"],
project_mapping,
session_variables_set
)
prepared_testcase_tests = __prepare_testcase_tests(
testcase["teststeps"],
prepared_config,
project_mapping,
session_variables_set
)
return {
"config": prepared_config,
"teststeps": prepared_testcase_tests
}
try:
prepared_config = __prepare_config(
testcase["config"],
project_mapping,
session_variables_set
)
prepared_testcase_tests = __prepare_testcase_tests(
testcase["teststeps"],
prepared_config,
project_mapping,
session_variables_set
)
return {
"config": prepared_config,
"teststeps": prepared_testcase_tests
}
except (exceptions.MyBaseFailure, exceptions.MyBaseError):
testcase_type = testcase["type"]
testcase_path = testcase.get("path")
global parse_failed_testfiles
if testcase_type not in parse_failed_testfiles:
parse_failed_testfiles[testcase_type] = []
parse_failed_testfiles[testcase_type].append(testcase_path)
return None
def __get_parsed_testsuite_testcases(testcases, testsuite_config, project_mapping):
@@ -1286,6 +1309,7 @@ def __get_parsed_testsuite_testcases(testcases, testsuite_config, project_mappin
parsed_testcase = testcase.pop("testcase_def")
parsed_testcase.setdefault("config", {})
parsed_testcase["path"] = testcase["testcase"]
parsed_testcase["type"] = "testcase"
parsed_testcase["config"]["name"] = testcase_name
if "weight" in testcase:
@@ -1331,6 +1355,8 @@ def __get_parsed_testsuite_testcases(testcases, testsuite_config, project_mappin
parameter_variables
)
parsed_testcase_copied = _parse_testcase(testcase_copied, project_mapping)
if not parsed_testcase_copied:
continue
parsed_testcase_copied["config"]["name"] = parse_lazy_data(
parsed_testcase_copied["config"]["name"],
testcase_copied["config"]["variables"]
@@ -1339,6 +1365,8 @@ def __get_parsed_testsuite_testcases(testcases, testsuite_config, project_mappin
else:
parsed_testcase = _parse_testcase(parsed_testcase, project_mapping)
if not parsed_testcase:
continue
parsed_testcase_list.append(parsed_testcase)
return parsed_testcase_list
@@ -1435,7 +1463,10 @@ def parse_tests(tests_mapping):
elif test_type == "testcases":
for testcase in tests_mapping["testcases"]:
testcase["type"] = "testcase"
parsed_testcase = _parse_testcase(testcase, project_mapping)
if not parsed_testcase:
continue
testcases.append(parsed_testcase)
elif test_type == "apis":
@@ -1445,9 +1476,13 @@ def parse_tests(tests_mapping):
"config": {
"name": api_content.get("name")
},
"teststeps": [api_content]
"teststeps": [api_content],
"path": api_content.pop("path", None),
"type": api_content.pop("type", "api")
}
parsed_testcase = _parse_testcase(testcase, project_mapping)
if not parsed_testcase:
continue
testcases.append(parsed_testcase)
return testcases

View File

@@ -5,6 +5,7 @@ from datetime import datetime
from jinja2 import Template
from httprunner import logger
from httprunner.exceptions import SummaryEmpty
def gen_html_report(summary, report_template=None, report_dir=None, report_file=None):
@@ -17,6 +18,10 @@ def gen_html_report(summary, report_template=None, report_dir=None, report_file=
report_file (str): specify html report file path, this has higher priority than specifying report dir.
"""
if not summary["time"] or summary["stat"]["testcases"]["total"] == 0:
logger.log_error("test result summary is empty ! {}".format(summary))
raise SummaryEmpty
if not report_template:
report_template = os.path.join(
os.path.abspath(os.path.dirname(__file__)),

View File

@@ -222,7 +222,8 @@ class ResponseObject(object):
# others
else:
err_msg = u"Failed to extract attribute from response! => {}\n".format(field)
err_msg += u"available response attributes: status_code, cookies, elapsed, headers, content, text, json, encoding, ok, reason, url.\n\n"
err_msg += u"available response attributes: status_code, cookies, elapsed, headers, content, " \
u"text, json, encoding, ok, reason, url.\n\n"
err_msg += u"If you want to set attribute in teardown_hooks, take the following example as reference:\n"
err_msg += u"response.new_attribute = 'new_attribute_value'\n"
logger.log_error(err_msg)

View File

@@ -1,5 +1,6 @@
# encoding: utf-8
from enum import Enum
from unittest.case import SkipTest
from httprunner import exceptions, logger, response, utils
@@ -8,6 +9,11 @@ from httprunner.context import SessionContext
from httprunner.validator import Validator
class HookTypeEnum(Enum):
SETUP = 1
TEARDOWN = 2
class Runner(object):
""" Running testcases.
@@ -74,11 +80,11 @@ class Runner(object):
self.session_context = SessionContext(config_variables)
if testcase_setup_hooks:
self.do_hook_actions(testcase_setup_hooks, "setup")
self.do_hook_actions(testcase_setup_hooks, HookTypeEnum.SETUP)
def __del__(self):
if self.testcase_teardown_hooks:
self.do_hook_actions(self.testcase_teardown_hooks, "teardown")
self.do_hook_actions(self.testcase_teardown_hooks, HookTypeEnum.TEARDOWN)
def __clear_test_data(self):
""" clear request and response data
@@ -131,10 +137,10 @@ class Runner(object):
format2 (str): only call hook functions.
${func()}
hook_type (enum): setup/teardown
hook_type (HookTypeEnum): setup/teardown
"""
logger.log_debug("call {} hook actions.".format(hook_type))
logger.log_debug("call {} hook actions.".format(hook_type.name))
for action in actions:
if isinstance(action, dict) and len(action) == 1:
@@ -215,7 +221,7 @@ class Runner(object):
# setup hooks
setup_hooks = test_dict.get("setup_hooks", [])
if setup_hooks:
self.do_hook_actions(setup_hooks, "setup")
self.do_hook_actions(setup_hooks, HookTypeEnum.SETUP)
try:
method = parsed_test_request.pop('method')
@@ -245,32 +251,7 @@ class Runner(object):
)
resp_obj = response.ResponseObject(resp)
# teardown hooks
teardown_hooks = test_dict.get("teardown_hooks", [])
if teardown_hooks:
self.session_context.update_test_variables("response", resp_obj)
self.do_hook_actions(teardown_hooks, "teardown")
self.http_client_session.update_last_req_resp_record(resp_obj)
# extract
extractors = test_dict.get("extract", {})
extracted_variables_mapping = resp_obj.extract_response(extractors)
self.session_context.update_session_variables(extracted_variables_mapping)
# validate
validators = test_dict.get("validate") or test_dict.get("validators") or []
validate_script = test_dict.get("validate_script", [])
if validate_script:
validators.append({
"type": "python_script",
"script": validate_script
})
validator = Validator(self.session_context, resp_obj)
try:
validator.validate(validators)
except (exceptions.ParamsError,
exceptions.ValidationFailure, exceptions.ExtractFailure):
def log_req_resp_details():
err_msg = "{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)
# log request
@@ -291,12 +272,39 @@ class Runner(object):
err_msg += "body: {}\n".format(repr(resp_obj.text))
logger.log_error(err_msg)
# teardown hooks
teardown_hooks = test_dict.get("teardown_hooks", [])
if teardown_hooks:
self.session_context.update_test_variables("response", resp_obj)
self.do_hook_actions(teardown_hooks, HookTypeEnum.TEARDOWN)
self.http_client_session.update_last_req_resp_record(resp_obj)
# extract
extractors = test_dict.get("extract", {})
try:
extracted_variables_mapping = resp_obj.extract_response(extractors)
self.session_context.update_session_variables(extracted_variables_mapping)
except (exceptions.ParamsError, exceptions.ExtractFailure):
log_req_resp_details()
raise
finally:
# get request/response data and validate results
self.meta_datas = getattr(self.http_client_session, "meta_data", {})
self.meta_datas["validators"] = validator.validation_results
# validate
validators = test_dict.get("validate") or test_dict.get("validators") or []
validate_script = test_dict.get("validate_script", [])
if validate_script:
validators.append({
"type": "python_script",
"script": validate_script
})
validator = Validator(self.session_context, resp_obj)
try:
validator.validate(validators)
except exceptions.ValidationFailure:
log_req_resp_details()
raise
return validator.validation_results
def _run_testcase(self, testcase_dict):
""" run single testcase.
@@ -374,13 +382,18 @@ class Runner(object):
self._run_testcase(test_dict)
else:
# api
validation_results = {}
try:
self._run_test(test_dict)
validation_results = self._run_test(test_dict)
except Exception:
# log exception request_type and name for locust stat
self.exception_request_type = test_dict["request"]["method"]
self.exception_name = test_dict.get("name")
raise
finally:
# get request/response data and validate results
self.meta_datas = getattr(self.http_client_session, "meta_data", {})
self.meta_datas["validators"] = validation_results
def export_variables(self, output_variables_list):
""" export current testcase variables

View File

@@ -571,6 +571,10 @@ def dump_json_file(json_data, json_file_abs_path):
except TypeError:
return str(obj)
file_foder_path = os.path.dirname(json_file_abs_path)
if not os.path.isdir(file_foder_path):
os.makedirs(file_foder_path)
try:
with io.open(json_file_abs_path, 'w', encoding='utf-8') as outfile:
if is_py2:
@@ -627,9 +631,6 @@ def prepare_dump_json_file_abs_path(project_mapping, tag_name):
test_file_name, _file_suffix = os.path.splitext(test_file)
dump_file_name = "{}.{}.json".format(test_file_name, tag_name)
if not os.path.isdir(file_foder_path):
os.makedirs(file_foder_path)
dumped_json_file_abs_path = os.path.join(file_foder_path, dump_file_name)
return dumped_json_file_abs_path

17
poetry.lock generated
View File

@@ -49,6 +49,15 @@ optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4"
version = "4.5.4"
[[package]]
category = "main"
description = "Python 3.4 Enum backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4"
marker = "python_version >= \"2.7\" and python_version < \"2.8\""
name = "enum34"
optional = false
python-versions = "*"
version = "1.1.6"
[[package]]
category = "main"
description = "Infer file type and MIME type of any file/buffer. No external dependencies."
@@ -227,7 +236,7 @@ termcolor = ["termcolor"]
watchdog = ["watchdog"]
[metadata]
content-hash = "3a262aa9fb64682ee5fcecc0e72249d0a549b78c697fce7bbabc79648d615fef"
content-hash = "7b478db27fe6f36aeed7f90b6c67efe5903fb43bb899bb66a1a65b80b8637c5a"
python-versions = "~2.7 || ^3.5"
[metadata.files]
@@ -285,6 +294,12 @@ coverage = [
{file = "coverage-4.5.4-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5"},
{file = "coverage-4.5.4.tar.gz", hash = "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c"},
]
enum34 = [
{file = "enum34-1.1.6-py2-none-any.whl", hash = "sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79"},
{file = "enum34-1.1.6-py3-none-any.whl", hash = "sha256:644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a"},
{file = "enum34-1.1.6.tar.gz", hash = "sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1"},
{file = "enum34-1.1.6.zip", hash = "sha256:2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850"},
]
filetype = [
{file = "filetype-1.0.5-py2.py3-none-any.whl", hash = "sha256:4967124d982a71700d94a08c49c4926423500e79382a92070f5ab248d44fe461"},
{file = "filetype-1.0.5.tar.gz", hash = "sha256:17a3b885f19034da29640b083d767e0f13c2dcb5dcc267945c8b6e5a5a9013c7"},

View File

@@ -1,13 +1,13 @@
[tool.poetry]
name = "httprunner"
version = "2.4.7"
version = "2.4.8"
description = "One-stop solution for HTTP(S) testing."
license = "Apache-2.0"
readme = "README.md"
authors = ["debugtalk <debugtalk@gmail.com>"]
homepage = "https://github.com/HttpRunner/HttpRunner"
repository = "https://github.com/HttpRunner/HttpRunner"
homepage = "https://github.com/httprunner/httprunner"
repository = "https://github.com/httprunner/httprunner"
documentation = "https://docs.httprunner.org"
keywords = ["HTTP", "api", "test", "requests", "locustio"]
@@ -27,7 +27,7 @@ classifiers = [
"Programming Language :: Python :: 3.8"
]
include = ["CHANGELOG.md", "httprunner/static/*"]
include = ["docs/CHANGELOG.md"]
[tool.poetry.dependencies]
python = "~2.7 || ^3.5"
@@ -40,8 +40,9 @@ colorama = "^0.4.1"
colorlog = "^4.0.2"
filetype = "^1.0.5"
jsonpath = "^0.82"
future = { version = "^0.18.1", python = "~2.7" }
sentry-sdk = "^0.13.5"
future = { version = "^0.18.1", python = "~2.7" }
enum34 = { version = "^1.1.6", python = "~2.7" }
[tool.poetry.dev-dependencies]
flask = "<1.0.0"

View File

@@ -1206,8 +1206,9 @@ class TestParser(unittest.TestCase):
}
]
}
with self.assertRaises(exceptions.VariableNotFound):
parser.parse_tests(tests_mapping)
parser.parse_tests(tests_mapping)
parse_failed_testfiles = parser.get_parse_failed_testfiles()
self.assertIn("testcase", parse_failed_testfiles)
def test_parse_tests_base_url_teststep_empty(self):
""" base_url & verify: priority test_dict > config