Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking pyodbc module calls for django unit tests

I want to make unit tests for some django views which use a custom pyodbc database connection

views.py

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:

  • The mock library won't work since pyodbc is a Python C extension
  • Using sys.modules doesn't seem to work, probably because the module is being used in views.py and not on tests.py

Here is my test driver

tests.py

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)
like image 619
tiagovrtr Avatar asked Apr 29 '16 15:04

tiagovrtr


1 Answers

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.

like image 123
Michele d'Amico Avatar answered Nov 11 '22 08:11

Michele d'Amico