Django: лучший способ модульного тестирования абстрактной модели

Мне нужно написать некоторые модульные тесты для абстрактной базовой модели, которая предоставляет некоторые базовые функции, которые должны использоваться другими приложениями. Было бы необходимо определить модель, которая наследуется от нее только для целей тестирования; существуют ли какие-либо элегантные/простые способы определения этой модели только для тестирования?

Я видел некоторые "хаки", которые делают это возможным, но никогда не видели "официального" способа в документации django или в других подобных местах.

Ответы

Ответ 1

Просто наткнулся на эту функцию сам: вы можете просто унаследовать свою абстрактную модель в tests.py и протестировать ее как обычно. Когда вы запускаете "manage.py tests", Django не только создает тестовую базу данных, но также проверяет и синхронизирует ваши тестовые модели.

Протестировал его с текущей магистралью Django (версия 1.2).

Ответ 2

У меня такая же ситуация. Я закончил тем, что использовал версию решения @dylanboxalot. Есть дополнительные детали из здесь специально после прочтения раздела "Тест структуры обзора".

setUp и tearDown вызываются при каждом запуске теста. Лучшее решение состоит в том, чтобы запустить создание "абстрактной" модели один раз, прежде чем будут выполнены все тесты. Для этого вы можете реализовать setUpClassData а также реализовать tearDownClass.

class ModelMixinTestCase(TestCase):
    '''
    Base class for tests of model mixins. To use, subclass and specify the
    mixin class variable. A model using the mixin will be made available in
    self.model
    '''
    @classmethod
    def setUpClass(cls):
        # Create a dummy model which extends the mixin
        cls.model = ModelBase('__TestModel__' +
            cls.mixin.__name__, (cls.mixin,),
            {'__module__': cls.mixin.__module__}
        )

        # Create the schema for  our test model
        with connection.schema_editor() as schema_editor:
            schema_editor.create_model(cls.model)
        super(ModelMixinTestCase, cls).setUpClass()

    @classmethod
    def tearDownClass(cls):
        # Delete the schema for the test model
        with connection.schema_editor() as schema_editor:
            schema_editor.delete_model(cls.model)
        super(ModelMixinTestCase, cls).tearDownClass()

Возможная реализация может выглядеть так:

class MyModelTestCase(ModelMixinTestCase):
    mixin = MyModel

    def setUp(self):
        # Runs every time a test is run.
        self.model.objects.create(pk=1)

    def test_my_unit(self):
        # a test
        aModel = self.objects.get(pk=1)
        ...

Может быть, класс ModelMixinTestCase должен быть добавлен в Django? :П

Ответ 3

Я наткнулся на это недавно и хотел обновить его для новых версий Django (1.9 и более поздних версий). Вместо устаревшего sql_create_model

вы можете использовать SchemaEditor create_model
from django.db import connection
from django.db.models.base import ModelBase
from django.test import TestCase


class ModelMixinTestCase(TestCase):
    """
    Base class for tests of model mixins. To use, subclass and specify
    the mixin class variable. A model using the mixin will be made
    available in self.model.
    """

    def setUp(self):
        # Create a dummy model which extends the mixin
        self.model = ModelBase('__TestModel__' + self.mixin.__name__, (self.mixin,), {'__module__': self.mixin.__module__})

        # Create the schema for our test model
        with connection.schema_editor() as schema_editor:
            schema_editor.create_model(self.model)

    def tearDown(self):
        # Delete the schema for the test model
        with connection.schema_editor() as schema_editor:
            schema_editor.delete_model(self.model)

Ответ 4

Я думаю, что вы ищете что-то вроде этого.

Это полный код по ссылке:

from django.test import TestCase
from django.db import connection
from django.core.management.color import no_style
from django.db.models.base import ModelBase

class ModelMixinTestCase(TestCase):                                         
    """                                                                     
    Base class for tests of model mixins. To use, subclass and specify      
    the mixin class variable. A model using the mixin will be made          
    available in self.model.                                                
    """                                                                     

    def setUp(self):                                                        
        # Create a dummy model which extends the mixin                      
        self.model = ModelBase('__TestModel__'+self.mixin.__name__, (self.mixin,),
            {'__module__': self.mixin.__module__})                          

        # Create the schema for our test model                              
        self._style = no_style()                                            
        sql, _ = connection.creation.sql_create_model(self.model, self._style)

        self._cursor = connection.cursor()                                  
        for statement in sql:                                               
            self._cursor.execute(statement)                                 

    def tearDown(self):                                                     
        # Delete the schema for the test model                              
        sql = connection.creation.sql_destroy_model(self.model, (), self._style)
        for statement in sql:                                               
            self._cursor.execute(statement)                                 

Ответ 5

Обновлено для Django> = 2.0

Поэтому я столкнулся с несколькими проблемами при использовании ответа m4rk4l: одна из них - это проблема "RuntimeWarning: Model" myapp.__ test__mymodel "уже была зарегистрирована", поднятая в одном из комментариев, другая - тесты не пройдены, поскольку таблица уже существует.

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

from django.db import connection
from django.db.models.base import ModelBase
from django.db.utils import OperationalError
from django.test import TestCase


class AbstractModelMixinTestCase(TestCase):
    """
    Base class for tests of model mixins/abstract models.
    To use, subclass and specify the mixin class variable.
    A model using the mixin will be made available in self.model
    """

