diff --git a/httprunner/__about__.py b/httprunner/__about__.py
index 9fc5c801..0cde284c 100644
--- a/httprunner/__about__.py
+++ b/httprunner/__about__.py
@@ -1,7 +1,7 @@
__title__ = 'HttpRunner'
__description__ = 'One-stop solution for HTTP(S) testing.'
__url__ = 'https://github.com/HttpRunner/HttpRunner'
-__version__ = '1.5.6'
+__version__ = '1.5.7'
__author__ = 'debugtalk'
__author_email__ = 'mail@debugtalk.com'
__license__ = 'MIT'
diff --git a/httprunner/built_in.py b/httprunner/built_in.py
index 32f94aad..e2fb0e69 100644
--- a/httprunner/built_in.py
+++ b/httprunner/built_in.py
@@ -13,7 +13,7 @@ import string
import time
from httprunner.compat import basestring, builtin_str, integer_types, str
-from httprunner.exception import ParamsError
+from httprunner.exceptions import ParamsError
from requests_toolbelt import MultipartEncoder
diff --git a/httprunner/client.py b/httprunner/client.py
index 9b2ecfd5..0f9e4d9a 100644
--- a/httprunner/client.py
+++ b/httprunner/client.py
@@ -6,7 +6,7 @@ import time
import requests
import urllib3
from httprunner import logger
-from httprunner.exception import ParamsError
+from httprunner.exceptions import ParamsError
from requests import Request, Response
from requests.exceptions import (InvalidSchema, InvalidURL, MissingSchema,
RequestException)
@@ -115,7 +115,7 @@ class HttpSession(requests.Session):
def log_print(request_response):
msg = "\n================== {} details ==================\n".format(request_response)
for key, value in self.meta_data[request_response].items():
- msg += "{:<16} : {}\n".format(key, value)
+ msg += "{:<16} : {}\n".format(key, repr(value))
logger.log_debug(msg)
# record original request info
diff --git a/httprunner/compat.py b/httprunner/compat.py
index 3753affc..ae88f4c9 100644
--- a/httprunner/compat.py
+++ b/httprunner/compat.py
@@ -8,6 +8,11 @@ This module handles import compatibility issues between Python 2 and
Python 3.
"""
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
import sys
# -------
@@ -23,15 +28,16 @@ is_py2 = (_ver[0] == 2)
#: Python 3.x?
is_py3 = (_ver[0] == 3)
-try:
- import simplejson as json
-except ImportError:
- import json
# ---------
# Specifics
# ---------
+try:
+ JSONDecodeError = json.JSONDecodeError
+except AttributeError:
+ JSONDecodeError = ValueError
+
if is_py2:
from urllib3.packages.ordered_dict import OrderedDict
@@ -42,6 +48,8 @@ if is_py2:
numeric_types = (int, long, float)
integer_types = (int, long)
+ FileNotFoundError = IOError
+
elif is_py3:
from collections import OrderedDict
@@ -51,3 +59,5 @@ elif is_py3:
basestring = (str, bytes)
numeric_types = (int, float)
integer_types = (int,)
+
+ FileNotFoundError = FileNotFoundError
diff --git a/httprunner/context.py b/httprunner/context.py
index 9decb7ca..59734b29 100644
--- a/httprunner/context.py
+++ b/httprunner/context.py
@@ -5,7 +5,7 @@ import os
import re
import sys
-from httprunner import exception, testcase, utils
+from httprunner import exceptions, logger, testcase, utils
from httprunner.compat import OrderedDict
@@ -17,6 +17,7 @@ class Context(object):
self.testset_shared_variables_mapping = OrderedDict()
self.testcase_variables_mapping = OrderedDict()
self.testcase_parser = testcase.TestcaseParser()
+ self.evaluated_validators = []
self.init_context()
def init_context(self, level='testset'):
@@ -201,13 +202,8 @@ class Context(object):
# format 1/2/3
check_value = self.eval_content(check_item)
else:
- try:
- # format 4/5
- check_value = resp_obj.extract_field(check_item)
- except exception.ParseResponseError:
- msg = "failed to extract check item from response!\n"
- msg += "response content: {}".format(resp_obj.content)
- raise exception.ParseResponseError(msg)
+ # format 4/5
+ check_value = resp_obj.extract_field(check_item)
validator["check_value"] = check_value
@@ -227,7 +223,7 @@ class Context(object):
validate_func = self.testcase_parser.get_bind_function(comparator)
if not validate_func:
- raise exception.FunctionNotFound("comparator not found: {}".format(comparator))
+ raise exceptions.FunctionNotFound("comparator not found: {}".format(comparator))
check_item = validator_dict["check"]
check_value = validator_dict["check_value"]
@@ -235,34 +231,56 @@ class Context(object):
if (check_value is None or expect_value is None) \
and comparator not in ["is", "eq", "equals", "=="]:
- raise exception.ParamsError("Null value can only be compared with comparator: eq/equals/==")
+ raise exceptions.ParamsError("Null value can only be compared with comparator: eq/equals/==")
+
+ validate_msg = "validate: {} {} {}({})".format(
+ check_item,
+ comparator,
+ expect_value,
+ type(expect_value).__name__
+ )
try:
- validator_dict["check_result"] = "passed"
- validate_func(validator_dict["check_value"], validator_dict["expect"])
+ validator_dict["check_result"] = "pass"
+ validate_func(check_value, expect_value)
+ validate_msg += "\t==> pass"
+ logger.log_debug(validate_msg)
except (AssertionError, TypeError):
- err_msg = "\n" + "\n".join([
- "\tcheck item name: %s;" % check_item,
- "\tcheck item value: %s (%s);" % (check_value, type(check_value).__name__),
- "\tcomparator: %s;" % comparator,
- "\texpected value: %s (%s)." % (expect_value, type(expect_value).__name__)
- ])
- validator_dict["check_result"] = "failed"
- raise exception.ValidationError(err_msg)
+ validate_msg += "\t==> fail"
+ validate_msg += "\n{}({}) {} {}({})".format(
+ check_value,
+ type(check_value).__name__,
+ comparator,
+ expect_value,
+ type(expect_value).__name__
+ )
+ logger.log_error(validate_msg)
+ validator_dict["check_result"] = "fail"
+ raise exceptions.ValidationFailure(validate_msg)
- def eval_validators(self, validators, resp_obj):
- """ evaluate validators with context variable mapping.
+ def validate(self, validators, resp_obj):
+ """ make validations
"""
- return [
- self.eval_check_item(
+ if not validators:
+ return
+
+ logger.log_info("start to validate.")
+ self.evaluated_validators = []
+ validate_pass = True
+
+ for validator in validators:
+ # evaluate validators with context variable mapping.
+ evaluated_validator = self.eval_check_item(
testcase.parse_validator(validator),
resp_obj
)
- for validator in validators
- ]
- def validate(self, validators):
- """ make validations
- """
- for validator_dict in validators:
- self.do_validation(validator_dict)
+ try:
+ self.do_validation(evaluated_validator)
+ except exceptions.ValidationFailure:
+ validate_pass = False
+
+ self.evaluated_validators.append(evaluated_validator)
+
+ if not validate_pass:
+ raise exceptions.ValidationFailure
diff --git a/httprunner/exception.py b/httprunner/exceptions.py
similarity index 53%
rename from httprunner/exception.py
rename to httprunner/exceptions.py
index 4bcef4d8..29b743d7 100644
--- a/httprunner/exception.py
+++ b/httprunner/exceptions.py
@@ -1,16 +1,24 @@
# encoding: utf-8
-import json
+from httprunner.compat import JSONDecodeError, FileNotFoundError
-try:
- FileNotFoundError = FileNotFoundError
-except NameError:
- FileNotFoundError = IOError
+""" failure type exceptions
+ these exceptions will mark test as failure
+"""
-try:
- JSONDecodeError = json.decoder.JSONDecodeError
-except AttributeError:
- JSONDecodeError = ValueError
+class MyBaseFailure(BaseException):
+ pass
+
+class ValidationFailure(MyBaseFailure):
+ pass
+
+class ExtractFailure(MyBaseFailure):
+ pass
+
+
+""" error type exceptions
+ these exceptions will mark test as error
+"""
class MyBaseError(BaseException):
pass
@@ -21,18 +29,12 @@ class FileFormatError(MyBaseError):
class ParamsError(MyBaseError):
pass
-class ResponseError(MyBaseError):
- pass
-
-class ParseResponseError(MyBaseError):
- pass
-
-class ValidationError(MyBaseError):
- pass
-
class NotFoundError(MyBaseError):
pass
+class FileNotFound(FileNotFoundError, NotFoundError):
+ pass
+
class FunctionNotFound(NotFoundError):
pass
diff --git a/httprunner/report.py b/httprunner/report.py
index 7cb6b172..73381570 100644
--- a/httprunner/report.py
+++ b/httprunner/report.py
@@ -90,7 +90,9 @@ def render_html_report(summary, html_report_name=None, html_report_template=None
if not os.path.isdir(report_dir_path):
os.makedirs(report_dir_path)
- for suite_summary in summary["details"]:
+ for index, suite_summary in enumerate(summary["details"]):
+ if not suite_summary.get("name"):
+ suite_summary["name"] = "test suite {}".format(index)
for record in suite_summary.get("records"):
meta_data = record['meta_data']
stringify_data(meta_data, 'request')
diff --git a/httprunner/response.py b/httprunner/response.py
index 9a2a3b89..d562769a 100644
--- a/httprunner/response.py
+++ b/httprunner/response.py
@@ -3,7 +3,7 @@
import json
import re
-from httprunner import exception, logger, testcase, utils
+from httprunner import exceptions, logger, testcase, utils
from httprunner.compat import OrderedDict, basestring
from requests.structures import CaseInsensitiveDict
from requests.models import PreparedRequest
@@ -31,7 +31,7 @@ class ResponseObject(object):
except AttributeError:
err_msg = "ResponseObject does not have attribute: {}".format(key)
logger.log_error(err_msg)
- raise exception.ParamsError(err_msg)
+ raise exceptions.ParamsError(err_msg)
def _extract_field_with_regex(self, field):
""" extract field from response content with regex.
@@ -44,11 +44,10 @@ class ResponseObject(object):
"""
matched = re.search(field, self.text)
if not matched:
- err_msg = u"Failed to extract data with regex!\n"
- err_msg += u"response content: {}\n".format(self.content)
- err_msg += u"regex: {}\n".format(field)
+ err_msg = u"Failed to extract data with regex! => {}\n".format(field)
+ err_msg += u"response body: {}\n".format(self.text)
logger.log_error(err_msg)
- raise exception.ParamsError(err_msg)
+ raise exceptions.ExtractFailure(err_msg)
return matched.group(1)
@@ -63,92 +62,120 @@ class ResponseObject(object):
"headers.content-type"
"content.person.name.first_name"
"""
+ # string.split(sep=None, maxsplit=-1) -> list of strings
+ # e.g. "content.person.name" => ["content", "person.name"]
try:
- # string.split(sep=None, maxsplit=-1) -> list of strings
- # e.g. "content.person.name" => ["content", "person.name"]
- try:
- top_query, sub_query = field.split('.', 1)
- except ValueError:
- top_query = field
- sub_query = None
-
- if top_query == "cookies":
- cookies = self.cookies
- try:
- return cookies[sub_query]
- except KeyError:
- err_msg = u"Failed to extract attribute from cookies!\n"
- err_msg += u"cookies: {}\n".format(cookies)
- err_msg += u"attribute: {}".format(sub_query)
- logger.log_error(err_msg)
- raise exception.ParamsError(err_msg)
- elif top_query == "elapsed":
- if sub_query in ["days", "seconds", "microseconds"]:
- return getattr(self.elapsed, sub_query)
- elif sub_query == "total_seconds":
- return self.elapsed.total_seconds()
- else:
- err_msg = "{}: {} is not valid timedelta attribute.\n".format(field, sub_query)
- err_msg += "elapsed only support attributes: days, seconds, microseconds, total_seconds.\n"
- logger.log_error(err_msg)
- raise exception.ParamsError(err_msg)
-
- try:
- top_query_content = getattr(self, top_query)
- except AttributeError:
- err_msg = u"Failed to extract attribute from response object: resp_obj.{}".format(top_query)
- logger.log_error(err_msg)
- raise exception.ParamsError(err_msg)
+ top_query, sub_query = field.split('.', 1)
+ except ValueError:
+ top_query = field
+ sub_query = None
+ # status_code
+ if top_query in ["status_code", "encoding", "ok", "reason", "url"]:
if sub_query:
- if not isinstance(top_query_content, (dict, CaseInsensitiveDict, list)):
- try:
- # TODO: remove compatibility for content, text
- if isinstance(top_query_content, bytes):
- top_query_content = top_query_content.decode("utf-8")
+ # status_code.XX
+ err_msg = u"Failed to extract: {}\n".format(field)
+ logger.log_error(err_msg)
+ raise exceptions.ParamsError(err_msg)
- if isinstance(top_query_content, PreparedRequest):
- top_query_content = top_query_content.__dict__
- else:
- top_query_content = json.loads(top_query_content)
- except json.decoder.JSONDecodeError:
- err_msg = u"Failed to extract data with delimiter!\n"
- err_msg += u"response content: {}\n".format(self.content)
- err_msg += u"regex: {}\n".format(field)
- logger.log_error(err_msg)
- raise exception.ParamsError(err_msg)
+ return getattr(self, top_query)
- # e.g. key: resp_headers_content_type, sub_query = "content-type"
- return utils.query_json(top_query_content, sub_query)
+ # cookies
+ elif top_query == "cookies":
+ cookies = self.cookies.get_dict()
+ if not sub_query:
+ # extract cookies
+ return cookies
+
+ try:
+ return cookies[sub_query]
+ except KeyError:
+ err_msg = u"Failed to extract cookie! => {}\n".format(field)
+ err_msg += u"response cookies: {}\n".format(cookies)
+ logger.log_error(err_msg)
+ raise exceptions.ExtractFailure(err_msg)
+
+ # elapsed
+ elif top_query == "elapsed":
+ available_attributes = u"available attributes: days, seconds, microseconds, total_seconds"
+ if not sub_query:
+ err_msg = u"elapsed is datetime.timedelta instance, attribute should also be specified!\n"
+ err_msg += available_attributes
+ logger.log_error(err_msg)
+ raise exceptions.ParamsError(err_msg)
+ elif sub_query in ["days", "seconds", "microseconds"]:
+ return getattr(self.elapsed, sub_query)
+ elif sub_query == "total_seconds":
+ return self.elapsed.total_seconds()
else:
- # e.g. key: resp_status_code, resp_content
- return top_query_content
+ err_msg = "{} is not valid datetime.timedelta attribute.\n".format(sub_query)
+ err_msg += available_attributes
+ logger.log_error(err_msg)
+ raise exceptions.ParamsError(err_msg)
- except AttributeError:
- err_msg = u"Failed to extract value from response!\n"
- err_msg += u"response content: {}\n".format(self.content)
- err_msg += u"extract field: {}\n".format(field)
+ # headers
+ elif top_query == "headers":
+ headers = self.headers
+ if not sub_query:
+ # extract headers
+ return headers
+
+ try:
+ return headers[sub_query]
+ except KeyError:
+ err_msg = u"Failed to extract header! => {}\n".format(field)
+ err_msg += u"response headers: {}\n".format(headers)
+ logger.log_error(err_msg)
+ raise exceptions.ExtractFailure(err_msg)
+
+ # response body
+ elif top_query in ["content", "text", "json"]:
+ try:
+ body = self.json
+ except exceptions.JSONDecodeError:
+ body = self.text
+
+ if not sub_query:
+ # extract response body
+ return body
+
+ if isinstance(body, (dict, list)):
+ # content = {"xxx": 123}, content.xxx
+ return utils.query_json(body, sub_query)
+ elif sub_query.isdigit():
+ # content = "abcdefg", content.3 => d
+ return utils.query_json(body, sub_query)
+ else:
+ # content = "abcdefg", content.xxx
+ err_msg = u"Failed to extract attribute from response body! => {}\n".format(field)
+ err_msg += u"response body: {}\n".format(body)
+ logger.log_error(err_msg)
+ raise exceptions.ExtractFailure(err_msg)
+
+ # 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."
logger.log_error(err_msg)
- raise exception.ParamsError(err_msg)
+ raise exceptions.ParamsError(err_msg)
def extract_field(self, field):
""" extract value from requests.Response.
"""
- msg = "extract field: {}".format(field)
+ if not isinstance(field, basestring):
+ err_msg = u"Invalid extractor! => {}\n".format(field)
+ logger.log_error(err_msg)
+ raise exceptions.ParamsError(err_msg)
- try:
- if text_extractor_regexp_compile.match(field):
- value = self._extract_field_with_regex(field)
- else:
- value = self._extract_field_with_delimiter(field)
+ msg = "extract: {}".format(field)
- msg += "\t=> {}".format(value)
- logger.log_debug(msg)
+ if text_extractor_regexp_compile.match(field):
+ value = self._extract_field_with_regex(field)
+ else:
+ value = self._extract_field_with_delimiter(field)
- # TODO: unify ParseResponseError type
- except (exception.ParseResponseError, TypeError):
- logger.log_error("failed to extract field: {}".format(field))
- raise
+ msg += "\t=> {}".format(value)
+ logger.log_debug(msg)
return value
@@ -171,9 +198,6 @@ class ResponseObject(object):
extract_binds_order_dict = utils.convert_to_order_dict(extractors)
for key, field in extract_binds_order_dict.items():
- if not isinstance(field, basestring):
- raise exception.ParamsError("invalid extractors in testcase!")
-
extracted_variables_mapping[key] = self.extract_field(field)
return extracted_variables_mapping
diff --git a/httprunner/runner.py b/httprunner/runner.py
index fbf8031f..0174bb9c 100644
--- a/httprunner/runner.py
+++ b/httprunner/runner.py
@@ -2,7 +2,7 @@
from unittest.case import SkipTest
-from httprunner import exception, logger, response, utils
+from httprunner import exceptions, logger, response, utils
from httprunner.client import HttpSession
from httprunner.context import Context
@@ -11,7 +11,6 @@ class Runner(object):
def __init__(self, config_dict=None, http_client_session=None):
self.http_client_session = http_client_session
- self.evaluated_validators = []
self.context = Context()
config_dict = config_dict or {}
@@ -155,7 +154,15 @@ class Runner(object):
method = parsed_request.pop('method')
group_name = parsed_request.pop("group", None)
except KeyError:
- raise exception.ParamsError("URL or METHOD missed!")
+ raise exceptions.ParamsError("URL or METHOD missed!")
+
+ # TODO: move method validation to json schema
+ valid_methods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
+ if method.upper() not in valid_methods:
+ err_msg = u"Invalid HTTP method! => {}\n".format(method)
+ err_msg += "Available HTTP methods: {}".format("/".join(valid_methods))
+ logger.log_error(err_msg)
+ raise exceptions.ParamsError(err_msg)
logger.log_info("{method} {url}".format(method=method, url=url))
logger.log_debug("request kwargs(raw): {kwargs}".format(kwargs=parsed_request))
@@ -183,22 +190,21 @@ class Runner(object):
# validate
validators = testcase_dict.get("validate", []) or testcase_dict.get("validators", [])
try:
- self.evaluated_validators = self.context.eval_validators(validators, resp_obj)
- self.context.validate(self.evaluated_validators)
- except (exception.ParamsError, exception.ResponseError, \
- exception.ValidationError, exception.ParseResponseError):
+ self.context.validate(validators, resp_obj)
+ except (exceptions.ParamsError, \
+ exceptions.ValidationFailure, exceptions.ExtractFailure):
# log request
err_req_msg = "request: \n"
err_req_msg += "headers: {}\n".format(parsed_request.pop("headers", {}))
for k, v in parsed_request.items():
- err_req_msg += "{}: {}\n".format(k, v)
+ err_req_msg += "{}: {}\n".format(k, repr(v))
logger.log_error(err_req_msg)
# log response
err_resp_msg = "response: \n"
err_resp_msg += "status_code: {}\n".format(resp_obj.status_code)
err_resp_msg += "headers: {}\n".format(resp_obj.headers)
- err_resp_msg += "content: {}\n".format(resp_obj.content)
+ err_resp_msg += "body: {}\n".format(repr(resp_obj.text))
logger.log_error(err_resp_msg)
raise
diff --git a/httprunner/task.py b/httprunner/task.py
index c65a7809..437ae6d3 100644
--- a/httprunner/task.py
+++ b/httprunner/task.py
@@ -4,7 +4,7 @@ import copy
import sys
import unittest
-from httprunner import exception, logger, runner, testcase, utils
+from httprunner import exceptions, logger, runner, testcase, utils
from httprunner.compat import is_py3
from httprunner.report import (HtmlTestResult, get_platform, get_summary,
render_html_report)
@@ -25,10 +25,12 @@ class TestCase(unittest.TestCase):
"""
try:
self.test_runner.run_test(self.testcase_dict)
+ except exceptions.MyBaseFailure as ex:
+ self.fail(repr(ex))
finally:
if hasattr(self.test_runner.http_client_session, "meta_data"):
self.meta_data = self.test_runner.http_client_session.meta_data
- self.meta_data["validators"] = self.test_runner.evaluated_validators
+ self.meta_data["validators"] = self.test_runner.context.evaluated_validators
self.test_runner.http_client_session.init_meta_data()
@@ -106,7 +108,7 @@ class TestSuite(unittest.TestSuite):
self.testcase_parser.update_binded_variables(variables)
try:
testcase_name = self.testcase_parser.eval_content_with_bindings(testcase_dict["name"])
- except (AssertionError, exception.ParamsError):
+ except (AssertionError, exceptions.ParamsError):
logger.log_warning("failed to eval testcase name: {}".format(testcase_dict["name"]))
testcase_name = testcase_dict["name"]
self.test_runner_list.append((test_runner, variables))
@@ -189,7 +191,7 @@ def init_test_suites(path_or_testsets, mapping=None, http_client_session=None):
mapping = mapping or {}
if not testsets:
- raise exception.TestcaseNotFound
+ raise exceptions.TestcaseNotFound
if isinstance(testsets, dict):
testsets = [testsets]
@@ -236,7 +238,7 @@ class HttpRunner(object):
"""
try:
test_suite_list = init_test_suites(path_or_testsets, mapping)
- except exception.TestcaseNotFound:
+ except exceptions.TestcaseNotFound:
logger.log_error("Testcases not found in {}".format(path_or_testsets))
sys.exit(1)
@@ -301,7 +303,7 @@ class LocustTask(object):
for test in test_suite:
try:
test.runTest()
- except exception.MyBaseError as ex:
+ except exceptions.MyBaseError as ex:
from locust.events import request_failure
request_failure.fire(
request_type=test.testcase_dict.get("request", {}).get("method"),
diff --git a/httprunner/templates/default_report_template.html b/httprunner/templates/default_report_template.html
index 4c7b4441..78d36986 100644
--- a/httprunner/templates/default_report_template.html
+++ b/httprunner/templates/default_report_template.html
@@ -187,69 +187,91 @@
{% for test_suite_summary in details %}
{% set suite_index = loop.index %}
{{test_suite_summary.name}}
-
-
- | base_url |
- {{test_suite_summary.base_url}} |
-
- parameters & output
-
-
-
-
- |
-
- | TOTAL: {{test_suite_summary.stat.testsRun}} |
- SUCCESS: {{test_suite_summary.stat.successes}} |
- FAILED: {{test_suite_summary.stat.failures}} |
- ERROR: {{test_suite_summary.stat.errors}} |
- SKIPPED: {{test_suite_summary.stat.skipped}} |
-
-
- | Status |
- Name |
- Response Time |
- Detail |
-
-
- {% for record in test_suite_summary.records %}
- {% set record_index = "{}_{}".format(suite_index, loop.index) %}
-
- | {{record.status}}
- | {{record.name}} |
- {{ record.meta_data.response.response_time_ms }} ms |
-
-
- log
- |
content