在 Django 中模拟一个模型字段验证器

unl*_*kme 9 python django unit-testing mocking

根据python模拟库的文档。我们从正在使用/调用它的模块中模拟出一个函数。

a.py
def function_to_mock(x):
   print('Called function to mock')

b.py
from a import function_to_mock

def function_to_test(some_param):
    function_to_mock(some_param)

# According to the documentation 
#if we want to mock out function_to_mock in a
# test we have to patch it from the b.py module because that is where 
# it is called from

class TestFunctionToTest(TestCase):

    @patch('b.function_to_mock')
    def test_function_to_test(self, mock_for_function_to_mock):
        function_to_test()
        mock_for_function_to_mock.assert_called_once()
   
# this should mock out function to mock and the assertion should work

Run Code Online (Sandbox Code Playgroud)

我陷入了一种无法确切说出如何模拟相关函数的情况。这是情况。

# some application
validators.py
def validate_a_field(value):
    # do your validation here.

models.py
from .validators import validate_a_field

class ModelClass(models.Model):
      a_field = models.CharField(max_length=25, validators=[validate_a_field])

forms.py
class ModelClassModelForm(forms.ModelForm):
      class Meta:
           model = ModelClass
           fields = ['a_field',]

Finally in my tests.py
tests.py

class TestModelClassModelForm(TestCase):
      @patch('models.validate_a_field') <<< What to put here ???
      def test_valid_data_validates(self, validate_a_field_mock):
           data = {'a_field':'Some Text'}
           validate_a_field_mock.return_value = True

           form = ModelClassModelForm(data=data)
           is_valid = form.is_valid()
           validate_a_field_mock.assert_called_once() <<< This is failing
Run Code Online (Sandbox Code Playgroud)

尽管在models.py 中调用了validate_a_field,但根据我的一点理解。当验证发生时,它永远不会从那里使用。因此,当我修补为models.validate_a_field

我最好的猜测是它在django.forms.field. 但我不知道如何或在哪里。

有谁知道如何解决这个难题?我必须模拟validate_a_field,因为它确实调用了外部API,这更像是一个集成测试。我想写一个单元测试。

Don*_*kby 1

问题是,validate_a_field()当您模拟该函数时,您的模型已经获取了该函数的副本,因此该模型仍然调用原始函数。据我所知,该副本不会暴露在任何地方,因此我会添加一个包装函数,只是为了允许模拟。为真正的验证代码制作validate_a_field()一个包装器,然后模拟内部函数。

# validators.py
def validate_a_field(value):
    # Delegate to allow testing.
    really_validate_a_field(value)

def really_validate_a_field(value):
    # do your validation here.
Run Code Online (Sandbox Code Playgroud)
# tests.py
class TestModelClassModelForm(TestCase):
      @patch('validators.really_validate_a_field')
      def test_valid_data_validates(self, validate_a_field_mock):
           data = {'a_field':'Some Text'}
           validate_a_field_mock.return_value = True

           form = ModelClassModelForm(data=data)
           is_valid = form.is_valid()
           validate_a_field_mock.assert_called_once()
Run Code Online (Sandbox Code Playgroud)

这是一个完整的、可运行的示例供您使用。这些文件全部合并为一个,但您可以单独运行它以查看一切如何工作。

当你运行它时,test_patch_outer()失败,然后test_patch_inner()通过。

""" A Django web app and unit tests in a single file.

Based on Nsukami's blog post: https://nskm.xyz/posts/dsfp/

To get it running, copy it into a directory named udjango:
$ pip install django
$ python udjango_test.py

Change the DJANGO_COMMAND to runserver to switch back to web server.

Tested with Django 4.0 and Python 3.9.
"""


import os
import sys
from unittest.mock import patch

import django
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib import admin
from django.core.management import call_command
from django.core.management.utils import get_random_secret_key
from django.core.wsgi import get_wsgi_application
from django import forms
from django.db import models
from django.db.models.base import ModelBase
from django.test import TestCase

WIPE_DATABASE = True
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_FILE = os.path.join(BASE_DIR, 'udjango.db')
DJANGO_COMMAND = 'test'  # 'test' or 'runserver'

# the current folder name will also be our app
APP_LABEL = os.path.basename(BASE_DIR)
urlpatterns = []
ModelClass = ModelClassModelForm = None


