Mock function used in flask request handler - python

I'm trying to mock the upload_image function called in a flask request handler from my test file.
However, with my current implementation, the original function is still called in the request handler and does not return the mocked return value. How can I mock this function from my test.py file?
The code:
tests/test.py
import os
from unittest.mock import patch
def test_image_upload(test_client):
with patch("app.utils.upload_image", 'test_path'):
test_file_path = os.path.dirname(os.path.abspath(__file__)) + '/test_file.png'
f = open(test_file_path, 'rb')
data = {
'num-files': 1,
'test_file.png': f
}
response = test_client.post('/file/upload', data=data)
assert response.status_code == 200
app/files/handler.py
from utils import upload_image
#app.route('/file/upload', methods=['POST'])
def upload_file():
# should be returning 'test_path', but is actually uploading the file and returning a URL
file_url = upload_image(image)
return jsonify({'file_url': file_url})

Related

Unit Testing Replace remote API Server with predefined response

So, I have a server running FastAPI which will make a API call to a remote API upon request.
I am developping unit-testing for this application, but here comes the question:
Can I, for the purpose of the test, replace a legit remote API server response by a predefined response ?
Example of the tests runned:
from fastapi.testclient import TestClient
from web_api import app
client = TestClient(app)
def test_get_root():
response = client.get('/')
assert response.status_code == 200
assert response.json() == {"running": True}
And the my server
from fastapi import FastAPI
app = FastAPI()
#app.get("/")
def home():
return {"running": True}
This is a simple example, but on other endpoints of my API I would call an external remote API
def call_api(self, endpoint:str, params:dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response
Because I want to test the response of MY API, I would like to replace the remote API with a predefined response.
Also, one user request can end-up in multiple background API requests with transformed pieces of data.
Edit
Here are some more details on the structure of the application:
#app.get("/stuff/.......",
# lots of params
)
def get_stuff_from_things(stuff:list, params):
api = API(api_key=...)
# Do some stuff with the params
things = generate_things_list(params)
api.search_things(params)
# Check the result
# do some other stuff
return some_response
class API:
BASE_URL = 'https://api.example.com/'
def search_things(self, params):
# Do some stuff
# like putting stuff in the params
for s in stuff:
s.update(self.get_thing(params)) # -> get_thing()
# Do some more stuff
return stuff
# get_thing <- search_things
def get_thing(self, params...):
# Some stuff
results = self.call_api('something', params) # -> call_api()
json = results.json()
# Some more stuff
things = []
for thing in json['things']:
t = Thing(thing)
things.append(t)
return things
# call_api <- get_thing
def call_api(self, endpoint:str, params:dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
self.last_response = response
return response
Nb. That is pseudo-code, I simplified the functions by removing the parameters, etc.
I hope it is clear, thanks for your help.
A complex API method might look like this (please pay attention to the depends mechanism - it is crucial):
import urllib
import requests
from fastapi import FastAPI, Depends
app = FastAPI()
# this can be in a different file
class RemoteCallWrapper:
def call_api(self, baseurl: str, endpoint: str, params: dict):
url = baseurl + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response
#app.get("/complex_api")
def calls_other_api(remote_call_wrapper=Depends(RemoteCallWrapper)):
response = remote_call_wrapper.call_api("https://jsonplaceholder.typicode.com",
"/todos/1", None)
return {"result": response.json()}
Now, we wish to replace the remote call class. I wrote a helper library that simplifies the replacement for tests - pytest-fastapi-deps:
from fastapi.testclient import TestClient
from mock.mock import Mock
from requests import Response
from web_api import app, RemoteCallWrapper
client = TestClient(app)
class MyRemoteCallWrapper:
def call_api(self, baseurl: str, endpoint: str, params: dict):
the_response = Mock(spec=Response)
the_response.json.return_value = {"my": "response"}
return the_response
def test_get_root(fastapi_dep):
with fastapi_dep(app).override({RemoteCallWrapper: MyRemoteCallWrapper}):
response = client.get('/complex_api')
assert response.status_code == 200
assert response.json() == {"result": {"my": "response"}}
You override the RemoteCallWrapper with your MyRemoteCallWrapper implementation for the test, which has the same spec.
As asserted - the response changed to our predefined response.
It sounds like you'd want to mock your call_api() function.
With a small modification to call_api() (returning the result of .json()), you can easily mock the whole function while calling the endpoint in your tests.
I'll use two files, app.py and test_app.py, to demonstrate how I would do this:
# app.py
import requests
import urllib
from fastapi import FastAPI
app = FastAPI()
def call_api(self, endpoint: str, params: dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response.json() # <-- This is the only change. Makes it easier to test things.
#app.get("/")
def home():
return {"running": True}
#app.get("/call-api")
def make_call_to_external_api():
# `endpoint` and `params` could be anything here and could be different
# depending on the query parameters when calling this endpoint.
response = call_api(endpoint="something", params={})
# Do something with the response...
result = response["some_parameter"]
return result
# test_app.py
from unittest import mock
from fastapi import status
from fastapi.testclient import TestClient
import app as app_module
from app import app
def test_call_api_endpoint():
test_response = {
"some_parameter": "some_value",
"another_parameter": "another_value",
}
# The line below will "replace" the result of `call_api()` with whatever
# is given in `return_value`. The original function is never executed.
with mock.patch.object(app_module, "call_api", return_value=test_response) as mock_call:
with TestClient(app) as client:
res = client.get("/call-api")
assert res.status_code == status.HTTP_200_OK
assert res.json() == "some_value"
# Make sure the function has been called with the right parameters.
# This could be dynamic based on how the endpoint has been called.
mock_call.assert_called_once_with(endpoint="something", params={})
If app.py and test_app.py are in the same directory you can run the tests simply by running pytest inside that directory.

Using flask to run a python script

I have a script called output.py
This script takes in 2 inputs, fileA and file B.
I can run it on my terminal by using the command output.py -fileA -fileB. The script will create a new JSON file and save it to the directory.
I want to run this script using Flask. I've defined a bare bones App here but I'm not sure how I'd run this using Flask
from flask import Flask
import output
import scripting
app = Flask(__name__)
#app.route('/')
def script():
return output
if __name__ == '__main__':
app.run()
Can someone help me out here, thanks!
It appears you are new to Flask. Get some basic tutorials (there are many on the web).
There are a couple of options:
Send contents of file A and File b as json payload. Pull the A and B content off the json body and do the processing you need to and return the body.
Send the contents of the file as multipart/form-data (you can send multiple files).
Note: This is not working code - just for illustration.
from flask import Flask, request, make_response
app = Flask(__name__)
def build_response(status=False, error="", data={}, total=0, headers=[], contentType="application/json", expose_headers=["X-Total-Count"], retcode=400, additional_data=None):
resp = {"success": status, "error": error, "data": data}
resp = make_response(json.dumps(resp))
for item in headers:
resp.headers[item] = headers[item]
resp.headers['Content-Type'] = contentType
resp.headers.add('Access-Control-Expose-Headers', ','.join(expose_headers))
resp.status_code = retcode
return resp
#app.route('/run-script', methods=['POST'])
def run_script():
# check if the post request has the file part
try:
# Note: THis code is just to illustrate the concept.
# Option-1 (content type must be application/json)
json_dict = request.get_json()
fileA = json_dict["fileA"]
fileB = json_dict["fileB"]
# Option-2 (Note: fileA/fileB are objects, put a pdb and check it out)
fileA = request.files['fileA']
fileB = request.files['fileA']
resp = process(fileA, fileB)
return build_response(status=True, data=resp, retcode=200)
except Exception as e:
msg = f"Error - {str(ec)}"
return build_response(status=False, error=msg, retcode=400)

How to write unit tests using MOCKS that that test the endpoint

I wrote an endpoint that calls an API to get the time of day for a timezone defined by the user. Now I need to Mock this endpoint but I am having trouble coming up with the correct answer. Here is the code that I wrote:
Im not quite sure what I am suppose to call to get a response.
import requests
import json
import jsonpath
import dateutil
from flask import Flask, render_template, request
from flask import jsonify, make_response
app = Flask(__name__, template_folder="templates")
#app.route('/get_time', methods=['GET'])
def get_time():
try:
time_zone = request.args.get('time_zone')
url = "http://worldclockapi.com/api/json/" + time_zone + "/now"
r = requests.get(url)
except Exception:
return make_response(jsonify({"Error": "Some error message"}), 400)
return r.json()["currentDateTime"]
if response.status_code != 200:
print("Error on response")
return response.status_code, response.text
if __name__ == '__main__':
app.run(debug=True)
This is what I have for the Test:
import json
import unittest
import unittest.mock
import requests
#name of the file being tested
import timeofday
class MockResponse:
def __init__(self, text, status_code):
self.text = text
self.status_code = status_code
def json(self):
return json.loads(self.text)
def __iter__(self):
return self
def __next__(self):
return self
#json returned by the API http://worldclockapi.com/api/json/est/now
def mock_requests_timeofday(*args, **kwargs):
text = """
{
"$id": "1",
"currentDateTime": "2019-11-08T15:52-05:00",
"utcOffset": "-05:00:00",
"isDayLightSavingsTime": false,
"dayOfTheWeek": "Friday",
"timeZoneName": "Eastern Standard Time",
"currentFileTime": 132177019635463680,
"ordinalDate": "2019-312",
"serviceResponse": null
}}
"""
response = MockResponse(text, 200)
return response
class TestLocation(unittest.TestCase):
#unittest.mock.patch('requests.get', mock_requests_get_success)
def test_get_time(self):
self.assertEqual(response.status_code, 200)
class TestTimeofday(unittest.TestCase):
#unittest.mock.patch('timeofday.requests.get', mock_requests_timeofday)
def get_time(self):
self.assertEqual(response.status_code, 200)
Your code is currently failing because you directly mock the get function of the module you've imported in your test file.
In order to make your test work, you will have to mock directly the requests.get method of your other file.
This is what a mock of the get method you did in the timeofday.py could look like:
mock.patch('timeofday.requests.get', mock_requests_get_success)
Now when you execute get_time the API call should be mocked and you will receive the answer you defined.
PS: Be aware that the if statement you wrote after the return r.json()["currentDateTime"] will never be executed because your function is ending when you use return.

In Django how to mock an object method called by views.py during its import?

I am writing System Tests for my Django app, where I test the complete application via HTTP requests and mock its external dependencies' APIs.
In views.py I have something like:
from external_service import ExternalService
externalService = ExternalService
data = externalService.get_data()
#crsf_exempt
def endpoint(request):
do_something()
What I want is to mock (or stub) ExternalService to return a predefined response when its method get_data() is called.
The problem is that when I run python manage.py test, views.py is loaded before my test class. So when I patch the object with a mocked one, the function get_data() was already called.
This solution didn't work either.
First off, don't call your method at import time. That can't be necessary, surely?
If get_data does something like a get request, e.g.
def get_data():
response = requests.get(DATA_URL)
if response.ok:
return response
else:
return None
Then you can mock it;
from unittest.mock import Mock, patch
from nose.tools import assert_is_none, assert_list_equal
from external_service import ExternalService
#patch('external_service.requests.get')
def test_getting_data(mock_get):
data = [{
'content': 'Response data'
}]
mock_get.return_value = Mock(ok=True)
mock_get.return_value.json.return_value = data
response = ExternalService.get_data()
assert_list_equal(response.json(), data)
#patch('external_service.requests.get')
def test_getting_data_error(mock_get):
mock_get.return_value.ok = False
response = ExternalService.get_data()
assert_is_none(response)
For this you'll need pip install nose if you don't already have it.

How to send requests with JSON in unit tests

I have code within a Flask application that uses JSONs in the request, and I can get the JSON object like so:
Request = request.get_json()
This has been working fine, however I am trying to create unit tests using Python's unittest module and I'm having difficulty finding a way to send a JSON with the request.
response=self.app.post('/test_function',
data=json.dumps(dict(foo = 'bar')))
This gives me:
>>> request.get_data()
'{"foo": "bar"}'
>>> request.get_json()
None
Flask seems to have a JSON argument where you can set json=dict(foo='bar') within the post request, but I don't know how to do that with the unittest module.
Changing the post to
response=self.app.post('/test_function',
data=json.dumps(dict(foo='bar')),
content_type='application/json')
fixed it.
Thanks to user3012759.
Since Flask 1.0 release flask.testing.FlaskClient methods accepts json argument and Response.get_json method added, see pull request
with app.test_client() as c:
rv = c.post('/api/auth', json={
'username': 'flask', 'password': 'secret'
})
json_data = rv.get_json()
For Flask 0.x compatibility you may use receipt below:
from flask import Flask, Response as BaseResponse, json
from flask.testing import FlaskClient
class Response(BaseResponse):
def get_json(self):
return json.loads(self.data)
class TestClient(FlaskClient):
def open(self, *args, **kwargs):
if 'json' in kwargs:
kwargs['data'] = json.dumps(kwargs.pop('json'))
kwargs['content_type'] = 'application/json'
return super(TestClient, self).open(*args, **kwargs)
app = Flask(__name__)
app.response_class = Response
app.test_client_class = TestClient
app.testing = True

Categories

Resources