I want to make unit tests for some django views which use a custom pyodbc database connection
from django.http import JsonResponse, HttpResponseNotFound, HttpResponseBadRequest, HttpResponseServerError, HttpResponseForbidden
from django.core.exceptions import SuspiciousOperation
from django.utils.datastructures import MultiValueDictKeyError
import os
import pyodbc
# Create your views here.
db_credentials = os.environ.get('DATABASE_CREDENTIALS')
dbh = pyodbc.connect(db_credentials)
def get_domains(request):
if request.method == 'GET':
args = request.GET
elif request.method == 'POST':
args = request.POST
try:
cursor = dbh.cursor()
if 'owner' in args:
owner = args['owner']
cursor.execute('{call GET_DOMAINS_FOR_OWNER(?)}', owner)
else:
cursor.execute('{call GET_DOMAINS()}')
result = cursor.fetchall()
if(result):
return JsonResponse([row[0] for row in result], safe=False)
else:
return JsonResponse([], safe=False)
except pyodbc.Error as e:
return HttpResponseServerError(e)
except SuspiciousOperation as e:
return HttpResponseForbidden(e)
Since I don't want the unit tests to be hitting the database, how can I mock the behaviour given that:
Here is my test driver
from django.test import SimpleTestCase
from sms_admin import *
# Create your tests here.
HTTP_OK = 200
HTTP_NOTFOUND = 404
class AdminTestCase(SimpleTestCase):
"""docstring for AdminTestCase"""
def test_get_pool_for_lds(self):
response = self.client.get('/sms_admin/get_pool_for_lds', {'domain': 'sqlconnect', 'stage': 'dev', 'lds': 'reader'})
self.assertEqual(response.content, b'pdss_reader')
self.assertEqual(response.status_code, HTTP_OK)
You can patch pyodbc.connect
without any limitations as showed in the follow example:
import pyodbc
from unittest.mock import patch
with patch("pyodbc.connect") as mock_connect:
pyodbc.connect("Credentials")
mock_connect.assert_called_with("Credentials")
Now the real issue in view.py
is the line
dbh = pyodbc.connect(db_credentials)
That line is executed while your are importing view.py
and you cannot control it without implement some kind of hack in your test code like patching connect before importing view.py
or anything else importing it.
I would like strongly discourage you on write this kind of dirty tricks and change just a little your code to implement a lazy dbh
property. Another way can write your own db class wrapper (better) and patch it in your tests but that is a strong design change and you can introduce it later by take the power of implemented tests.
In view.py
use:
_dbh = None
def get_db():
global _dbh
if _dbh is None:
_dbh = pyodbc.connect(db_credentials)
return _dbh
where cusror
become
cursor = get_db().cursor()
Now you can patch get_db()
and use return_value
mock in your test
class AdminTestCase(SimpleTestCase):
"""docstring for AdminTestCase"""
def setUp(self):
super().setUp()
p = patch("yourpackage.view.get_db")
self.addCleanup(p.stop)
self.get_db_mock = p.start()
self.db_mock = self.get_db_mock.return_value
self.cursor_mock = self.db_mock.cursor.return_value
def test_get_pool_for_lds(self, get_db_mock):
.... configure self.cursor_mock to behave as you need
response = self.client.get('/sms_admin/get_pool_for_lds', {'domain': 'sqlconnect', 'stage': 'dev', 'lds': 'reader'})
self.assertEqual(response.content, b'pdss_reader')
self.assertEqual(response.status_code, HTTP_OK)
I left out the detail of how mock_cursor
should behave and cursor calls asserts. You can write it by reading mock
framework documentation. I used to patch connection in setUp()
method because I can guess that you need it in almost all your tests in this class where cursor_mock
, db_mock
and get_db_mock
can be used with different behavior: my experience is that this approach will pay a lot later while you'll add more tests.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With