class Tests(TestCase):
    @patch('__main__.validate_a_field')
    def test_patch_outer(self, validate_a_field_mock):
        data = {'a_field':'Some Text'}
        validate_a_field_mock.return_value = True

        form = ModelClassModelForm(data=data)
        is_valid = form.is_valid()
        validate_a_field_mock.assert_called_once()

    @patch('__main__.really_validate_a_field')
    def test_patch_inner(self, validate_a_field_mock):
        data = {'a_field':'Some Text'}
        validate_a_field_mock.return_value = True

        form = ModelClassModelForm(data=data)
        is_valid = form.is_valid()
        validate_a_field_mock.assert_called_once()

def validate_a_field(value):
    really_validate_a_field(value)

def really_validate_a_field(value):
    # do your validation here.
    raise RuntimeError('External dependency not available.')

def main():
    global ModelClass, ModelClassModelForm
    setup()

    # Create your models here.
    class ModelClass(models.Model):
        a_field = models.CharField(max_length=25, validators=[validate_a_field])

    class ModelClassModelForm(forms.ModelForm):
        class Meta:
            model = ModelClass
            fields = ['a_field',]

    admin.site.register(ModelClass)
    admin.autodiscover()

    if __name__ == "__main__":
        if DJANGO_COMMAND == 'test':
            call_command('test', '__main__.Tests')
        else:
            if WIPE_DATABASE or not os.path.exists(DB_FILE):
                with open(DB_FILE, 'w'):
                    pass
                call_command('makemigrations', APP_LABEL)
                call_command('migrate')
                get_user_model().objects.create_superuser('admin', '', 'admin')
            call_command(DJANGO_COMMAND)
    else:
        get_wsgi_application()


def setup():
    sys.path[0] = os.path.dirname(BASE_DIR)

    static_path = os.path.join(BASE_DIR, "static")
    try:
        os.mkdir(static_path)
    except FileExistsError:
        pass
    settings.configure(
        DEBUG=True,
        ROOT_URLCONF=__name__,
        MIDDLEWARE=[
            'django.middleware.security.SecurityMiddleware',
            'django.contrib.sessions.middleware.SessionMiddleware',
            'django.middleware.common.CommonMiddleware',
            'django.middleware.csrf.CsrfViewMiddleware',
            'django.contrib.auth.middleware.AuthenticationMiddleware',
            'django.contrib.messages.middleware.MessageMiddleware',
            'django.middleware.clickjacking.XFrameOptionsMiddleware',
            'django.middleware.locale.LocaleMiddleware',
            ],
        INSTALLED_APPS=[
            APP_LABEL,
            'django.contrib.admin',
            'django.contrib.auth',
            'django.contrib.contenttypes',
            'django.contrib.sessions',
            'django.contrib.messages',
            'django.contrib.staticfiles',
            'rest_framework',
            ],
        STATIC_URL='/static/',
        STATICFILES_DIRS=[
            static_path,
        ],
        STATIC_ROOT=os.path.join(BASE_DIR, "static_root"),
        MEDIA_ROOT=os.path.join(BASE_DIR, "media"),
        MEDIA_URL='/media/',
        SECRET_KEY=get_random_secret_key(),
        DEFAULT_AUTO_FIELD='django.db.models.AutoField',
        TEMPLATES=[
            {
                'BACKEND': 'django.template.backends.django.DjangoTemplates',
                'DIRS': [os.path.join(BASE_DIR, "templates")],
                'APP_DIRS': True,
                'OPTIONS': {
                    'context_processors': [
                        'django.template.context_processors.debug',
                        'django.template.context_processors.i18n',
                        'django.template.context_processors.request',
                        'django.contrib.auth.context_processors.auth',
                        'django.template.context_processors.tz',
                        'django.contrib.messages.context_processors.messages',
                    ],
                },
            },
            ],
        DATABASES={
            'default': {
                'ENGINE': 'django.db.backends.sqlite3',
                'NAME': DB_FILE,
                }
            },
        REST_FRAMEWORK={
            'DEFAULT_PERMISSION_CLASSES': [
                'rest_framework.permissions.IsAdminUser',
            ],
        }
    )

    django.setup()
    app_config = django.apps.apps.app_configs[APP_LABEL]
    app_config.models_module = app_config.models
    original_new_func = ModelBase.__new__

    @staticmethod
    def patched_new(cls, name, bases, attrs):
        if 'Meta' not in attrs:
            class Meta:
                app_label = APP_LABEL
            attrs['Meta'] = Meta
        return original_new_func(cls, name, bases, attrs)
    ModelBase.__new__ = patched_new


main()
Run Code Online (Sandbox Code Playgroud)