I'm writing a set of tools to test the behavior of a custom HTTP server: whether it is setting appropriate response codes, header fields etc. I'm using pytest to write tests.
The goal is to make requests to several resources, and then evaluate the response in multiple tests: each test should test a single aspect of the HTTP response. However, not every response is tested with every test and vice-versa.
To avoid sending the same HTTP request multiple time and reuse HTTP responses messages, I'm thinking of using pytest's fixtures, and to run the same tests on different HTTP responses I'd like to use pytest's generate test capabilities.
import pytest
import requests
def pytest_generate_tests(metafunc):
funcarglist = metafunc.cls.params[metafunc.function.__name__]
argnames = sorted(funcarglist[0])
metafunc.parametrize(argnames, [[funcargs[name] for name in argnames]
for funcargs in funcarglist])
class TestHTTP(object):
#pytest.fixture(scope="class")
def get_root(self, request):
return requests.get("http://test.com")
#pytest.fixture(scope="class")
def get_missing(self, request):
return requests.get("http://test.com/not-there")
def test_status_code(self, response, code):
assert response.status_code == code
def test_header_value(self, response, field, value):
assert response.headers[field] == value
params = {
'test_status_code': [dict(response=get_root, code=200),
dict(response=get_missing, code=404), ],
'test_header_value': [dict(response=get_root, field="content-type", value="text/html"),
dict(response=get_missing, field="content-type", value="text/html"), ],
}
The problem appears to be in defining params: dict(response=get_root, code=200) and similar definitions do not realize, I'd like to bind on the fixture and on on the actual function reference.
When running tests, I get this kinds of errors:
________________________________________________ TestHTTP.test_header_value[content-type-response0-text/html] _________________________________________________
self = <ev-question.TestHTTP object at 0x7fec8ce33d30>, response = <function TestHTTP.get_root at 0x7fec8ce8aa60>, field = 'content-type', value = 'text/html'
def test_header_value(self, response, field, value):
> assert response.headers[field] == value
E AttributeError: 'function' object has no attribute 'headers'
test_server.py:32: AttributeError
How may I convince the pytest to take the fixture value instead of the function?
No need to generate tests from fixtues, just parameterize your fixture and write regular tests for the values it returns:
import pytest
import requests
should_work = [
{
"url": "http://test.com",
"code": 200,
"fields": {"content-type": "text/html"}
},
]
should_fail = [
{
"url": "http://test.com/not-there",
"code": 404,
"fields": {"content-type": "text/html"}
},
]
should_all = should_work + should_fail
def response(request):
retval = dict(request.param) # {"url": ..., "code": ... }
retval['response'] = requests.get(request.param['url'])
return retval # {"reponse": ..., "url": ..., "code": ... }
# One fixture for working requests
response_work = pytest.fixture(scope="module", params=should_work)(response)
# One fixture for failing requests
response_fail = pytest.fixture(scope="module", params=should_fail)(response)
# One fixture for all requests
response_all = pytest.fixture(scope="module", params=should_all)(response)
# This test only requests failing fixture data
def test_status_code(response_fail):
assert response_fail['response'].status_code == response_fail['code']
# This test all requests fixture data
#pytest.mark.parametrize("field", ["content-type"])
def test_header_content_type(response_all, field):
assert response_all['response'].headers[field] == response_all['fields'][field]
Related
Using pytest, I wish to mock a function that can raise several exceptions. My app will catch the exception and create a response object, and I need to assert that each response contains the correct message, type, and in reality several other properties.
In the first instance, I have created a separate fixture to mock the function and raise each exception, and then I'm passing those fixtures in to a test with a series of events. However, because my fixtures are mocking the same function, the exception raised for every test is the same - in this case, that would be mock_exc_2, the last fixture passed to the test.
One thing I know will work is to separate the test function into multiple functions, but that seems inefficient because any future change would need to be made to multiple functions.
What is the most appropriate / efficient way to do this with with pytest?
Fixtures in 'conftest.py'
#pytest.fixture(scope='function')
def mock_exc_1(mocker):
def mock_response(self, path):
raise MissingOrgIdException()
mocker.patch('proxy.app.mcpcore.ProxyRequest._validate_org_id', mock_response)
#pytest.fixture(scope='function')
def mock_exc_2(mocker):
def mock_response(self, path):
# Parameter values are not emitted in the error message that is included in the response to the user.
raise InvalidOrgIdException('xxx', 'xxx')
mocker.patch('proxy.app.mcpcore.ProxyRequest._validate_org_id', mock_response)
# Working fixtures for 'event and 'mock_context' go here.
Broken tests in 'test_events.py'
In this scenario, only the last test is successful because both mock_exc_1 and mock_exc_2 are mocking the same function.
bad_request_args = ('event, expected',
[
(
'400-org-id-missing.json',
{
'message': 'URI path does not include an organisation ID.',
'type': 'MissingOrgIdException'
}
),
(
'400-org-id-invalid.json',
{
'message': 'Invalid organisation ID in URI path.',
'type': 'InvalidOrgIdException'
}
)
]
)
#pytest.mark.parametrize(*bad_request_args, indirect=['event'])
def test_400_events(event, expected, mock_context, mock_exc_1, mock_exc_2):
response = lambda_handler(json.loads(event), mock_context)
body = json.loads(response['body'])
assert body['errorMessage'] == expected['message']
assert body['errorType'] == expected['type']
Working tests in 'test_events.py'
Here the tests will pass, because each test is only using the fixture that raises the correct exception for the mocked function.
However, in reality there are more than two exceptions to test, and having to maintain a parameter with the parametrize args and a function to test each exception seems inefficient and prone to error when a change is made.
bad_request_args_1 = ('event, expected',
[
(
'400-org-id-missing.json',
{
'message': 'URI path does not include an organisation ID.',
'type': 'MissingOrgIdException'
}
)
]
)
bad_request_args_2 = ('event, expected',
[
(
'400-org-id-invalid.json',
{
'message': 'Invalid organisation ID in URI path.',
'type': 'InvalidOrgIdException'
}
)
]
)
#pytest.mark.parametrize(*bad_request_args_1, indirect=['event'])
def test_400_events_1(event, expected, mock_context, mock_exc_1):
response = lambda_handler(json.loads(event), mock_context)
body = json.loads(response['body'])
assert body['errorMessage'] == expected['message']
assert body['errorType'] == expected['type']
#pytest.mark.parametrize(*bad_request_args_2, indirect=['event'])
def test_400_events_2(event, expected, mock_context, mock_exc_2):
response = lambda_handler(json.loads(event), mock_context)
body = json.loads(response['body'])
assert body['errorMessage'] == expected['message']
assert body['errorType'] == expected['type']
It seems that at the moment there is no "proper" way to do this. However, it is possible to do this by using the request.getfixturevalue('fixture')
bad_request_args = ('event, expected, mock_fixture_name',
[
(
'400-org-id-missing.json',
{
'message': 'URI path does not include an organisation ID.',
'type': 'MissingOrgIdException'
},
'mock_exc_1'
),
(
'400-org-id-invalid.json',
{
'message': 'Invalid organisation ID in URI path.',
'type': 'InvalidOrgIdException'
},
'mock_exc_2'
)
]
)
#pytest.mark.parametrize(*bad_request_args, indirect=['event'])
def test_400_events(event, expected, mock_fixture_name, mock_context, request):
request.getfixturevalue(mock_fixture_name)
response = lambda_handler(json.loads(event), mock_context)
body = json.loads(response['body'])
assert body['errorMessage'] == expected['message']
assert body['errorType'] == expected['type']
I am trying to patch out the Azure Digital Twin API in my code. Currently I have achieved a way which works but is probably not the most Pythonic by nesting with patch statements.
What is the best way to rewrite this such that I can use it in multiple test functions and change the return values if needed?
def test_create_digital_twin(self):
with patch("endpoints.digital_twin.ClientSecretCredential"):
with patch("azure_digital_twin.create_digital_twin.DigitalTwinsClient.query_twins",) as mock_query:
with patch("azure_digital_twin.create_digital_twin.DigitalTwinsClient.upsert_digital_twin") as mock_upsert_twin:
with patch("azure_digital_twin.create_digital_twin.DigitalTwinsClient.upsert_relationship") as mock_upsert_relationship:
mock_query.return_value = []
mock_upsert_twin.return_value = {
"$dtId":"spc-1",
"$etag":"random",
"$model":"dtmi:digitaltwins:rec_3_3:core:Asset;1"
}
mock_upsert_relationship.return_value = {
"$relationshipId":"spc-1-hasPart-spc-2",
"$sourceId":"spc-1",
"$targetId" : "spc-2",
"$relationshipName":"hasPart"
}
response = self.client.post(
endpoint,
params={"node" : "spc-1"},
)
assert response.status_code == status.HTTP_201_CREATED
You might use an ExitStack from the contextlib module.
from contextlib import ExitStack
def test_create_digital_twin(self):
with ExitStack() as es:
def make_azure_patch(x):
return es.enter_context(patch(f'azure_digital_twin.create_digital_twin.DigitalTwinsClient.{x}'))
es.enter_context("endpoints.digital_twin.ClientSecretCredential"))
mock_query = make_patch("query_twins")
mock_upsert_twin = make_patch("upsert_digital_twin")
mock_upsert_relationship = make_patch("upsert_relationship")
mock_query.return_value = []
mock_upsert_twin.return_value = {
"$dtId":"spc-1",
"$etag":"random",
"$model":"dtmi:digitaltwins:rec_3_3:core:Asset;1"
}
mock_upsert_relationship.return_value = {
"$relationshipId":"spc-1-hasPart-spc-2",
"$sourceId":"spc-1",
"$targetId" : "spc-2",
"$relationshipName":"hasPart"
}
response = self.client.post(
endpoint,
params={"node" : "spc-1"},
)
assert response.status_code == status.HTTP_201_CREATED
make_azure_patch is just a helper function to reduce the length of the lines creating three of the individual patches.
I am preparing code for querying some endpoints. Code is ok, works quite good but it takes too much time. I would like to use Python multiprocessing module to speed up the process. My main target is to put 12 API queries to be processed in parallel. Once jobs are processed I would like to fetch the result and put them into the list of dictionaries, one response as one dictionary in the list. API response is in json format. I am new to Python and don't have experience in such kind of cases.
Code I want to run in parallel below.
def api_query_process(cloud_type, api_name, cloud_account, resource_type):
url = "xxx"
payload = {
"limit": 0,
"query": f'config from cloud.resource where cloud.type = \'{cloud_type}\' AND api.name = \'{api_name}\' AND '
f'cloud.account = \'{cloud_account}\'',
"timeRange": {
"relativeTimeType": "BACKWARD",
"type": "relative",
"value": {
"amount": 0,
"unit": "minute"
}
},
"withResourceJson": True
}
headers = {
"content-type": "application/json; charset=UTF-8",
"x-redlock-auth": api_token_input
}
response = requests.request("POST", url, json=payload, headers=headers)
result = response.json()
resource_count = len(result["data"]["items"])
if resource_count:
dictionary = dictionary_create(cloud_type, cloud_account, resource_type, resource_count)
property_list_summary.append(dictionary)
else:
dictionary = dictionary_create(cloud_type, cloud_account, resource_type, 0)
property_list_summary.append(dictionary)
Interesting problem and I think you should think about idempotency. What would happen if you hit the end-point consecutively. You can use multiprocessing with or without lock.
Without Lock:
import multiprocessing
with multiprocessing.Pool(processes=12) as pool:
jobs = []
for _ in range(12):
jobs.append(pool.apply_async(api_query_process(*args))
for job in jobs:
job.wait()
With Lock:
import multiprocessing
multiprocessing_lock = multiprocessing.Lock()
def locked_api_query_process(cloud_type, api_name, cloud_account, resource_type):
with multiprocessing_lock:
api_query_process(cloud_type, api_name, cloud_account, resource_type)
with multiprocessing.Pool(processes=12) as pool:
jobs = []
for _ in range(12):
jobs.append(pool.apply_async(locked_api_query_process(*args)))
for job in jobs:
job.wait()
Can't really do an End-2-End test but hopefully this general setup helps you get it up and running.
Since a HTTP request is an I/O Bound operation, you do not need multiprocessing. You can use threads to get a better performance. Something like the following would help.
MAX_WORKERS would say how many requests you want to send in
parallel
API_INPUTS are all the requests you want to make
Untested code sample:
from concurrent.futures import ThreadPoolExecutor
import requests
API_TOKEN = "xyzz"
MAX_WORKERS = 4
API_INPUTS = (
("cloud_type_one", "api_name_one", "cloud_account_one", "resource_type_one"),
("cloud_type_two", "api_name_two", "cloud_account_two", "resource_type_two"),
("cloud_type_three", "api_name_three", "cloud_account_three", "resource_type_three"),
)
def make_api_query(api_token_input, cloud_type, api_name, cloud_account):
url = "xxx"
payload = {
"limit": 0,
"query": f'config from cloud.resource where cloud.type = \'{cloud_type}\' AND api.name = \'{api_name}\' AND '
f'cloud.account = \'{cloud_account}\'',
"timeRange": {
"relativeTimeType": "BACKWARD",
"type": "relative",
"value": {
"amount": 0,
"unit": "minute"
}
},
"withResourceJson": True
}
headers = {
"content-type": "application/json; charset=UTF-8",
"x-redlock-auth": api_token_input
}
response = requests.request("POST", url, json=payload, headers=headers)
return response.json()
def main():
futures = []
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
for (cloud_type, api_name, cloud_account, resource_type) in API_INPUTS:
futures.append(
pool.submit(make_api_query, API_TOKEN, cloud_type, api_name, cloud_account)
)
property_list_summary = []
for future, api_input in zip(futures, API_INPUTS):
api_response = future.result()
cloud_type, api_name, cloud_account, resource_type = api_input
resource_count = len(api_response["data"]["items"])
dictionary = dictionary_create(cloud_type, cloud_account, resource_type, resource_count)
property_list_summary.append(dictionary)
I think using async functions would help a lot in speeding this up.
Your code is blocking while it waits for a response from the external API. So using more processes or threads is overkill. You dont need more resources on your end. Instead you should just make your code execute the next request instead of idling until the response arrives. This can be done using coroutines.
You could use aiohttp instead of requests, collect the individual tasks and execute them in an event loop.
Here is a small example code to run get requests, and collect the json bodies from the responses. Should be easy to adapt to your use case
from aiohttp import ClientSession
import asyncio
RESULTS = dict()
async def get_url(url, session):
async with session.get(url) as response:
print("Status:", response.status)
print("Content-type:", response.headers['content-type'])
result = await response.json()
RESULTS[url] = result
async def get_all_urls(urls):
async with ClientSession() as session:
tasks = [get_url(url, session) for url in urls]
await asyncio.gather(*tasks)
if __name__ == "__main__":
urls = [
"https://accounts.google.com/.well-known/openid-configuration",
"https://www.facebook.com/.well-known/openid-configuration/"
]
asyncio.run(get_all_urls(urls=urls))
print(RESULTS.keys())
What is the proper way to handle response classes in Flask-RESTplus?
I am experimenting with a simple GET request seen below:
i_throughput = api.model('Throughput', {
'date': fields.String,
'value': fields.String
})
i_server = api.model('Server', {
'sessionId': fields.String,
'throughput': fields.Nested(i_throughput)
})
#api.route('/servers')
class Server(Resource):
#api.marshal_with(i_server)
def get(self):
servers = mongo.db.servers.find()
data = []
for x in servers:
data.append(x)
return data
I want to return my data in as part of a response object that looks like this:
{
status: // some boolean value
message: // some custom response message
error: // if there is an error store it here
trace: // if there is some stack trace dump throw it in here
data: // what was retrieved from DB
}
I am new to Python in general and new to Flask/Flask-RESTplus. There is a lot of tutorials out there and information. One of my biggest problems is that I'm not sure what to exactly search for to get the information I need. Also how does this work with marshalling? If anyone can post good documentation or examples of excellent API's, it would be greatly appreciated.
https://blog.miguelgrinberg.com/post/customizing-the-flask-response-class
from flask import Flask, Response, jsonify
app = Flask(__name__)
class CustomResponse(Response):
#classmethod
def force_type(cls, rv, environ=None):
if isinstance(rv, dict):
rv = jsonify(rv)
return super(MyResponse, cls).force_type(rv, environ)
app.response_class = CustomResponse
#app.route('/hello', methods=['GET', 'POST'])
def hello():
return {'status': 200, 'message': 'custom_message',
'error': 'error_message', 'trace': 'trace_message',
'data': 'input_data'}
result
import requests
response = requests.get('http://localhost:5000/hello')
print(response.text)
{
"data": "input_data",
"error": "error_message",
"message": "custom_message",
"status": 200,
"trace": "trace_message"
}
I running some unit tests for a method with mock objects. In the method, attributes are set, but I can't seem to access them in the unit test. When I try I get back a mock object, not the string I am trying to access
Here is my unit test
#mock.patch("bpy.data.cameras.new")
def test_load_camera(self, mock_camera_data):
loader = self.SceneLoader(self.json_data)
self.mock_bpy.context.scene.objects.link.return_value = 5
cam_data = {"name": "camera 1",
"type": "PERSP",
"lens_length": 50.0,
"lens_unit": "MILLIMETERS",
"translation": [
4.5,
74,
67
],
"rotation": [
-0.008,
-0.002,
0.397,
0.918
]
}
data = mock.Mock()
mock_camera_data.return_value = data
loader._load_camera(cam_data)
assert mock_camera_data.called_with("Camera")
assert data.type == "PERSP"
The method I am testing is
def _load_camera(self, cam_data):
camera_data = bpy.data.cameras.new("Camera")
camera_data.type = cam_data["type"]
When I run the unit test, I get this error
AssertionError: assert <Mock name='new().type' id='140691645666360'> == 'PERSP'
E + where <Mock name='new().type' id='140691645666360'> = <Mock name='new()' id='140691645594760'>.type```
Figured it out. I needed to do configure mock so the code now looks like
data = mock.Mock()
data.configure_mock(type=None)
mock_camera_data.return_value = data
loader._load_camera(cam_data)
You need to configure/set the attribute first in the mock so that you can access it later. Now the attribute "type" can be accessed after the method has been run