Mock psycopg2 database insertion in Python - python

My unit test does not mock the database insertion because I do see in the logs and in the database that the record id 42 had been inserted in the database, see logs:
./tests/test_requesthandler.py::TestRequestHandler::test_handle_post_coordinates Failed: [undefined]AssertionError: {'id': 42, 'latitude': 12.9, 'longitude': 77.6} != [{'latitude': 12.9, 'longitude': 77.6}]
How can I mock the database insertion in my unit test?
Here is my unit test:
import unittest
from unittest.mock import patch, Mock, MagicMock
from http.server import BaseHTTPRequestHandler
import json
from src.requesthandler import RequestHandler # The code to test
class TestRequestHandler(unittest.TestCase):
def setUp(self):
self.handler = RequestHandler()
#patch('src.requesthandler.psycopg2.connect')
def test_handle_post_coordinates(self, mock_connect):
print(json.dumps({"latitude": "12.9", "longitude": "77.6"}).encode('utf-8'))
expected = [{'latitude': 12.9, 'longitude': 77.6}]
# This will disable the database connection
# mock_connect.return_value.cursor.return_value.execute.return_value = None
mock_con = mock_connect.return_value # result of psycopg2.connect(**connection_stuff)
mock_cur = mock_con.cursor.return_value # result of con.cursor(cursor_factory=DictCursor)
mock_cur.execute.return_value = expected # return this when calling cur.fetchall()
mock_cur.fetchone.return_value = expected # return this when calling cur.fetchall()
mock_con.commit.return_value = expected # return this when calling cur.fetchall()
environ = {
'CONTENT_LENGTH': '23',
'REQUEST_METHOD': 'POST',
'PATH_INFO': '/coordinates',
'wsgi.input': Mock(read=Mock(return_value=json.dumps({'latitude': 12.9, 'longitude': 77.6}).encode('utf-8')))
}
start_response = Mock()
response = self.handler.handle_post_coordinates(environ, start_response)
self.assertEqual(json.loads(response[0].decode().replace("'", '"')), [{'latitude': 12.9, 'longitude': 77.6}])
start_response.assert_called_with('200 OK', [('Content-type', 'text/plain')])
def test_handle_get(self):
environ = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/coordinates',
}
start_response = Mock()
response = self.handler.handle_get(environ, start_response)
self.assertEqual(json.loads(response[0].decode()), {'mssg': 'werkt123'})
start_response.assert_called_with('200 OK', [('Content-type', 'application/json')])
if __name__ == '__main__':
unittest.main()
And here is my code:
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
import psycopg2
from json import dumps
from waitress import serve
import logging
class RequestHandler(BaseHTTPRequestHandler):
# the constructor is called "__init__"for convenience
def __init__(self):
self.coordinates = []
print('qwe')
# Connect to the PostgreSQL database
self.conn = psycopg2.connect(
host="localhost",
database="postgres",
user="postgres",
password="admin"
)
# Create a cursor object
self.cursor = self.conn.cursor()
def _send_response(self, message, status=200):
self.send_response(status)
self.send_header("Content-type", "application/json")
self.end_headers()
self.wfile.write(bytes(json.dumps(message), "utf8"))
def handle_post_coordinates(self, environ, start_response):
content_length = int(environ.get('CONTENT_LENGTH', 0))
request_body = environ['wsgi.input'].read(content_length).decode()
coordinates = json.loads(request_body)
self.coordinates.append(coordinates)
self.cursor.execute("INSERT INTO coordinates (latitude, longitude) VALUES (%s, %s) RETURNING id", (coordinates['latitude'], coordinates['longitude']))
new_coordinate_id = self.cursor.fetchone()[0]
self.conn.commit()
new_coordinate = {'id': new_coordinate_id, 'latitude': coordinates['latitude'], 'longitude': coordinates['longitude']}
status = '200 OK'
headers = [('Content-type', 'text/plain')]
start_response(status, headers)
# return [b"Coordinates added"]
return [bytes(str(new_coordinate), 'utf-8')]
def handle_get(self, environ, start_response):
if environ['PATH_INFO'] == '/coordinates':
# self.cursor.execute("SELECT * FROM coordinates")
# coordinates = self.cursor.fetchall()
# coordinates = [{'id': c[0], 'latitude': c[1], 'longitude': c[2]} for c in coordinates]
# response = dumps(coordinates)
response = dumps({'mssg' : 'werkt123'})
# response = {'mssg' : 'haha3'}
elif environ['PATH_INFO'].startswith('/coordinates/'):
coordinate_id = int(environ['PATH_INFO'].split('/')[-1])
self.cursor.execute("SELECT * FROM coordinates WHERE id = %s", (coordinate_id,))
coordinate = self.cursor.fetchone()
if coordinate:
coordinate = {'id': coordinate[0], 'latitude': coordinate[1], 'longitude': coordinate[2]}
response = dumps(coordinate)
else:
response = dumps({'error': 'Coordinate not found'})
start_response("404 Not Found", [('Content-type', 'application/json')])
return [response.encode()]
else:
response = dumps({'error': 'Invalid endpoint'})
start_response("404 Not Found", [('Content-type', 'application/json')])
return [response.encode()]
start_response("200 OK", [('Content-type', 'application/json')])
return [response.encode()]
def application(self, environ, start_response):
try:
path = environ.get('PATH_INFO', '').lstrip('/')
if path == 'coordinates':
if environ['REQUEST_METHOD'] == 'GET':
return self.handle_get(environ, start_response)
elif environ['REQUEST_METHOD'] == 'POST':
return self.handle_post_coordinates(environ, start_response)
# return self.do_POST(environ, start_response)
# elif environ['REQUEST_METHOD'] == 'PUT':
# return handle_put_coordinates(environ, start_response)
# elif environ['REQUEST_METHOD'] == 'DELETE':
# return handle_delete_coordinates(environ, start_response)
else:
start_response("404 Not Found", [('Content-type', 'text/plain')])
return [b"Not Found"]
except Exception as e:
print("Error:", str(e))
start_response("500 Internal Server Error", [])
return [b"Error: " + str(e).encode()]

