Merge pull request #3 from debugtalk/master

lastest  pull from debugtalk
This commit is contained in:
firefoxwang
2017-11-01 20:14:38 +08:00
committed by GitHub
24 changed files with 216 additions and 96 deletions

View File

@@ -38,7 +38,7 @@ To ensure the installation or upgrade is successful, you can execute command `at
```text
$ ate -V
ApiTestEngine version: 0.7.5
ApiTestEngine version: 0.7.7
```
Execute the command `ate -h` to view command help.
@@ -98,9 +98,9 @@ And here is testset example of typical scenario: get `token` at the beginning, a
app_version: $app_version
json:
sign: ${get_sign($user_agent, $device_sn, $os_platform, $app_version)}
extractors:
extract:
- token: content.token
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "content.token", "comparator": "len_eq", "expected": 16}
@@ -114,7 +114,7 @@ And here is testset example of typical scenario: get `token` at the beginning, a
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 201}
- {"check": "content.success", "comparator": "eq", "expected": true}
```

View File

@@ -1 +1 @@
__version__ = '0.7.5'
__version__ = '0.7.7'

View File

@@ -54,7 +54,7 @@ class HttpSession(requests.Session):
else:
raise ParamsError("base url missed!")
def request(self, method, url, **kwargs):
def request(self, method, url, name=None, **kwargs):
"""
Constructs and sends a :py:class:`requests.Request`.
Returns :py:class:`requests.Response` object.
@@ -63,6 +63,8 @@ class HttpSession(requests.Session):
method for the new :class:`Request` object.
:param url:
URL for the new :class:`Request` object.
:param name: (optional)
Placeholder, make compatible with Locust's HttpSession
:param params: (optional)
Dictionary or bytes to be sent in the query string for the :class:`Request`.
:param data: (optional)

View File

@@ -34,7 +34,7 @@ class Context(object):
self.testcase_variables_mapping = copy.deepcopy(self.testset_shared_variables_mapping)
self.testcase_parser.bind_functions(self.testcase_functions_config)
self.testcase_parser.bind_variables(self.testcase_variables_mapping)
self.testcase_parser.update_binded_variables(self.testcase_variables_mapping)
if level == "testset":
self.import_module_items(["ate.built_in"], "testset")
@@ -117,7 +117,17 @@ class Context(object):
self.testset_shared_variables_mapping[variable_name] = variable_evale_value
self.testcase_variables_mapping[variable_name] = variable_evale_value
self.testcase_parser.bind_variables(self.testcase_variables_mapping)
self.testcase_parser.update_binded_variables(self.testcase_variables_mapping)
def bind_extracted_variables(self, variables):
""" bind extracted variables to testset context
@param (OrderDict) variables
extracted value do not need to evaluate.
"""
for variable_name, value in variables.items():
self.testset_shared_variables_mapping[variable_name] = value
self.testcase_variables_mapping[variable_name] = value
self.testcase_parser.update_binded_variables(self.testcase_variables_mapping)
def __update_context_functions_config(self, level, config_mapping):
"""

View File

@@ -1,6 +1,5 @@
#coding: utf-8
import zmq
import os
from locust import HttpLocust, TaskSet, task
from ate import runner, exception

View File

