简体   繁体   English

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

[英]Mock out a model field validator in Django

According to the documentation of the python mock library.根据 python 模拟库的文档。 We mock out a function from the module in which it is being used/called.我们从使用/调用它的模块中模拟出 function。

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

I got myself in a situation where I can't tell exactly how to mock the function in question.我让自己陷入了一种无法确切知道如何模拟有问题的 function 的情况。 Here is the situation.这是情况。

# 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

From my little understanding even though validate_a_field is called in models.py.即使在models.py中调用了validate_a_field,据我所知。 It is never used from there when validation takes place.当验证发生时,它永远不会从那里使用。 It, therefore, is not mocked when I patch as models.validate_a_field因此,当我修补为models.validate_a_field时,它不会被嘲笑

My best guess is that it is called somewhere in django.forms.field .我最好的猜测是它在django.forms.field的某个地方被调用。 But I don't know how or where.但我不知道如何或在哪里。

Does anyone know how to resolve this conundrum?有谁知道如何解决这个难题? I do have to mock out validate_a_field because it does call external APIs which is more of an integration test.我确实必须模拟 validate_a_field,因为它确实调用了更像是集成测试的外部 API。 I want to write a unit test.我想写一个单元测试。

If I have a choice, I would patch the ModelClass.full_clean method如果我有选择,我会修补ModelClass.full_clean方法

class TestModelClassModelForm(TestCase):

    @patch('sample.models.ModelClass.full_clean')
    def test_valid_data_validates(self, mock_model_full_clean):
        data = {'a_field': 'Some Text'}
        form = ModelClassModelForm(data=data)
        is_valid = form.is_valid()

        mock_model_full_clean.assert_called_once() self.assertTrue(is_valid) self.assertDictEqual(data, form.cleaned_data)

The problem is that your model already took a copy of the validate_a_field() function by the time you mock it, so the model still calls the original.问题是您的 model 在您模拟它时已经复制了validate_a_field() function,所以 model 仍然调用原始文件。 That copy isn't exposed anywhere that I know of, so I would add a wrapper function, just to allow mocking.该副本没有在我知道的任何地方公开,所以我会添加一个包装器 function,只是为了允许 mocking。 Make validate_a_field() just a wrapper for the real validation code, then mock the inner function.使validate_a_field()只是真正验证代码的包装器,然后模拟内部 function。

# 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.
# 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()

Here's a complete, runnable example for you to play with.这是一个完整的、可运行的示例供您使用。 The files are all mushed into one, but you can run it on its own to see how everything works.这些文件都合并为一个,但您可以自己运行它以查看一切如何工作。

When you run it, test_patch_outer() fails, and test_patch_inner() passes.当你运行它时, 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()

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM