36

I need to write some unit tests for an abstract base model, that provides some basic functionality that should be used by other apps. It would be necessary to define a model that inherits from it just for testing purposes. Are there any elegant/simple ways to define that model just for testing?

I have seen some "hacks" that make this possible, but never seen an "official" way in the Django documentation or in other similar places.

cezar
  • 11,616
  • 6
  • 48
  • 84
Bernhard Vallant
  • 49,468
  • 20
  • 120
  • 148

14 Answers14

26

Just stumbled across this feature myself: You can just inherit from your abstract model in tests.py and test that as usual. When you run 'manage.py tests', Django not only creates a test database, but also validates & syncs your test models.

Tested it with current Django trunk (version 1.2).

jrobichaud
  • 1,272
  • 1
  • 11
  • 23
maikhoepfel
  • 269
  • 2
  • 2
  • 9
    Just one thing to add: if your tests are in a folder, rather than just in tests.py (and mine never just fit in one file), then you'll need to have the Meta inner class, with your app_label set (just like if you had split your models.py file). – Matthew Schinckel Dec 18 '10 at 07:32
  • 2
    Sounds good... but it does not work in my case. Placing the inherited class in models.py works as normal, but placing the class in tests.py will not get the "syncdb for tests" create the table. To be clear: I only want this table for testing. Using Django 1.2.3. Any ideas? Note: using django-nose test runner. Maybe it behaves differently (looking into it right now). – Jack Ha Aug 16 '11 at 10:26
  • 2
    Update: indeed in the django-nose runner the error occurs, but using the standard django test runner it works fine. – Jack Ha Aug 16 '11 at 11:42
  • There's a django-nose issue at https://github.com/jbalogh/django-nose/issues/15 with some background and some fixes. – Reinout van Rees Oct 14 '11 at 08:57
  • 1
    Working with django 1.6.0 – Jocelyn delalande Dec 27 '13 at 10:31
  • 1
    It's raising an error "django.db.utils.OperationalError: no such table: myapp_childclass" (which makes sense because the class that inherits the abstract one has not gone through makemigrations. – Overdrivr Sep 29 '20 at 10:22
15

Updated for Django >=2.0

So I was running into a few problems using m4rk4l's answer: one being the 'RuntimeWarning: Model 'myapp.__test__mymodel' was already registered' issue brought up in one of the comments, another being tests failing because the table already exists.

I've added a few checks to help solve these issues and now it works flawlessly. I hope this helps people

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

To use, implement the same way as above (now with the correcting indentation):

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)
Dustin Gault
  • 157
  • 1
  • 5
  • 1
    I think `OperationalError` should be `ProgrammingError`? – LondonAppDev Aug 10 '18 at 13:17
  • I don't think so? OperationalError: This exception is raised for things that are not in control of the programmer. For e.g unexpected disconnect, error in memory allocation etc, selected database not exists. ProgrammingError: This exception is raised of programming errors. For e.g table not found, error in mysql syntax, wrong number of parameters specified etc. Taken from: [https://thepythonguru.com/handling-errors/] – Dustin Gault Nov 26 '18 at 23:03
  • 1
    So the database actually makes a difference here: Mysql: OperationalError Postgresql: ProgrammingError – Dustin Gault Mar 26 '19 at 00:41
11

I have the same situation as well. I ended up using a version of @dylanboxalot solution. Got extra details from here specifically after reading 'Test structure overview' section.

The setUp and the tearDown methods are called each time a tests is run. A better solution is to run the creation of the 'abstract' model once, before all the tests are run. To do so, you can implement the setUpClassData and also implement the 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()

A possible implementation may look like this:

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

Maybe ModelMixinTestCase class should be added to Django? :P

cezar
  • 11,616
  • 6
  • 48
  • 84
