【问题标题】:Mocking pyodbc module calls for django unit tests模拟 pyodbc 模块调用 django 单元测试
【发布时间】:2023-12-04 08:19:01
【问题描述】:

我想对一些使用自定义 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)

既然我不希望单元测试访问数据库,我该如何模拟这种行为:

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

这是我的测试驱动程序

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)

【问题讨论】:

    标签: python django unit-testing mocking pyodbc


    【解决方案1】:

    您可以无限制地修补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 时执行,如果不在您的测试代码中实施某种hack,例如修补连接之前 导入@987654327,您将无法控制它@ 或其他任何导入它的东西。

    我强烈反对您编写这种肮脏的技巧,只需更改一点您的代码即可实现懒惰的dbh 属性。另一种方法可以编写您自己的 db 类包装器(更好)并在您的测试中对其进行修补,但这是一个强大的设计更改,您可以稍后通过实施测试的力量来引入它。

    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 模拟

    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() 方法中修补连接,因为我猜你在这个类的几乎所有测试中都需要它,其中cursor_mockdb_mockget_db_mock 可以用于不同的行为:我的经验是这个当您添加更多测试时,这种方法会花很多钱。

    【讨论】:

    • 在这种情况下 db_mock 应该是另一个 Mock 对象吗?
    • db_mock 是另一个模拟,它与修补后的 get_db 在测试上下文中返回的结果相同