it-roy-ru.com

Джанго: Как создать модель динамически только для тестирования

У меня есть приложение Django, которое требует атрибут settings в виде:

RELATED_MODELS = ('appname1.modelname1.attribute1',
                  'appname1.modelname2.attribute2', 
                  'appname2.modelname3.attribute3', ...)

Затем перехватывает их сигнал post_save для обновления какой-либо другой фиксированной модели в зависимости от определенной attributeN.

Я хотел бы проверить это поведение, и тесты должны работать, даже если это приложение является единственным в проекте (за исключением его собственных зависимостей, никакое другое приложение-оболочка устанавливать не нужно). Как я могу создавать и прикреплять/регистрировать/активировать макеты только для тестовой базы данных? (или это вообще возможно?)

Решения, которые позволяют мне использовать тестовые приборы, были бы хорошими.

58
muhuk

Вы можете поместить свои тесты в подкаталог tests/ приложения (а не в файл tests.py) и включить tests/models.py с моделями только для тестирования.

Затем предоставьте сценарий запуска теста ( example ), который включает ваше tests/ "app" в INSTALLED_APPS. (Это не работает при запуске тестов приложений из реального проекта, в котором приложение INSTALLED_APPS не будет иметь тестов, но я редко нахожу полезным запускать тесты приложений многократного использования из проекта, а Django 1.6+ по умолчанию этого не делает .)

(NOTE: альтернативный динамический метод, описанный ниже, работает только в Django 1.1+, если ваши подклассы тестового набора TransactionTestCase - что значительно замедляет ваши тесты - и больше не работает вообще в Django 1.7+. Он оставлен только здесь для исторического интереса, не используйте его.)

В начале ваших тестов (то есть в методе setUp или в начале набора тестов) вы можете динамически добавить "myapp.tests" в настройку INSTALLED_APPS, а затем сделать это:

from Django.core.management import call_command
from Django.db.models import loading
loading.cache.loaded = False
call_command('syncdb', verbosity=0)

Затем в конце ваших тестов вы должны очиститься, восстановив старую версию INSTALLED_APPS и снова очистив кэш приложения.

Этот класс инкапсулирует шаблон, чтобы он не загромождает ваш тестовый код.

51
Carl Meyer

Ответ @ paluh требует добавления нежелательного кода в не тестовый файл, и, по моему опыту, решение @ carl не работает с Django.test.TestCase, который необходим для использования приборов. Если вы хотите использовать Django.test.TestCase, вам нужно убедиться, что вы вызываете syncdb до загрузки приборов. Это требует переопределения метода _pre_setup (недостаточно поместить код в метод setUp). Я использую свою собственную версию TestCase, которая позволяет мне добавлять приложения с тестовыми моделями. Он определяется следующим образом:

from Django.conf import settings
from Django.core.management import call_command
from Django.db.models import loading
from Django import test

class TestCase(test.TestCase):
    apps = ()

    def _pre_setup(self):
        # Add the models to the db.
        self._original_installed_apps = list(settings.INSTALLED_APPS)
        for app in self.apps:
            settings.INSTALLED_APPS.append(app)
        loading.cache.loaded = False
        call_command('syncdb', interactive=False, verbosity=0)
        # Call the original method that does the fixtures etc.
        super(TestCase, self)._pre_setup()

    def _post_teardown(self):
        # Call the original method.
        super(TestCase, self)._post_teardown()
        # Restore the settings.
        settings.INSTALLED_APPS = self._original_installed_apps
        loading.cache.loaded = False
18
Conley Owens

Это решение работает только для более ранних версий Django (до 1.7). Вы можете легко проверить свою версию:

import Django
django.VERSION < (1, 7)

Оригинальный ответ:

Это довольно странно, но у меня работает очень простая схема:

  1. добавьте tests.py в приложение, которое вы собираетесь тестировать,
  2. в этом файле просто определите модели тестирования,
  3. ниже поместите свой тестовый код (определение doctest или TestCase),

Ниже я поместил некоторый код, который определяет модель Article, которая необходима только для тестов (она существует в someapp/tests.py, и я могу протестировать ее просто с помощью: ./manage.py test someapp ):

class Article(models.Model):
    title = models.CharField(max_length=128)
    description = models.TextField()
    document = DocumentTextField(template=lambda i: i.description)

    def __unicode__(self):
        return self.title

__test__ = {"doctest": """
#smuggling model for tests
>>> from .tests import Article

#testing data
>>> by_two = Article.objects.create(title="divisible by two", description="two four six eight")
>>> by_three = Article.objects.create(title="divisible by three", description="three six nine")
>>> by_four = Article.objects.create(title="divisible by four", description="four four eight")

>>> Article.objects.all().search(document='four')
[<Article: divisible by two>, <Article: divisible by four>]
>>> Article.objects.all().search(document='three')
[<Article: divisible by three>]
"""}