m4rk4l
  • 145
  • 1
  • 9
  • Why `setUpTestData` and not `setUpClass`? The Django documentation states that `setUpTestData` is for providing initial data. – cezar Aug 25 '17 at 11:05
  • You are right!, ill change the method. I found more info in this answer: [link](https://stackoverflow.com/questions/43594519/testing-in-django-what-are-differences-between-setupclass-setuptestdata-and-se) – m4rk4l Aug 25 '17 at 12:47
  • Using this I get an errror: `AttributeError: type object 'MyModelTestCase' has no attribute 'cls_atomics'`. Reverting `setUpClass` to `setUpTestData` solves the problem. So my comment introduced the error. I'm sorry because of this, the documentation was somewhat missleading and the linked SO answer supported my objection. – cezar Aug 28 '17 at 06:45
  • I think too, that there should be an integrated solution in Django for testing abstract model classes. This should be the accepted answer. It's the most elegant solution. Unfortunately I can upvote only once. – cezar Aug 28 '17 at 06:48
  • My bad with `setUpClass`. The parent method should be called: `super(ModelMixinTestCase, cls).setUpClass()`. But when overriding `setUpTestData` there is no need to call the parent method, as it contains only `pass`. – cezar Sep 19 '17 at 09:00
  • 1
    Raises error on teardown: `E django.db.utils.NotSupportedError: SQLite schema editor cannot be used while foreign key constraint checks are enabled. Make sure to disable them before entering a transaction.atomic() context because SQLite does not support disabling them in the middle of a multi-statement transaction.`. Solution in this thread : https://stackoverflow.com/questions/57583985/testing-abstract-models-django-2-2-4-sqlite3-2-6-0 – Overdrivr Sep 29 '20 at 10:35
8

I stumbled across this recently and wanted to update it for newer Django versions (1.9 and later) You can use the SchemaEditor's create_model instead of the outdated sql_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)
dylanboxalot
  • 129
  • 1
  • 6
  • 2
    I am getting `django/db/models/base.py:325: RuntimeWarning: Model 'myapp.__test__mymodel' was already registered` when the second test method in my test class runs. Shouldn't the tearDown method be preventing this? – Ryan Allen Aug 16 '17 at 23:39
7

I think what you are looking for is something like this.

This is the full code from the link:

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)                                 
simlmx
  • 999
  • 12
  • 17
  • 1
    this goes to a Dead Link – Wade Williams May 22 '14 at 22:17
  • But, how do to use it? I mean, nice, I extend... now what? – anizzomc Feb 11 '15 at 19:45
  • 1
    In the example, you would just set the `self.mixin` attribute to whatever abstract class you want to test. The `setUp` will then create a subclass to your abstract class (`self.model`) and add it to the database. Then you can add methods to `ModelMixinTestCase` that actually test the functionalities of your abstract class, by testing them on `self.model`. – simlmx Feb 12 '15 at 02:00
  • 1
    here are the imports for the code above. `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` – Bagee Sep 14 '15 at 17:11
  • Using the sample code requires the following: 1) extend `ModelMixingTestCase`, 2) override `setUp` and do this: `self.mixin = MyClass` 3) call `super` like this (Python 2.7): `super(TestMyClass, self).setUp()` where `TestMyClass` is the name of my class inheriting from `ModelMixinTestCase` – Max Heiber Nov 17 '15 at 02:31
  • @simlmx, I keep getting this warning when using multiple test cases: `RuntimeWarning: Model 'shared.__testmodel__timestamped' was already registered. Reloading models is not advised as it can lead to inconsistencies, most notably with related models.` How can I stop Django from re-registering my test models every time? – Max Heiber Nov 29 '15 at 20:49
  • Thanks for the answer. A small nitpick, I think closing the cursor is safer (less likely to interferes the tests). i.e., `with connection.cursor() as c: \n\t statement in sql: \n\t\t c.execute(statement)`. – Thomas - BeeDesk Dec 29 '15 at 20:42
  • `sql_create_model` is deprecated and removed in latest django versions – Overdrivr Sep 29 '20 at 10:28
