change v3: add basic validation

This commit is contained in:
debugtalk
2020-04-19 17:37:23 +08:00
parent 92d71c29cf
commit 569e06e21f
7 changed files with 200 additions and 8 deletions

View File

@@ -24,7 +24,8 @@ class TestCaseRequestMethodsHardcode(TestCaseRunner):
} }
}, },
"validate": [ "validate": [
{"eq": ["status_code", 200]} {"eq": ["status_code", 200]},
{"eq": ["headers.Server", "nginx"]}
] ]
}), }),
TestStep(**{ TestStep(**{

View File

@@ -1,8 +1,8 @@
import re import re
from typing import Any, Set from typing import Any, Set, Text
from typing import Dict from typing import Dict
from httprunner.v3.exceptions import ParamsError from httprunner.v3 import exceptions
absolute_http_url_regexp = re.compile(r"^https?://", re.I) absolute_http_url_regexp = re.compile(r"^https?://", re.I)
@@ -21,7 +21,7 @@ def build_url(base_url, path):
elif base_url: elif base_url:
return "{}/{}".format(base_url.rstrip("/"), path.lstrip("/")) return "{}/{}".format(base_url.rstrip("/"), path.lstrip("/"))
else: else:
raise ParamsError("base url missed!") raise exceptions.ParamsError("base url missed!")
def regex_findall_variables(content): def regex_findall_variables(content):
@@ -58,7 +58,6 @@ def regex_findall_variables(content):
return [] return []
def extract_variables(content: Any) -> Set: def extract_variables(content: Any) -> Set:
""" extract all variables in content recursively. """ extract all variables in content recursively.
""" """

13
httprunner/v3/response.py Normal file
View File

@@ -0,0 +1,13 @@
import requests
class ResponseObject(object):
def __init__(self, resp_obj: requests.Response):
""" initialize with a requests.Response object
Args:
resp_obj (instance): requests.Response instance
"""
self.obj = resp_obj

View File

@@ -2,8 +2,12 @@ from typing import List
import requests import requests
from loguru import logger
from httprunner.v3.parser import build_url from httprunner.v3.parser import build_url
from httprunner.v3.schema import TestsConfig, TestStep from httprunner.v3.schema import TestsConfig, TestStep
from httprunner.v3.validator import Validator
from httprunner.v3.response import ResponseObject
class TestCaseRunner(object): class TestCaseRunner(object):
@@ -21,18 +25,35 @@ class TestCaseRunner(object):
return self return self
def run_step(self, step): def run_step(self, step):
request_dict = step.request.dict() logger.info(f"run step: {step.name}")
# prepare arguments
request_dict = step.request.dict()
method = request_dict.pop("method") method = request_dict.pop("method")
url_path = request_dict.pop("url") url_path = request_dict.pop("url")
url = build_url(self.config.base_url, url_path) url = build_url(self.config.base_url, url_path)
request_dict["json"] = request_dict.pop("req_json", {}) request_dict["json"] = request_dict.pop("req_json", {})
logger.info(f"{method} {url}")
logger.debug(f"request kwargs(raw): {request_dict}")
# request
session = self.session or requests.Session() session = self.session or requests.Session()
resp = session.request(method, url, **request_dict) resp = session.request(method, url, **request_dict)
def run(self): # validate
resp_obj = ResponseObject(resp)
validator = Validator(resp_obj)
validators = step.validation
validator.validate(validators)
def test_start(self):
"""main entrance"""
for step in self.teststeps: for step in self.teststeps:
step.variables.update(self.config.variables) step.variables.update(self.config.variables)
self.run_step(step) self.run_step(step)
def run(self):
"""main entrance alias for test_start"""
return self.test_start()

145
httprunner/v3/validator.py Normal file
View File

@@ -0,0 +1,145 @@
from typing import Text
import jmespath
from loguru import logger
from httprunner.v3.exceptions import ParamsError, ValidationFailure
from httprunner.v3.response import ResponseObject
def get_uniform_comparator(comparator: Text):
""" convert comparator alias to uniform name
"""
if comparator in ["eq", "equals", "==", "is"]:
return "equals"
elif comparator in ["lt", "less_than"]:
return "less_than"
elif comparator in ["le", "less_than_or_equals"]:
return "less_than_or_equals"
elif comparator in ["gt", "greater_than"]:
return "greater_than"
elif comparator in ["ge", "greater_than_or_equals"]:
return "greater_than_or_equals"
elif comparator in ["ne", "not_equals"]:
return "not_equals"
elif comparator in ["str_eq", "string_equals"]:
return "string_equals"
elif comparator in ["len_eq", "length_equals", "count_eq"]:
return "length_equals"
elif comparator in ["len_gt", "count_gt", "length_greater_than", "count_greater_than"]:
return "length_greater_than"
elif comparator in ["len_ge", "count_ge", "length_greater_than_or_equals",
"count_greater_than_or_equals"]:
return "length_greater_than_or_equals"
elif comparator in ["len_lt", "count_lt", "length_less_than", "count_less_than"]:
return "length_less_than"
elif comparator in ["len_le", "count_le", "length_less_than_or_equals",
"count_less_than_or_equals"]:
return "length_less_than_or_equals"
else:
return comparator
def uniform_validator(validator):
""" unify validator
Args:
validator (dict): validator maybe in two formats:
format1: this is kept for compatiblity with the previous versions.
{"check": "status_code", "assert": "eq", "expect": 201}
{"check": "$resp_body_success", "assert": "eq", "expect": True}
format2: recommended new version, {assert: [check_item, expected_value]}
{'eq': ['status_code', 201]}
{'eq': ['$resp_body_success', True]}
Returns
dict: validator info
{
"check": "status_code",
"expect": 201,
"assert": "equals"
}
"""
if not isinstance(validator, dict):
raise ParamsError(f"invalid validator: {validator}")
if "check" in validator and "expect" in validator:
# format1
check_item = validator["check"]
expect_value = validator["expect"]
comparator = validator.get("comparator", "eq")
elif len(validator) == 1:
# format2
comparator = list(validator.keys())[0]
compare_values = validator[comparator]
if not isinstance(compare_values, list) or len(compare_values) != 2:
raise ParamsError(f"invalid validator: {validator}")
check_item, expect_value = compare_values
else:
raise ParamsError(f"invalid validator: {validator}")
# uniform comparator, e.g. lt => less_than, eq => equals
assert_method = get_uniform_comparator(comparator)
return {
"check": check_item,
"expect": expect_value,
"assert": assert_method
}
class AssertMethods(object):
@staticmethod
def equals(actual_value, expect_value):
assert actual_value == expect_value
@staticmethod
def less_than(actual_value, expect_value):
assert actual_value < expect_value
@staticmethod
def greater_than(actual_value, expect_value):
assert actual_value > expect_value
class Validator(object):
def __init__(self, resp_obj: ResponseObject):
self.resp_meta = {
"status_code": resp_obj.obj.status_code,
"headers": resp_obj.obj.headers,
"body": resp_obj.obj.json()
}
def validate(self, validators):
for v in validators:
u_validator = uniform_validator(v)
field = u_validator["check"]
assert_method = u_validator["assert"]
expect_value = u_validator["expect"]
actual_value = jmespath.search(field, self.resp_meta)
msg = f"assert {field} {assert_method} {expect_value}"
try:
assert_func = getattr(AssertMethods, assert_method)
except AttributeError:
raise ParamsError(f"Assert Method not supported: {assert_method}")
try:
assert_func(actual_value, expect_value)
msg += " - success"
logger.info(msg)
except AssertionError:
msg += " - fail"
logger.error(msg)
raise ValidationFailure(f"assert {field}: {actual_value} {assert_method} {expect_value}")

14
poetry.lock generated
View File

@@ -173,6 +173,14 @@ MarkupSafe = ">=0.23"
[package.extras] [package.extras]
i18n = ["Babel (>=0.8)"] i18n = ["Babel (>=0.8)"]
[[package]]
category = "main"
description = "JSON Matching Expressions"
name = "jmespath"
optional = false
python-versions = "*"
version = "0.9.5"
[[package]] [[package]]
category = "main" category = "main"
description = "An XPath for JSON" description = "An XPath for JSON"
@@ -345,7 +353,7 @@ version = "1.0.1"
dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
[metadata] [metadata]
content-hash = "e1204ede1ab227bc33783b362d866c2a0b1fb8faba283216b2973e2261b0b966" content-hash = "85e388b2b80681f40a72f5ec0a1746a5a7e834c63fbfc9e2a07058bebb6f6c29"
python-versions = "^3.6" python-versions = "^3.6"
[metadata.files] [metadata.files]
@@ -470,6 +478,10 @@ jinja2 = [
{file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"}, {file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"},
{file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"}, {file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"},
] ]
jmespath = [
{file = "jmespath-0.9.5-py2.py3-none-any.whl", hash = "sha256:695cb76fa78a10663425d5b73ddc5714eb711157e52704d69be03b1a02ba4fec"},
{file = "jmespath-0.9.5.tar.gz", hash = "sha256:cca55c8d153173e21baa59983015ad0daf603f9cb799904ff057bfb8ff8dc2d9"},
]
jsonpath = [ jsonpath = [
{file = "jsonpath-0.82.tar.gz", hash = "sha256:46d3fd2016cd5b842283d547877a02c418a0fe9aa7a6b0ae344115a2c990fef4"}, {file = "jsonpath-0.82.tar.gz", hash = "sha256:46d3fd2016cd5b842283d547877a02c418a0fe9aa7a6b0ae344115a2c990fef4"},
] ]

View File

@@ -37,6 +37,7 @@ filetype = "^1.0.5"
jsonpath = "^0.82" jsonpath = "^0.82"
pydantic = "^1.4" pydantic = "^1.4"
loguru = "^0.4.1" loguru = "^0.4.1"
jmespath = "^0.9.5"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
flask = "<1.0.0" flask = "<1.0.0"