模拟 pyodbc 模块调用 Django 单元测试

Mocking pyodbc module calls for django unit tests

我想对一些使用自定义 pyodbc 数据库连接的 Django 视图进行单元测试

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)

由于我不希望单元测试访问数据库,因此我如何模拟该行为:

这是我的试车手

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)

您可以无限制地修补 pyodbc.connect,如下例所示:

import pyodbc
from unittest.mock import patch

with patch("pyodbc.connect") as mock_connect:
    pyodbc.connect("Credentials")
    mock_connect.assert_called_with("Credentials")

现在 view.py 中的真正问题是行

dbh = pyodbc.connect(db_credentials)

该行在您正在导入时执行 view.py,如果不在您的测试代码中实施某种黑客攻击,例如修补连接,您将无法控制它 导入 view.py 或任何其他导入它之前。

我强烈建议您不要编写这种卑鄙的把戏,只需稍微更改您的代码即可实现惰性 dbh 属性。另一种方法可以编写您自己的 db class 包装器(更好)并在您的测试中对其进行修补,但这是一个强大的设计更改,您可以稍后通过利用已实施测试的力量来引入它。

view.py中使用:

_dbh = None
def get_db():
    global _dbh
    if _dbh is None:
        _dbh = pyodbc.connect(db_credentials)
    return _dbh

其中 cusror 变为

cursor = get_db().cursor()

现在您可以修补 get_db() 并在您的测试中使用 return_value mock

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)

我省略了 mock_cursor 应该如何表现和游标调用断言的细节。阅读mock framework documentation就可以写出来了。我曾经在 setUp() 方法中修补连接,因为我猜你在这个 class 的几乎所有测试中都需要它,其中 cursor_mockdb_mockget_db_mock可以用于不同的行为:我的经验是这种方法会在你添加更多测试时付出很多。