3

Develop a minimal example app that you distribute with your 'abstract' models. Provide tests for the example app to prove the abstract models.

hash1baby
  • 131
  • 2
3

Maikhoepfel's answer is correct and most of the others seem unnecessarily complex. I wanted to provide further clarification as the other more complex answers seem to be quite popular.

project/
├─ app1/
├─ app2/
│  ├─ tests/
│  │  ├─ __init__.py
│  │  ├─ models.py
│  │  ├─ test_models.py
│  ├─ __init__.py
│  ├─ apps.py
│  ├─ models.py

Given the above project structure a model in app2.tests.models that inherits from app2.models.YourAbstractModel will be available for use in any tests (eg. app2.tests.test_models) without the need to run migrations.

Examples of this can be seen in Django test source code.

bdoubleu
  • 5,568
  • 2
  • 20
  • 53
  • 1
    I can see it in the Django test code... but I still get `psycopg2.errors.InvalidCursorName: cursor "_django_curs_139693788616512_sync_35" does not exist psycopg2.errors.InvalidCursorName: cursor "_django_curs_139693788616512_sync_35" does not exist` when I try this myself... not sure what I am doing wrong. – tbrlpld May 08 '23 at 17:03
2

I came to this problem my self and my solution is on this gist django-test-abstract-models

you can use it like this:

1- subclass your django abstract models

2- write your test case like this:

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

3- if you didn't provide self.models attribute it will search the current app for models in the path myapp.tests.models.*

MYaser
  • 369
  • 4
  • 14
  • does it work for you on Django 1.10, if yes is it possible that you may publish a small full working example, would be great? – Greeneco Sep 09 '16 at 17:09
1

In Django 2.2, if you only have one abstract class to test, you can use the following:

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's 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

This answer is only a tweaking of DSynergy's answer. One notable difference is that we are using setUpClass() instead of setUpTestData(). This difference is important since using the latter will result in InterfaceError (when using PostgreSQL) or the equivalent in other databases when the other test cases are run. As to the reason why this happens, I do not know at the time of writing.

NOTE: If you have more than one abstract class to test, it is better to use the other solutions.

Sean Francis N. Ballais
  • 2,338
  • 2
  • 24
  • 42
  • 1
    Just a quick note: if you are using a multi-db setup you can use ```from django.db import connections``` (note connections, not connection) and do something along the lines of ```with connections['other'].schema_editor() as schema_editor: ...```. – Jamie Williams Sep 10 '19 at 21:41
1

I tried solutions here but ran into issues like

RuntimeWarning: Model 'myapp.__test__mymodel' was already registered

Looking up how to test abstract models with pytest wasn't any successful either. I eventually came up with this solution that works perfectly for me:

import tempfile

import pytest
from django.db import connection, models
from model_mommy import mommy

from ..models import AbstractModel


@pytest.fixture(scope='module')
def django_db_setup(django_db_setup, django_db_blocker):
    with django_db_blocker.unblock():

        class DummyModel(AbstractModel):
            pass

        class DummyImages(models.Model):
            dummy = models.ForeignKey(
                DummyModel, on_delete=models.CASCADE, related_name='images'
            )
            image = models.ImageField()

        with connection.schema_editor() as schema_editor:
            schema_editor.create_model(DummyModel)
            schema_editor.create_model(DummyImages)


@pytest.fixture
def temporary_image_file():
    image = tempfile.NamedTemporaryFile()
    image.name = 'test.jpg'
    return image.name


@pytest.mark.django_db
def test_fileuploader_model_file_name(temporary_image_file):
    image = mommy.make('core.dummyimages', image=temporary_image_file)
    assert image.file_name == 'test.jpg'


@pytest.mark.django_db
def test_fileuploader_model_file_mime_type(temporary_image_file):
    image = mommy.make('core.dummyimages', image=temporary_image_file)
    assert image.file_mime_type == 'image/jpeg'