You import psycopg2 in your module and use psycopg2.connect, that's what you need to patch (see where to patch).
#patch('psycopg2.connect')
Then, you initiate your RequestHandler instance in setUp, which is not affected by the patch as the patch only applies to the test_handle_post_coordinates. Easiest would be to move this instantiation to inside the test methods, or patch over the whole TestCase.

I have managed to fix it like this:
from unittest import TestCase, mock
from unittest.mock import patch, Mock, MagicMock
from http.server import BaseHTTPRequestHandler
import json
from src.requesthandler import RequestHandler # The code to test
class TestRequestHandler(TestCase):
#mock.patch('src.requesthandler.psycopg2.connect')
def setUp(self, mock_connect):
self.handler = RequestHandler()
self.mock_connect = mock_connect
# #patch('src.requesthandler.psycopg2')
def test_handle_post_coordinates(self):
expected = [{'latitude': 12.9, 'longitude': 77.6}]
mock_con = self.mock_connect.return_value
mock_cur = mock_con.cursor.return_value
mock_cur.execute.return_value = expected
mock_cur.fetchone.return_value = expected
mock_con.commit.return_value = expected
environ = {
'CONTENT_LENGTH': '23',
'REQUEST_METHOD': 'POST',
'PATH_INFO': '/coordinates',
'wsgi.input': Mock(read=Mock(return_value=json.dumps({'latitude': 12.9, 'longitude': 77.6}).encode('utf-8')))
}
start_response = Mock()
response = self.handler.handle_post_coordinates(environ, start_response)
print(json.loads(response[0].decode().replace("'", '"')))
print(response[0].decode())
print([{'id': {'latitude': 12.9, 'longitude': 77.6}, 'latitude': 12.9, 'longitude': 77.6}])
self.assertEqual(json.loads(response[0].decode().replace("'", '"')), {'id': {'latitude': 12.9, 'longitude': 77.6}, 'latitude': 12.9, 'longitude': 77.6})
start_response.assert_called_with('200 OK', [('Content-type', 'text/plain')])
def test_handle_get(self):
environ = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/coordinates',
}
start_response = Mock()
response = self.handler.handle_get(environ, start_response)
self.assertEqual(json.loads(response[0].decode()), {'mssg': 'werkt123'})
start_response.assert_called_with('200 OK', [('Content-type', 'application/json')])
if __name__ == '__main__':
unittest.main()

Related

400 Bad Request: Failed to decode JSON object; PUT request (Flask)