Модульные тесты также работают с таким определением модели.

11
paluh

Я поделился своим решением , которое я использую в своих проектах. Может, это кому-то поможет.

pip install Django-fake-model

Два простых шага для создания поддельной модели:

1) Определить модель в любом файле (обычно я определяю модель в тестовом файле рядом с тестовым набором)

from Django_fake_model import models as f


class MyFakeModel(f.FakeModel):

    name = models.CharField(max_length=100)

2) Добавьте декоратор @MyFakeModel.fake_me к вашему TestCase или к тестовой функции.

class MyTest(TestCase):

    @MyFakeModel.fake_me
    def test_create_model(self):
        MyFakeModel.objects.create(name='123')
        model = MyFakeModel.objects.get(name='123')
        self.assertEqual(model.name, '123')

Этот декоратор создает таблицу в вашей базе данных перед каждым тестом и удаляет таблицу после теста.

Также вы можете создать/удалить таблицу вручную: MyFakeModel.create_table()/MyFakeModel.delete_table()

10
Kirill Ermolov

Цитирование из связанный ответ :

Если вы хотите, чтобы модели были определены только для тестирования, вам следует проверить Билет Django № 7835 в частности комментарий № 24 часть которого дается ниже:

По-видимому, вы можете просто определить модели непосредственно в вашем tests.py . Syncdb никогда не импортирует tests.py, поэтому эти модели не будут синхронизированы с обычный db, но они синхронизируются с тестовой базой данных и могут быть используется в тестах.

9
joeharrie

Я придумала способ использовать только тестовые модели для Django 1.7+.

Основная идея заключается в том, чтобы сделать ваше tests приложением и добавить свой tests в INSTALLED_APPS.

Вот пример:

$ ls common
__init__.py   admin.py      apps.py       fixtures      models.py     pagination.py tests         validators.py views.py

$ ls common/tests
__init__.py        apps.py            models.py          serializers.py     test_filter.py     test_pagination.py test_validators.py views.py

И у меня есть разные settings для разных целей (ref: разделение файла настроек ), а именно:

  • settings/default.py: файл базовых настроек
  • settings/production.py: для производства
  • settings/development.py: для разработки
  • settings/testing.py: для тестирования.

А в settings/testing.py вы можете изменить INSTALLED_APPS:

settings/testing.py:

from default import *

DEBUG = True

INSTALLED_APPS += ['common', 'common.tests']

И убедитесь, что вы установили правильную метку для своего тестового приложения, а именно: 

common/tests/apps.py

from Django.apps import AppConfig


class CommonTestsConfig(AppConfig):
    name = 'common.tests'
    label = 'common_tests'

common/tests/__init__.py, установите правильную AppConfig (ref: Django Applications ).

default_app_config = 'common.tests.apps.CommonTestsConfig'

Затем сгенерируйте миграцию БД с помощью

python manage.py makemigrations --settings=<your_project_name>.settings.testing tests

Наконец, вы можете запустить свой тест с параметром --settings=<your_project_name>.settings.testing

Если вы используете py.test, вы можете даже удалить файл pytest.ini вместе с manage.py в Django.

py.test

[pytest]
Django_SETTINGS_MODULE=kungfu.settings.testing
9
Xiao Hanyu

Я выбрал немного другой, хотя и более связанный, подход к динамическому созданию моделей только для тестирования. 

Я храню все свои тесты в подкаталоге tests, который находится в моем приложении files. Файл models.py в подкаталоге tests содержит мои модели только для тестирования. Связанная часть приходит сюда, где мне нужно добавить следующее в мой файл settings.py:

# check if we are testing right now
TESTING = 'test' in sys.argv

if TESTING:
    # add test packages that have models
    INSTALLED_APPS += ['files.tests',]

Я также установил db_table в своей тестовой модели, потому что в противном случае Django создал бы таблицу с именем tests_<model_name>, что могло вызвать конфликт с другими тестовыми моделями в другом приложении. Вот моя моя тестовая модель:

class Recipe(models.Model):

    '''Test-only model to test out thumbnail registration.'''

    dish_image = models.ImageField(upload_to='recipes/')

    class Meta:
        db_table = 'files_tests_recipe'
9
Jashugan

Вот шаблон, который я использую для этого. 

Я написал этот метод, который я использую в подклассовой версии TestCase. Это выглядит следующим образом:

@classmethod
def create_models_from_app(cls, app_name):
    """
    Manually create Models (used only for testing) from the specified string app name.
    Models are loaded from the module "<app_name>.models"
    """
    from Django.db import connection, DatabaseError
    from Django.db.models.loading import load_app

    app = load_app(app_name)
    from Django.core.management import sql
    from Django.core.management.color import no_style
    sql = sql.sql_create(app, no_style(), connection)
    cursor = connection.cursor()
    for statement in sql:
        try:
            cursor.execute(statement)
        except DatabaseError, excn:
            logger.debug(excn.message)
            pass