As you can see, I define a Class that inherits from the Abstractmodel, and add it as a fixture. Now with the flexibility of model mommy, I can create a DummyImages object, and it will automatically create a DummyModel for me too!

Alternatively, I could've made the example simple by not including foreign keys, but it demonstrates the flexibility of pytest and model mommy in combination quite well.

0

I thought I could share with you my solution, which is in my opinion much simpler and I do not see any cons.

Example goes for using two abstract classes.

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
KrazyMax
  • 939
  • 6
  • 16
0

Here is a working solution in django 3.0 with Postgres. It allows testing any number of abstract models and also maintains any integrity related to foreign objects.

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

# Category and Product are abstract models
from someApp.someModule.models import Category, Product, Vendor, Invoice

class MyModelsTestBase(TestCase):
    @classmethod
    def setUpTestData(cls):
        # keep track of registered fake models
        # to avoid RuntimeWarning when creating
        # abstract models again in the class
        cls.fake_models_registry = {}

    def setUp(self):
        self.fake_models = []

    def tearDown(self):
        try:
            with connection.schema_editor(atomic=True) as schema_editor:
                for model in self.fake_models:
                    schema_editor.delete_model(model)
        except ProgrammingError:
            pass

    def create_abstract_models(self, models: Union[list, tuple]):
        """
        param models: list/tuple of abstract model class
        """
        # by keeping model names same as abstract model names
        # we are able to maintain any foreign key relationship
        model_names = [model.__name__ for model in models]
        modules = [model.__module__ for model in models]
        for idx, model_name in enumerate(model_names):
            # if we already have a ModelBase registered
            # avoid re-registering.
            registry_key = f'{modules[idx]}.{model_name}'
            model_base = self.fake_models_registry.get(registry_key)
            if model_base is not None:
                self.fake_models.append(model_base)
                continue

            # we do not have this model registered
            # so register it and track it in our
            # cls.fake_models_registry            
            self.fake_models.append(
                ModelBase(
                    model_name,
                    (models[idx],),
                    {'__module__': modules[idx]}
                )
            )
            self.fake_models_registry[registry_key] = self.fake_models[idx]

        errors = []
        # atomic=True allows creating multiple models in the db
        with connection.schema_editor(atomic=True) as schema_editor:
            try:
                for model in self.fake_models:
                    schema_editor.create_model(model)
             except ProgrammingError as e:
                 errors.append(e)
                 pass
        return errors

    def test_create_abstract_models(self):
        abstract_models = (Category, Product)
        errors = self.create_abstract_models(abstract_models)
        self.assertEqual(len(errors), 0)

        category_model_class, product_model_class = self.fake_models

        # and use them like any other concrete model class:
        category = category_model_class.objects.create(name='Pet Supplies')
        product = product_model_class.objects.create(
            name='Dog Food', category_id=category.id
        )


Zul Qasar
  • 1
  • 1
0

Having read through all the answers above, I found out a solution that worked for me, in Django 3.1.1 with PostgreSQL 12.4 database.

from django.db import connection
from django.db.utils import ProgrammingError
from django.test import TestCase


class AbstractModelTestCase(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):
        if not hasattr(cls, "model"):
            super(AbstractModelTestCase, cls).setUpClass()
        else:
            # 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(AbstractModelTestCase, cls).setUpClass()
            except ProgrammingError:
                pass

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

It also gets rid of the annoying RuntimeWarning: Model 'xxx' was already registered warning.

Dharman
  • 30,962
  • 25
  • 85
  • 135
-6

Testing an abstract class is not too useful, as a derived class can override its methods. The other applications are responsible for testing their classes based on your abstract class.

  • 7
    Your abstract base model can be very rich in functionality, and you wan't to test all it's methods work properly. And you wan't to do it once. Otherwise, others would have to test the same code every time they derive from your abstract model. Those apps only need to test methods they have overriden, and only them. – Ivan Virabyan Aug 16 '13 at 12:17