簡體   English   中英

在 Django 中模擬一個 model 字段驗證器

[英]Mock out a model field validator in Django

根據 python 模擬庫的文檔。 我們從使用/調用它的模塊中模擬出 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

我讓自己陷入了一種無法確切知道如何模擬有問題的 function 的情況。 這是情況。

# 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

即使在models.py中調用了validate_a_field,據我所知。 當驗證發生時,它永遠不會從那里使用。 因此,當我修補為models.validate_a_field時,它不會被嘲笑

我最好的猜測是它在django.forms.field的某個地方被調用。 但我不知道如何或在哪里。

有誰知道如何解決這個難題? 我確實必須模擬 validate_a_field,因為它確實調用了更像是集成測試的外部 API。 我想寫一個單元測試。

如果我有選擇,我會修補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)

問題是您的 model 在您模擬它時已經復制了validate_a_field() function,所以 model 仍然調用原始文件。 該副本沒有在我知道的任何地方公開,所以我會添加一個包裝器 function,只是為了允許 mocking。 使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()

這是一個完整的、可運行的示例供您使用。 這些文件都合並為一個,但您可以自己運行它以查看一切如何工作。

當你運行它時, 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