Затем я создаю специальный тестовый файл models.py в чем-то вроде myapp/tests/models.py, который не включен в INSTALLED_APPS. 

В моем методе setUp я вызываю create_models_from_app ('myapp.tests'), и он создает соответствующие таблицы.

Единственная «ошибка» в этом подходе заключается в том, что вы не хотите создавать модели при запуске setUp, поэтому я ловлю DatabaseError. Я предполагаю, что вызов этого метода может идти вверху тестового файла, и это будет работать немного лучше. 

4
slacy

Объединив ваши ответы, особенно @ slacy's, я сделал это:

class TestCase(test.TestCase):
    initiated = False

    @classmethod
    def setUpClass(cls, *args, **kwargs):
        if not TestCase.initiated:
            TestCase.create_models_from_app('myapp.tests')
            TestCase.initiated = True

        super(TestCase, cls).setUpClass(*args, **kwargs)

    @classmethod
    def create_models_from_app(cls, app_name):
        """
        Manually create Models (used only for testing) from the specified string app name.
        Models are loaded from the module "<app_name>.models"
        """
        from Django.db import connection, DatabaseError
        from Django.db.models.loading import load_app

        app = load_app(app_name)
        from Django.core.management import sql
        from Django.core.management.color import no_style
        sql = sql.sql_create(app, no_style(), connection)
        cursor = connection.cursor()
        for statement in sql:
            try:
                cursor.execute(statement)
            except DatabaseError, excn:
                logger.debug(excn.message)

При этом вы не пытаетесь создавать таблицы БД более одного раза, и вам не нужно менять свой INSTALLED_APPS.

3
zVictor

Если вы пишете повторно используемое Django-приложение, создайте для него минимальное тестовое приложение!

$ Django-admin.py startproject test_myapp_project
$ Django-admin.py startapp test_myapp

добавьте и myapp, и test_myapp к INSTALLED_APPS, создайте свои модели там, и это хорошо!

Я прошел через все эти ответы, а также билет Джанго 7835 , и, наконец, я пошел совершенно другим подходом. Я хотел, чтобы мое приложение (каким-то образом расширяющее queryset.values ​​()) можно было тестировать изолированно; Кроме того, мой пакет включает в себя некоторые модели, и я хотел провести четкое различие между тестовыми и пакетными моделями.

Именно тогда я понял, что было бы проще добавить в пакет очень маленький проект Django! Это также позволяет намного чище разделять код IMHO:

Там вы можете четко и безо всякого взлома определить ваши модели, и вы знаете, что они будут созданы при запуске ваших тестов оттуда!

Если вы не пишете независимое, повторно используемое приложение, вы все равно можете пойти по этому пути: создать приложение test_myapp и добавить его в INSTALLED_APPS только в отдельном settings_test_myapp.py!

1
Stefano

Кто-то уже упоминал билет Django № 7835 , но, похоже, есть более свежий ответ, который выглядит гораздо более многообещающим для более свежих версий Django. В частности # 42 , который предлагает другую TestRunner:

from importlib.util import find_spec
import unittest

from Django.apps import apps
from Django.conf import settings
from Django.test.runner import DiscoverRunner


class TestLoader(unittest.TestLoader):
    """ Loader that reports all successful loads to a runner """
    def __init__(self, *args, runner, **kwargs):
        self.runner = runner
        super().__init__(*args, **kwargs)

    def loadTestsFromModule(self, module, pattern=None):
        suite = super().loadTestsFromModule(module, pattern)
        if suite.countTestCases():
            self.runner.register_test_module(module)
        return suite


class RunnerWithTestModels(DiscoverRunner):
    """ Test Runner that will add any test packages with a 'models' module to INSTALLED_APPS.
        Allows test only models to be defined within any package that contains tests.
        All test models should be set with app_label = 'tests'
    """
    def __init__(self, *args, **kwargs):
        self.test_packages = set()
        self.test_loader = TestLoader(runner=self)
        super().__init__(*args, **kwargs)

    def register_test_module(self, module):
        self.test_packages.add(module.__package__)

    def setup_databases(self, **kwargs):
        # Look for test models
        test_apps = set()
        for package in self.test_packages:
            if find_spec('.models', package):
                test_apps.add(package)
        # Add test apps with models to INSTALLED_APPS that aren't already there
        new_installed = settings.INSTALLED_APPS + Tuple(ta for ta in test_apps if ta not in settings.INSTALLED_APPS)
        apps.set_installed_apps(new_installed)
        return super().setup_databases(**kwargs)
0
André Fratelli