Upon trying to test the JSON payload to a PUT request, I'm getting the following error. I'm not sure if its my test that's causing the error or if there is something else that is causing the issue? If I comment out that line the HTTP verb responds with no error.
werkzeug.exceptions.BadRequest:
400 Bad Request: Failed to decode JSON object: Expecting value: line 1 column 1 (char 0)
-> args = self.put_request_parser.parse_args()
Upon debugging, here what is actually being sent when parse_args() is invoked. I'm not sure why unparsed_arguments is an empty dictionary in this case?
EnvironHeaders([('User-Agent', 'werkzeug/0.14.1'), ('Host', 'localhost'),
('Content-Type', 'application/json'), ('Content-Length', '0'),
('Authorization', 'Bearer <token>')]),
'url': 'http://localhost/api/v1/todos/3', 'unparsed_arguments': {}}
tests.py
class TestUpdateTodoResource(ApiTestCase):
'''Verify that a client succesfully updates an existing todo.'''
def test_put_update_user_todo(self):
with app.test_client() as client:
http_response = client.put(
"api/v1/todos/3",
headers={
'content-type': 'application/json',
'authorization': f"Bearer {token}"
},
data = {
"name": "Never do this todo!"
}
)
self.assertEqual(http_response.status_code, 204)
todos.py
class ApiTodo(Resource):
put_request_parser = reqparse.RequestParser()
put_request_parser.add_argument(
'name',
required=True,
location=['form', 'json'],
help="Cannot accept a blank description"
)
#auth.login_required
def put(self, id):
try:
user_todo = Todo.select().join(User).where(
(Todo.id == id) & (User.id == g.user.id)
).get()
except Todo.DoesNotExist:
abort(404, description="That todo no longer exists")
args = self.put_request_parser.parse_args()
if not args['name']:
abort(400, description="Must provide a todo description")
updated_todo = user_todo.update(**args)
updated_todo.execute()
return marshal(set_todo_creator(updated_todo), todo_fields, 'todo'), 204

How to render react using python routes?

I want to use this route here (below) and render my react file. Using a python route.... Could someone give me some direction on how I can accomplish this?
#Auth.route('/login', methods=['GET'])
def login():
#data = {'username':'bob', 'password':'peepee123'}
#session['token'] = 'jsdkfkj934ujeklfjdlndsflds'
auth = request.authorization
if auth and auth.password == 'password':
token = jwt.encode({'user': auth.username}, app.config['SECRET_KEY'])
return jsonfiy({'token': token.decode('UTF-8')})
return make_response('Could Not verify!', 401, {'WWW-Authenticate': 'Basic realm = "Login Required"'})
If you want to render react in server site in python you can use python-react-v8, but you need to have the same react tree in server and client or it wont work, checkout hydrate in docs. For that you need to have working react app.
Example of usage:
import react
# setup react
react.set_up() # Initialize V8 machinery
react.utils.load_libs(['./bundle.js'])
#Auth.route('/login', methods=['GET'])
def login():
#data = {'username':'bob', 'password':'peepee123'}
#session['token'] = 'jsdkfkj934ujeklfjdlndsflds'
auth = request.authorization
if auth and auth.password == 'password':
token = jwt.encode({'user': auth.username}, app.config['SECRET_KEY'])
data = {'token': token.decode('UTF-8')};
react_ = react.React({
'url': request.get_full_url(),
'data': data
})
context = {
'content': react_.render(),
'data': react_.to_json(data)}
return render('index.html', context);
data = {'token': null, 'reason': "Login Required"}
react_ = react.React({
'url': request.get_full_url(),
'data': data
})
context = {
'content': react_.render(),
'data': react_.to_json(data)
}
return render('index.html', context);

Sharing attributes between classes: is multi-inheritance right and "pythonic" here?