@@ -1,6 +1,11 @@
import logging
import re
from collections import OrderedDict
from ate import exception, utils
from requests.structures import CaseInsensitiveDict
text_extractor_regexp_compile = re.compile(r".*\(.*\).*")
class ResponseObject(object):
@@ -10,23 +15,45 @@ class ResponseObject(object):
@param (requests.Response instance) resp_obj
"""
self.resp_obj = resp_obj
self.resp_text = resp_obj.text
self.resp_body = self.parsed_body()
def parsed_body(self):
try:
return self.resp_obj.json()
except ValueError:
return self.resp_obj.text
return self.resp_text
def parsed_dict(self):
return {
'status_code': self.resp_obj.status_code,
'headers': self.resp_obj.headers,
'body': self.parsed_body()
'body': self.resp_body
}
def extract_field(self, field, delimiter='.'):
""" extract field from requests.Response
@param (str) field of requests.Response object, and may be joined by delimiter
def _extract_field_with_regex(self, field):
""" extract field from response content with regex.
requests.Response body could be json or html text.
@param (str) field should only be regex string that matched r".*\(.*\).*"
e.g.
self.resp_text: "LB123abcRB789"
field: "LB[\d]*(.*)RB[\d]*"
return: abc
"""
matched = re.search(field, self.resp_text)
if not matched:
err_msg = "Extractor error: failed to extract data with regex!\n"
err_msg += "response body: {}\n".format(self.resp_text)
err_msg += "regex: {}\n".format(field)
logging.error(err_msg)
raise exception.ParamsError(err_msg)
return matched.group(1)
def _extract_field_with_delimiter(self, field):
""" response content could be json or html text.
@param (str) field should be string joined by delimiter.
e.g.
"status_code"
"content"
"headers.content-type"
@@ -36,28 +63,47 @@ class ResponseObject(object):
# string.split(sep=None, maxsplit=-1) -> list of strings
# e.g. "content.person.name" => ["content", "person.name"]
try:
top_query, sub_query = field.split(delimiter, 1)
top_query, sub_query = field.split('.', 1)
except ValueError:
top_query = field
sub_query = None
if top_query in ["body", "content", "text"]:
json_content = self.parsed_body()
top_query_content = self.parsed_body()
else:
json_content = getattr(self.resp_obj, top_query)
top_query_content = getattr(self.resp_obj, top_query)
if sub_query:
if not isinstance(top_query_content, (dict, CaseInsensitiveDict, list)):
err_msg = "Extractor error: failed to extract data with regex!\n"
err_msg += "response: {}\n".format(self.parsed_dict())
err_msg += "regex: {}\n".format(field)
logging.error(err_msg)
raise exception.ParamsError(err_msg)
# e.g. key: resp_headers_content_type, sub_query = "content-type"
return utils.query_json(json_content, sub_query)
return utils.query_json(top_query_content, sub_query)
else:
# e.g. key: resp_status_code, resp_content
return json_content
return top_query_content
except AttributeError:
raise exception.ParseResponseError("failed to extract bind variable in response!")
err_msg = "Failed to extract value from response!\n"
err_msg += "response: {}\n".format(self.parsed_dict())
err_msg += "extract field field: {}\n".format(field)
logging.error(err_msg)
raise exception.ParamsError(err_msg)
def extract_field(self, field):
""" extract value from requests.Response.
"""
if text_extractor_regexp_compile.match(field):
return self._extract_field_with_regex(field)
else:
return self._extract_field_with_delimiter(field)
def extract_response(self, extractors):
""" extract content from requests.Response
""" extract value from requests.Response and store in OrderedDict.
@param (list) extractors
[
{"resp_status_code": "status_code"},

View File

@@ -1,3 +1,4 @@
import logging
from collections import OrderedDict
from ate import exception, response, testcase, utils
@@ -84,8 +85,8 @@ class Runner(object):
},
"body": '{"name": "user", "password": "123456"}'
},
"extractors": [], # optional
"validators": [], # optional
"extract": [], # optional
"validate": [], # optional
"setup": [], # optional
"teardown": [] # optional
}
@@ -96,14 +97,16 @@ class Runner(object):
try:
url = parsed_request.pop('url')
method = parsed_request.pop('method')
group_name = parsed_request.pop("group", None)
except KeyError:
raise exception.ParamsError("URL or METHOD missed!")
run_times = int(testcase.get("times", 1))
extractors = testcase.get("extractors") \
or testcase.get("extractor") \
extractors = testcase.get("extract") \
or testcase.get("extractors") \
or testcase.get("extract_binds", [])
validators = testcase.get("validators", [])
validators = testcase.get("validate") \
or testcase.get("validators", [])
setup_actions = testcase.get("setup", [])
teardown_actions = testcase.get("teardown", [])
@@ -114,13 +117,24 @@ class Runner(object):
for _ in range(run_times):
setup_teardown(setup_actions)
resp = self.http_client_session.request(url=url, method=method, **parsed_request)
resp = self.http_client_session.request(
method,
url,
name=group_name,
**parsed_request
)
resp_obj = response.ResponseObject(resp)
extracted_variables_mapping = resp_obj.extract_response(extractors)
self.context.bind_variables(extracted_variables_mapping, level="testset")
self.context.bind_extracted_variables(extracted_variables_mapping)
resp_obj.validate(validators, self.context.get_testcase_variables_mapping())
try:
resp_obj.validate(validators, self.context.get_testcase_variables_mapping())
except (exception.ParamsError, exception.ResponseError, exception.ValidationError):
logging.error("Exception occured.")
logging.error("HTTP request kwargs: \n{}".format(parsed_request))
logging.error("HTTP response content: \n{}".format(resp.text))
raise
setup_teardown(teardown_actions)
@@ -144,8 +158,8 @@ class Runner(object):
"name": "testcase description",
"variables": [], # optional, override
"request": {},
"extractors": {}, # optional
"validators": {} # optional
"extract": {}, # optional
"validate": {} # optional
},
testcase12
]

View File

@@ -330,11 +330,11 @@ def substitute_variables_with_mapping(content, mapping):
class TestcaseParser(object):
def __init__(self, variables={}, functions={}, file_path=None):
self.bind_variables(variables)
self.update_binded_variables(variables)
self.bind_functions(functions)
self.file_path = file_path
def bind_variables(self, variables):
def update_binded_variables(self, variables):
""" bind variables to current testcase parser
@param (dict) variables, variables binds mapping
{

View File

@@ -24,7 +24,7 @@ Suppose we get the following HTTP response.
}
```
In `extractors` and `validators`, we can do chain operation to extract data field in HTTP response.
In `extract` and `validate`, we can do chain operation to extract data field in HTTP response.
For instance, if we want to get `Content-Type` in response headers, then we can specify `headers.content-type`; if we want to get `first_name` in response content, we can specify `content.person.name.first_name`.
@@ -46,10 +46,10 @@ content.person.cities.1
```
```yaml
extractors:
extract:
- content_type: headers.content-type
- first_name: content.person.name.first_name
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "headers.content-type", "expected": "application/json"}
- {"check": "headers.content-length", "comparator": "gt", "expected": 40}

View File

@@ -65,7 +65,7 @@ Open your favorite text editor and you can write test cases like this.
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 201}
- {"check": "content.success", "comparator": "eq", "expected": true}
```
@@ -120,9 +120,9 @@ To fix this problem, we should correlate `token` field in the second API test ca
app_version: 2.8.6
json:
sign: 19067cf712265eb5426db8d3664026c1ccea02b9
extractors:
extract:
- token: content.token
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "content.token", "comparator": "len_eq", "expected": 16}
@@ -137,12 +137,12 @@ To fix this problem, we should correlate `token` field in the second API test ca
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 201}
- {"check": "content.success", "comparator": "eq", "expected": true}
```
As you see, the `token` field is no longer hardcoded, instead it is extracted from the first API request with `extractors` mechanism. In the meanwhile, it is assigned to `token` variable, which can be referenced by the subsequent API requests.
As you see, the `token` field is no longer hardcoded, instead it is extracted from the first API request with `extract` mechanism. In the meanwhile, it is assigned to `token` variable, which can be referenced by the subsequent API requests.
Now we save the test cases to [`quickstart-demo-rev-1.yml`][quickstart-demo-rev-1] and rerun it, and we will find that both API requests to be successful.
@@ -202,9 +202,9 @@ And then, we can revise our demo test case and reference the functions. Suppose
app_version: $app_version
json:
sign: ${get_sign($user_agent, $device_sn, $os_platform, $app_version)}
extractors:
extract:
- token: content.token
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "content.token", "comparator": "len_eq", "expected": 16}
@@ -219,7 +219,7 @@ And then, we can revise our demo test case and reference the functions. Suppose
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 201}
- {"check": "content.success", "comparator": "eq", "expected": true}
```
@@ -266,9 +266,9 @@ To handle this case, overall `config` block is supported in `ApiTestEngine`. If
app_version: $app_version
json:
sign: ${get_sign($user_agent, $device_sn, $os_platform, $app_version)}
extractors:
extract:
- token: content.token
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "content.token", "comparator": "len_eq", "expected": 16}
@@ -282,7 +282,7 @@ To handle this case, overall `config` block is supported in `ApiTestEngine`. If
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 201}
- {"check": "content.success", "comparator": "eq", "expected": true}
```

View File

@@ -10,7 +10,7 @@
app_version: 2.8.6
json:
sign: 19067cf712265eb5426db8d3664026c1ccea02b9
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "content.token", "comparator": "len_eq", "expected": 16}
@@ -25,6 +25,6 @@
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 201}
- {"check": "content.success", "comparator": "eq", "expected": true}

View File

@@ -10,9 +10,9 @@
app_version: 2.8.6
json:
sign: 19067cf712265eb5426db8d3664026c1ccea02b9
extractors:
extract:
- token: content.token
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "content.token", "comparator": "len_eq", "expected": 16}
@@ -27,6 +27,6 @@
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 201}
- {"check": "content.success", "comparator": "eq", "expected": true}

View File

@@ -15,9 +15,9 @@
app_version: $app_version
json:
sign: ${get_sign($user_agent, $device_sn, $os_platform, $app_version)}
extractors:
extract:
- token: content.token
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "content.token", "comparator": "len_eq", "expected": 16}
@@ -32,6 +32,6 @@
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 201}
- {"check": "content.success", "comparator": "eq", "expected": true}

View File

@@ -22,9 +22,9 @@
app_version: $app_version
json:
sign: ${get_sign($user_agent, $device_sn, $os_platform, $app_version)}
extractors:
extract:
- token: content.token
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "content.token", "comparator": "len_eq", "expected": 16}
@@ -38,6 +38,6 @@
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 201}
- {"check": "content.success", "comparator": "eq", "expected": true}

View File

@@ -10,7 +10,7 @@
app_version: $app_version
json:
sign: ${get_sign($user_agent, $device_sn, $os_platform, $app_version)}
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "content.token", "comparator": "len_eq", "expected": 16}

View File

@@ -16,12 +16,12 @@
"sign": "f1219719911caae89ccc301679857ebfda115ca2"
}
},
"extractors": [
"extract": [
{
"token": "content.token"
}
],
"validators": [
"validate": [
{"check": "status_code", "comparator": "eq", "expected": 200},
{"check": "content.token", "comparator": "len_eq", "expected": 16}
]
@@ -43,7 +43,7 @@
"password": "123456"
}
},
"validators": [
"validate": [
{"check": "status_code", "comparator": "eq", "expected": 201},
{"check": "content.success", "comparator": "eq", "expected": true}
]
@@ -65,7 +65,7 @@
"password": "123456"
}
},
"validators": [
"validate": [
{"check": "status_code", "comparator": "eq", "expected": 500},
{"check": "content.success", "comparator": "eq", "expected": false}
]

View File

@@ -11,9 +11,9 @@
app_version: '2.8.6'
json:
sign: f1219719911caae89ccc301679857ebfda115ca2
extractors:
extract:
- token: content.token
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "content.token", "comparator": "len_eq", "expected": 16}
@@ -29,7 +29,7 @@
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 201}
- {"check": "content.success", "comparator": "eq", "expected": true}
@@ -45,6 +45,6 @@
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 500}
- {"check": "content.success", "comparator": "eq", "expected": false}

View File

@@ -16,20 +16,20 @@
- test:
name: get token
api: get_token($user_agent, $device_sn, $os_platform, $app_version)
extractors:
extract:
- token: content.token
- test:
name: reset all users
api: reset_all($token)
validators:
validate:
- {"check": "status_code", "expected": 200}
- {"check": "content.success", "expected": true}
- test:
name: get user that does not exist
api: get_user(1000, $token)
validators:
validate:
- {"check": "status_code", "expected": 404}
- {"check": "content.success", "expected": false}
@@ -39,14 +39,14 @@
- user_name: "user1"
- user_password: "123456"
api: create_user(1000, $user_name, $user_password, $token)
validators:
validate:
- {"check": "status_code", "expected": 201}
- {"check": "content.success", "expected": true}
- test:
name: get user that has been created
api: get_user(1000, $token)
validators:
validate:
- {"check": "status_code", "expected": 200}
- {"check": "content.success", "expected": true}
- {"check": "content.data.password", "expected": "123456"}
@@ -57,7 +57,7 @@
- user_name: "user1"
- user_password: "123456"
api: create_user(1000, $user_name, $user_password, $token)
validators:
validate:
- {"check": "status_code", "expected": 500}
- {"check": "content.success", "expected": false}
@@ -67,14 +67,14 @@
- user_name: "user1"
- user_password: "654321"
api: update_user(1000, $user_name, $user_password, $token)
validators:
validate:
- {"check": "status_code", "expected": 200}
- {"check": "content.success", "expected": true}
- test:
name: get user that has been updated
api: get_user(1000, $token)
validators:
validate:
- {"check": "status_code", "expected": 200}
- {"check": "content.success", "expected": true}
- {"check": "content.data.password", "expected": "654321"}
@@ -82,21 +82,21 @@
- test:
name: get users
api: get_users($token)
validators:
validate:
- {"check": "status_code", "expected": 200}
- {"check": "content.count", "expected": 1}
- test:
name: delete user that exists
api: delete_user(1000, $token)
validators:
validate:
- {"check": "status_code", "expected": 200}
- {"check": "content.success", "expected": true}
- test:
name: get users
api: get_users($token)
validators:
validate:
- {"check": "status_code", "expected": 200}
- {"check": "content.count", "expected": 0}
@@ -106,13 +106,13 @@
- user_name: "user1"
- user_password: "123456"
api: create_user(1000, $user_name, $user_password, $token)
validators:
validate:
- {"check": "status_code", "expected": 201}
- {"check": "content.success", "expected": true}
- test:
name: get users
api: get_users($token)
validators:
validate:
- {"check": "status_code", "expected": 200}
- {"check": "content.count", "expected": 1}

View File

@@ -25,9 +25,9 @@
app_version: $app_version
json:
sign: ${get_sign($user_agent, $device_sn, $os_platform, $app_version)}
extractors:
extract:
- token: content.token
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "content.token", "comparator": "len_eq", "expected": 16}
@@ -44,7 +44,7 @@
json:
name: $user_name
password: $user_password
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 201}
- {"check": "content.success", "comparator": "eq", "expected": true}
@@ -58,6 +58,6 @@
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 500}
- {"check": "content.success", "comparator": "eq", "expected": false}

View File

@@ -35,9 +35,9 @@
app_version: $app_version
json:
sign: ${get_sign_lambda($user_agent, $device_sn, $os_platform, $app_version)}
extractors:
extract:
- token: content.token
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "content.token", "comparator": "len_eq", "expected": 16}
@@ -54,7 +54,7 @@
json:
name: $user_name
password: $user_password
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 201}
- {"check": "content.success", "comparator": "eq", "expected": true}
@@ -68,6 +68,6 @@
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 500}
- {"check": "content.success", "comparator": "eq", "expected": false}

View File

@@ -26,9 +26,9 @@
app_version: $app_version
json:
sign: $sign
extractors:
extract:
- token: content.token
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 200}
- {"check": "content.token", "comparator": "len_eq", "expected": 16}
@@ -45,7 +45,7 @@
json:
name: $user_name
password: $user_password
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 201}
- {"check": "content.success", "comparator": "eq", "expected": true}
@@ -59,6 +59,6 @@
json:
name: "user1"
password: "123456"
validators:
validate:
- {"check": "status_code", "comparator": "eq", "expected": 500}
- {"check": "content.success", "comparator": "eq", "expected": false}

View File

@@ -149,6 +149,55 @@ class TestResponse(ApiServerUnittest):
"abc"
)
def test_extract_text_response(self):
resp = requests.post(
url="http://127.0.0.1:5000/customize-response",
json={
'headers': {
'Content-Type': "application/json"
},
'body': "LB123abcRB789"
}
)
extract_binds_list = [
{"resp_content_key1": "LB123(.*)RB789"},
{"resp_content_key2": "LB[\d]*(.*)RB[\d]*"},
{"resp_content_key3": "LB[\d]*(.*)9"}
]
resp_obj = response.ResponseObject(resp)
extract_binds_dict = resp_obj.extract_response(extract_binds_list)
self.assertEqual(
extract_binds_dict["resp_content_key1"],
"abc"
)
self.assertEqual(
extract_binds_dict["resp_content_key2"],
"abc"
)
self.assertEqual(
extract_binds_dict["resp_content_key3"],
"abcRB78"
)
def test_extract_text_response_exception(self):
resp = requests.post(
url="http://127.0.0.1:5000/customize-response",
json={
'headers': {
'Content-Type': "application/json"
},
'body': "LB123abcRB789"
}
)
extract_binds_list = [
{"resp_content_key1": "LB123.*RB789"}
]
resp_obj = response.ResponseObject(resp)
with self.assertRaises(exception.ParamsError):
resp_obj.extract_response(extract_binds_list)
def test_extract_response_empty(self):
resp = requests.post(
url="http://127.0.0.1:5000/customize-response",
@@ -174,7 +223,7 @@ class TestResponse(ApiServerUnittest):
{"resp_content_body": "content.abc"}
]
resp_obj = response.ResponseObject(resp)
with self.assertRaises(exception.ResponseError):
with self.assertRaises(exception.ParamsError):
resp_obj.extract_response(extract_binds_list)
def test_validate(self):

View File

@@ -53,10 +53,10 @@ class TestRunner(ApiServerUnittest):
"sign": "f1219719911caae89ccc301679857ebfda115ca2"
}
},
"extractors": [
"extract": [
{"token": "content.token"}
],
"validators": [
"validate": [
{"check": "status_code", "comparator": "eq", "expected": 205},
{"check": "content.token", "comparator": "len_eq", "expected": 19}
]

View File

@@ -421,7 +421,7 @@ class TestcaseParserUnittest(unittest.TestCase):
self.assertIn("request", testsets_list[0]["config"])
self.assertIn("request", testsets_list[0]["testcases"][0])
self.assertIn("url", testsets_list[0]["testcases"][0]["request"])
self.assertIn("validators", testsets_list[0]["testcases"][0])
self.assertIn("validate", testsets_list[0]["testcases"][0])
def test_substitute_variables_with_mapping(self):
content = {