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

tia*_*rtr 5 python django unit-testing mocking pyodbc

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

视图.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)
Run Code Online (Sandbox Code Playgroud)

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

  • 模拟库将无法工作,因为 pyodbc 是 Python C 扩展
  • 使用 sys.modules 似乎不起作用,可能是因为该模块在 views.py 中而不是在 tests.py 中使用

这是我的测试驱动程序

测试.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)
Run Code Online (Sandbox Code Playgroud)

Mic*_*ico 5

您可以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")
Run Code Online (Sandbox Code Playgroud)

现在真正的问题view.py是这条线

dbh = pyodbc.connect(db_credentials)
Run Code Online (Sandbox Code Playgroud)

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

我强烈建议您不要编写这种肮脏的技巧,并只更改一点代码来实现惰性dbh属性。另一种方法可以编写您自己的 db 类包装器(更好)并在您的测试中修补它,但这是一个强大的设计更改,您可以稍后通过实现测试的功能引入它。

view.py使用:

_dbh = None
def get_db():
    global _dbh
    if _dbh is None:
        _dbh = pyodbc.connect(db_credentials)
    return _dbh
Run Code Online (Sandbox Code Playgroud)

哪里cusror变成

cursor = get_db().cursor()
Run Code Online (Sandbox Code Playgroud)

现在您可以在测试中修补get_db()和使用return_value模拟

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)
Run Code Online (Sandbox Code Playgroud)

我遗漏了mock_cursor应该如何表现和游标调用断言的细节。您可以通过阅读mock框架文档来编写它。我曾经在setUp()方法中修补连接,因为我可以猜到你在这个类中几乎所有的测试中都需要它cursor_mockdb_mock并且get_db_mock可以用于不同的行为:我的经验是这种方法会在以后付出很多,而你会添加更多测试。