@classmethod
def setUpTestData(cls):
    # Create a dummy model which extends the mixin. A RuntimeWarning will
    # occur if the model is registered twice
    if not hasattr(cls, 'model'):
        cls.model = ModelBase(
            '__TestModel__' +
            cls.mixin.__name__, (cls.mixin,),
            {'__module__': cls.mixin.__module__}
        )

    # Create the schema for our test model. If the table already exists,
    # will pass
    try:
        with connection.schema_editor() as schema_editor:
            schema_editor.create_model(cls.model)
        super(AbstractModelMixinTestCase, cls).setUpClass()
    except OperationalError:
        pass

@classmethod
def tearDownClass(self):
    # Delete the schema for the test model. If no table, will pass
    try:
        with connection.schema_editor() as schema_editor:
            schema_editor.delete_model(self.model)
        super(AbstractModelMixinTestCase, self).tearDownClass()
    except OperationalError:
        pass

Чтобы использовать, реализовать так же, как указано выше (теперь с корректирующим отступом):

class MyModelTestCase(AbstractModelMixinTestCase):
    """Test abstract model."""
    mixin = MyModel

    def setUp(self):
        self.model.objects.create(pk=1)

    def test_a_thing(self):
        mod = self.model.objects.get(pk=1)

Ответ 6

Разработайте минимальное примерное приложение, которое вы распространяете с помощью своих "абстрактных" моделей. Предоставьте тесты для примера приложения, чтобы доказать абстрактные модели.

Ответ 7

Я пришел к этой проблеме, и мое решение находится на этом gist django-test-abstract-models

вы можете использовать его следующим образом:

1- подкласс ваших абстрактных моделей django

2- напишите свой тестовый пример следующим образом:

class MyTestCase(AbstractModelTestCase):
    self.models = [MyAbstractModelSubClass, .....]
    # your tests goes here ...

3, если вы не указали атрибут self.models, он будет искать текущее приложение для моделей в пути myapp.tests.models.*

Ответ 8

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

Пример идет для использования двух абстрактных классов.

from django.db import connection
from django.db.models.base import ModelBase
from mailalert.models import Mailalert_Mixin, MailalertManager_Mixin

class ModelMixinTestCase(TestCase):   

    @classmethod
    def setUpTestData(cls):

        # we define our models "on the fly", based on our mixins
        class Mailalert(Mailalert_Mixin):
            """ For tests purposes only, we fake a Mailalert model """
            pass

        class Profile(MailalertManager_Mixin):
            """ For tests purposes only, we fake a Profile model """
            user = models.OneToOneField(User, on_delete=models.CASCADE, 
                related_name='profile', default=None)

        # then we make those models accessible for later
        cls.Mailalert = Mailalert
        cls.Profile = Profile

        # we create our models "on the fly" in our test db
        with connection.schema_editor() as editor:
            editor.create_model(Profile)
            editor.create_model(Mailalert)

        # now we can create data using our new added models "on the fly"
        cls.user = User.objects.create_user(username='Rick')
        cls.profile_instance = Profile(user=cls.user)
        cls.profile_instance.save()
        cls.mailalert_instance = Mailalert()
        cls.mailalert_instance.save()

# then you can use this ModelMixinTestCase
class Mailalert_TestCase(ModelMixinTestCase):
    def test_method1(self):
       self.assertTrue(self.mailalert_instance.method1())
       # etc

Ответ 9

В Django 2.2, если у вас есть только один абстрактный класс для тестирования, вы можете использовать следующее:

from django.db import connection
from django.db import models
from django.db.models.base import ModelBase
from django.db.utils import ProgrammingError
from django.test import TestCase

from yourapp.models import Base  # Base here is the abstract model.


class BaseModelTest(TestCase):
    @classmethod
    def setUpClass(cls):
        # Create dummy model extending Base, a mixin, if we haven't already.
        if not hasattr(cls, '_base_model'):
            cls._base_model = ModelBase(
                'Base',
                ( Base, ),
                { '__module__': Base.__module__ }
            )

            # Create the schema for our base model. If a schema is already
            # create then let not create another one.
            try:
                with connection.schema_editor() as schema_editor:
                    schema_editor.create_model(cls._base_model)
                super(BaseModelTest, cls).setUpClass()
            except ProgrammingError:
                # NOTE: We get a ProgrammingError since that is what
                #       is being thrown by Postgres. If we were using
                #       MySQL, then we should catch OperationalError
                #       exceptions.
                pass

            cls._test_base = cls._base_model.objects.create()

    @classmethod
    def tearDownClass(cls):
        try:
            with connection.schema_editor() as schema_editor:
                schema_editor.delete_model(cls._base_model)
            super(BaseModelTest, cls).tearDownClass()
        except ProgrammingError:
            # NOTE: We get a ProgrammingError since that is what
            #       is being thrown by Postgres. If we were using
            #       MySQL, then we should catch OperationalError
            #       exceptions.
            pass

Этот ответ - всего лишь доработка ответа DSynergy. Одно заметное отличие состоит в том, что мы используем setUpClass() вместо setUpTestData(). Это различие важно, так как использование последнего приведет к InterfaceError (при использовании PostgreSQL) или аналогичному в других базах данных при выполнении других тестовых случаев. Что касается причины, по которой это происходит, я не знаю на момент написания.

ПРИМЕЧАНИЕ. Если у вас есть несколько абстрактных классов для тестирования, лучше использовать другие решения.

Ответ 10

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