I have an use case where multi-inheritance seems the right way to go. But it implies sharing attributes between "sibling" classes, attributes that are initialized on other classes (so somehow unknown for them).
I'm asking if this below is a "right" and "pythonic" model, or should I better go with a dertivated-classes model.
Let's say we want to develop different deliverers, which will take some source data, apply some format to it, and send it through some channel. And this three parts (data - format - send) can be customizable for each case.
First, come code to make the examples below working:
import sys
PY3 = not sys.version_info < (3,)
from string import Template
import csv, io, smtplib, requests, os
def read_test_movies(year_from, year_to, genre= None):
TEST_MOVIES= [
{'year': 1971, 'release': '01/01/1971', 'genre': 'thriller', 'title': 'Play Misty for Me'},
{'year': 1973, 'release': '02/02/1973', 'genre': 'romantic', 'title': 'Breezy'},
{'year': 1976, 'release': '03/03/1976', 'genre': 'western', 'title': 'The Outlaw'},
{'year': 1986, 'release': '04/04/1986', 'genre': 'war', 'title': 'Heartbreak'},
{'year': 1988, 'release': '05/05/1988', 'genre': 'music', 'title': 'Bird'},
{'year': 1992, 'release': '06/06/1992', 'genre': 'western', 'title': 'Unforgiven'},
{'year': 1995, 'release': '07/07/1995', 'genre': 'romantic', 'title': 'The Bridges of Madison County'},
{'year': 2000, 'release': '08/08/2000', 'genre': 'space', 'title': 'Space Cowboys'},
{'year': 2003, 'release': '09/09/2003', 'genre': 'trhiller', 'title': 'Mystic River'},
{'year': 2004, 'release': '10/10/2004', 'genre': 'sports', 'title': 'Million Dollar Baby'},
{'year': 2006, 'release': '11/11/2006', 'genre': 'war', 'title': 'Flags of Our Fathers'},
{'year': 2006, 'release': '12/12/2006', 'genre': 'war', 'title': 'Letters from Iwo Jima'},
{'year': 2008, 'release': '13/11/2008', 'genre': 'drama', 'title': 'Changeling'},
{'year': 2008, 'release': '14/10/2008', 'genre': 'drama', 'title': 'Gran Torino'},
{'year': 2009, 'release': '15/09/2009', 'genre': 'sports', 'title': 'Invictus'},
{'year': 2010, 'release': '16/08/2010', 'genre': 'drama', 'title': 'Hereafter'},
{'year': 2011, 'release': '17/07/2011', 'genre': 'drama', 'title': 'J. Edgar'},
{'year': 2014, 'release': '18/06/2014', 'genre': 'war', 'title': 'American Sniper'},
{'year': 2016, 'release': '19/05/2016', 'genre': 'drama', 'title': 'Sully'}
]
out= []
for m in TEST_MOVIES:
if year_from <= m['year'] and m['year'] <= year_to:
if genre is None or (genre is not None and genre == m['genre']):
out.append(m)
return out
Being this three parts (data - format - send) so distinguishable, we would start with these interface-like classes (I guess abc could be used too):
class ITheData(object):
def __init__(self, year_from, year_to, genre= None):
self.year_from= year_from
self.year_to = year_to
self.genre = genre
def readMovies(self):
raise NotImplementedError('%s.readMovies() must be implemented' % self.__class__.__name__)
class ITheFormat(object):
def filename(self):
raise NotImplementedError('%s.filename() must be implemented' % self.__class__.__name__)
def make(self):
raise NotImplementedError('%s.make() must be implemented' % self.__class__.__name__)
class ITheSend(object):
def send(self):
raise NotImplementedError('%s.send() must be implemented' % self.__class__.__name__)
For each custom deliver, we will subclass the three of them, and put them together in a class like:
class ITheDeliverer(ITheData, ITheFormat, ITheSend):
def deliver(self):
raise NotImplementedError('%s.deliver() must be implemented' % self.__class__.__name__)
So, we could have two different data sources. Apart from source, they may differ on post-processing actions. Although for simplicity I'm just doing a self.readMovies() all over the place, it could be some other custom method on the subclass.
class TheIMDBData(ITheData):
def readMovies(self):
# movies = some_read_from_IMDB(self.genre, self.year_from, self.year_to)
movies= read_test_movies(self.year_from, self.year_to, self.genre)
return movies
class TheTMDbData(ITheData):
def readMovies(self):
# movies = some_read_from_TMDb(self.genre, self.year_from, self.year_to)
movies= read_test_movies(self.year_from, self.year_to, self.genre)
return movies
We could use also two different formats:
class TheTXTFormat(ITheFormat):
def filename(self):
# Here `genre`, `year_from` and `year_to` are unknown
params= {'genre': self.genre, 'year_from': self.year_from, 'year_to': self.year_to}
return Template('movies_of_${genre}_from_${year_from}_to_${year_to}.txt').substitute(**params)
def make(self):
# Here `readMovies()` is unknown
strio = PY3 and io.StringIO() or io.BytesIO()
for movie in self.readMovies():
line= Template('$title, released on $release').substitute(**movie)
line+= '\n'
strio.write(line)
strio.seek(0)
return strio.read()
class TheCSVFormat(ITheFormat):
def filename(self):
# Here `genre`, `year_from` and `year_to` are unknown
params= {'genre': self.genre, 'year_from': self.year_from, 'year_to': self.year_to}
return Template('movies_of_${genre}_from_${year_from}_to_${year_to}.csv').substitute(**params)
def make(self):
# Here `readMovies()` is unknown
strio = PY3 and io.StringIO() or io.BytesIO()
writer = csv.writer(strio, delimiter=';', quotechar='"', quoting=csv.QUOTE_MINIMAL)
header = ('Title', 'Release')
writer.writerow(header)
for movie in self.readMovies():
writer.writerow((movie['title'], movie['release']))
strio.seek(0)
return strio.read()
And two different sending channels:
class TheMailSend(ITheSend):
host = 'localhost'
sender = 'movie#spammer.com'
receivers = ['movie#spammed.com']
def send(self):
# Here `make()` is unknown
print('TheMailSend.send() Sending to %s' % str(self.receivers))
try:
message = self.make() # Format agnostic
smtpObj = smtplib.SMTP(self.host)
smtpObj.sendmail(self.sender, self.receivers, message)
return True, 'ok'
except Exception as ss:
return False, str(ss)
class TheWSSend(ITheSend):
url = 'spammed.com/movies/send'
def send(self):
# Here `make()` is unknown
print('TheWSSend.send() Sending to %s' % str(self.url))
try:
content = self.make() # Format agnostic
s= requests.Session()
response= s.post(url= self.url, data= {'content': content})
s.close()
if response.status_code == 200:
return True, 'ok'
else:
return False, response.status_code
except Exception as ss:
return False, str(ss)
So, we could end with some deliverers like these:
class TheIMDBToTXTFile(ITheDeliverer, TheIMDBData, TheTXTFormat):
def __init__(self, year_from, year_to, genre= None):
TheIMDBData.__init__(self, year_from, year_to, genre)
def deliver(self):
filepath= os.path.join('/tmp', self.filename())
f= open(filepath, 'w')
f.write(self.make())
f.close()
print('TheIMDBToTXTFile.deliver() => Successfully delivered to %s' % str(filepath))
class TheIMDBToWS(ITheDeliverer, TheIMDBData, TheTXTFormat, TheWSSend):
def __init__(self, year_from, year_to, genre=None):
TheIMDBData.__init__(self, year_from, year_to, genre)
def deliver(self):
ok, msg = self.send()
if ok:
print('TheIMDBToWS.deliver() => Successfully delivered!')
else:
print('TheIMDBToWS.deliver() => Error delivering: %s' % str(msg))
class TheTMDbToMail(ITheDeliverer, TheTMDbData, TheCSVFormat, TheMailSend):
def __init__(self, year_from, year_to, genre=None):
TheTMDbData.__init__(self, year_from, year_to, genre)
def deliver(self):
ok, msg= self.send()
if ok:
print('TheTMDbToMail.deliver() => Successfully delivered!')
else:
print('TheTMDbToMail.deliver() => Error delivering: %s' % str(msg))
And they work fine -with obvious connection errors-:
>>> imdbToTxt = TheIMDBToTXTFile(year_from= 2000, year_to= 2010)
>>> imdbToTxt.deliver()
TheIMDBToTXTFile.deliver() => Successfully delivered to /tmp/movies_of_None_from_200_to_2010.txt
>>>
>>> imdbToWs = TheIMDBToWS(year_from= 2000, year_to= 2010)
>>> imdbToWs.deliver()
TheWSSend.send() Sending to http://spammed.com/movies/send?
TheIMDBToWS.deliver() => Error delivering: 405
>>>
>>> tmdbToMail = TheTMDbToMail(year_from= 1980, year_to= 2019, genre= 'war')
>>> tmdbToMail.deliver()
TheMailSend.send() Sending to ['movie#spammed.com']
TheTMDbToMail.deliver() => Error delivering: [Errno 111] Connection refused
But, as commented, some attributes are unknown for some classes, and the linter is -obviously- complaining about it:
Instance of 'TheTXTFormat' has no 'genre' member
Instance of 'TheTXTFormat' has no 'year_from' member
Instance of 'TheTXTFormat' has no 'year_to' member
Instance of 'TheTXTFormat' has no 'readMovies' member
Instance of 'TheCSVFormat' has no 'genre' member
Instance of 'TheCSVFormat' has no 'year_from' member
Instance of 'TheCSVFormat' has no 'year_to' member
Instance of 'TheCSVFormat' has no 'readMovies' member
Instance of 'TheMailSend' has no 'make' member
Instance of 'TheWSSend' has no 'make' member
So, the question remains: is here multi-inheritance a good model?
The alternatives could be: a derivated-classes model, or just independent classes and passing around parameters like data or formatter. But none of them seem so simple as multi-inheritance (although they'd fix linter -and probably conceptual- problems).
I don't think inheritance is a good model here. You have lots of classes and it gets messy.
I think It's ok to implement inheritance for different "flavors" of the same step, of using the "Template pattern" described here.
from abc import ABC, abstractmethod
class ITheSend(ABC):
def run(self) -> None:
"""
The template method defines the skeleton of an algorithm.
"""
self.pre_send_hook()
self.send()
self.post_send_hook()
# These operations have to be implemented in subclasses.
#abstractmethod
def send(self) -> None:
pass
# These are "hooks." Subclasses may override them, but it's not mandatory
# since the hooks already have default (but empty) implementation. Hooks
# provide additional extension points in some crucial places of the
# algorithm.
def pre_send_hook(self) -> None:
pass
def post_send_hook(self) -> None:
pass
class TheMailSend(ITheSend):
host = 'localhost'
sender = 'movie#spammer.com'
receivers = ['movie#spammed.com']
def send(self, message):
print('TheMailSend.send() Sending to %s' % str(self.receivers))
try:
smtpObj = smtplib.SMTP(self.host)
smtpObj.sendmail(self.sender, self.receivers, message)
return True, 'ok'
except Exception as ss:
return False, str(ss)
class TheWSSend(ITheSend):
url = 'spammed.com/movies/send'
def send(self, content):
print('TheWSSend.send() Sending to %s' % str(self.url))
try:
s= requests.Session()
response= s.post(url= self.url, data= {'content': content})
s.close()
if response.status_code == 200:
return True, 'ok'
else:
return False, response.status_code
except Exception as ss:
return False, str(ss)
However, for the full chain I'd explore composition instead of inheritance.
class Chain:
def __init__(self, data, format, send):
self._data = data
self._format = format
self._send = send
def deliver(self):
data = self._data.execute()
format = self._format.execute(data)
send = self._send.execute(format)
the_IMDB_to_TXT_file = Chain(send=ITheDeliverer, data=TheIMDBData, format=TheTXTFormat)

Unittest Django: Mock external API, what is proper way?

I am having a problem understanding how mock works and how to write unittests with mock objects. I wanted to mock an external api call every time when my model calls save() method.
My code:
models.py
from . import utils
class Book(Titleable, Isactiveable, Timestampable, IsVoidable, models.Model):
title
orig_author
orig_title
isbn
def save(self, *args, **kwargs):
if self.isbn:
google_data = utils.get_original_title_and_name(self.isbn)
if google_data:
self.original_author = google_data['author']
self.original_title = google_data['title']
super().save(*args, **kwargs)
utils.py
def get_original_title_and_name(isbn, **kawargs):
isbn_search_string = 'isbn:{}'.format(isbn)
payload = {
'key': GOOGLE_API_KEY,
'q': isbn_search_string,
'printType': 'books',
}
r = requests.get(GOOGLE_API_URL, params=payload)
response = r.json()
if 'items' in response.keys():
title = response['items'][THE_FIRST_INDEX]['volumeInfo']['title']
author = response['items'][THE_FIRST_INDEX]['volumeInfo']['authors'][THE_FIRST_INDEX]
return {
'title': title,
'author': author
}
else:
return None
I began read docs and write test:
test.py:
from unittest import mock
from django.test import TestCase
from rest_framework import status
from .constants import THE_FIRST_INDEX, GOOGLE_API_URL, GOOGLE_API_KEY
class BookModelTestCase(TestCase):
#mock.patch('requests.get')
def test_get_original_title_and_name_from_google_api(self, mock_get):
# Define new Mock object
mock_response = mock.Mock()
# Define response data from Google API
expected_dict = {
'kind': 'books#volumes',
'totalItems': 1,
'items': [
{
'kind': 'books#volume',
'id': 'IHxXBAAAQBAJ',
'etag': 'B3N9X8vAMWg',
'selfLink': 'https://www.googleapis.com/books/v1/volumes/IHxXBAAAQBAJ',
'volumeInfo': {
'title': "Alice's Adventures in Wonderland",
'authors': [
'Lewis Carroll'
]
}
}
]
}
# Define response data for my Mock object
mock_response.json.return_value = expected_dict
mock_response.status_code = 200
# Define response for the fake API
mock_get.return_value = mock_response
The first of all, I can't write target for the #mock.patch correct. If a define target as utuls.get_original_title_and_name.requests.get, I get ModuleNotFoundError. Also I can't understand how to make fake-call to external API and verify recieved data (whether necessarly its, if I've already define mock_response.json.return_value = expected_dict?) and verify that my save() method work well?
How do I write test for this cases? Could anyone explain me this case?
You should mock the direct collaborators of the code under test. For Book that would be utils. For utils that would be requests.
So for the BookModelTestCase:
class BookModelTestCase(TestCase):
#mock.patch('app.models.utils')
def test_save_book_calls_google_api(self, mock_utils):
mock_utils.get_original_title_and_name.return_value = {
'title': 'Google title',
'author': 'Google author'
}
book = Book(
title='Some title',
isbn='12345'
)
book.save()
self.assertEqual(book.title, 'Google title')
self.assertEqual(book.author, 'Google author')
mock_utils.get_original_title_and_name.assert_called_once_with('12345')
And then you can create a separate test case to test get_original_title_and_name:
class GetOriginalTitleAndNameTestCase(TestCase):
#mock.patch('app.utils.requests.get')
def test_get_original_title_and_name_from_google_api(self, mock_get):
mock_response = mock.Mock()
# Define response data from Google API
expected_dict = {
'kind': 'books#volumes',
'totalItems': 1,
'items': [
{
'kind': 'books#volume',
'id': 'IHxXBAAAQBAJ',
'etag': 'B3N9X8vAMWg',
'selfLink': 'https://www.googleapis.com/books/v1/volumes/IHxXBAAAQBAJ',
'volumeInfo': {
'title': "Alice's Adventures in Wonderland",
'authors': [
'Lewis Carroll'
]
}
}
]
}
# Define response data for my Mock object
mock_response.json.return_value = expected_dict
mock_response.status_code = 200
# Define response for the fake API
mock_get.return_value = mock_response
# Call the function
result = get_original_title_and_name(12345)
self.assertEqual(result, {
'title': "Alice's Adventures in Wonderland",
'author': 'Lewis Carroll'
})
mock_get.assert_called_once_with(GOOGLE_API_URL, params={
'key': GOOGLE_API_KEY,
'q': 'isbn:12345',
'printType': 'books',
})

Convert test client data to JSON

I'm building an app and I want to make some tests. I need to convert the response data from the test client to JSON.
The app:
tasks = [
{
'id': 1,
'title': u'Buy groceries',
'description': u'Milk, Cheese, Pizza, Fruit, Tylenol',
'done': False
},
{
'id': 2,
'title': u'Learn Python',
'description': u'Need to find a good Python tutorial on the web',
'done': False
}
]
app = Flask(__name__, static_url_path="")
#app.route('/myapp/api/v1.0/tasks', methods=['GET'])
def get_tasks():
return jsonify({'tasks': [task for task in tasks]})
if __name__ == '__main__':
app.run(debug=True)
The tests:
class MyTestCase(unittest.TestCase):
def setUp(self):
myapp.app.config['TESTING'] = True
self.app = myapp.app.test_client()
def test_empty_url(self):
response = self.app.get('/myapp/api/v1.0/tasks')
resp = json.loads(response.data)
print(resp)
if __name__ == '__main__':
unittest.main()
When I try to convert response.data to JSON, I get the following error:
TypeError: the JSON object must be str, not 'bytes'
How can I fix this error and get the JSON data?
Flask 1.0 adds the get_json method to the response object, similar to the request object. It handles parsing the response data as JSON, or raises an error if it can't.
data = response.get_json()
Prior to that, and prior to Python 3.6, json.loads expects text, but data is bytes. The response object provides the method get_data, with the as_text parameter to control this.
data = json.loads(response.get_data(as_text=True))

Categories

Resources