Django: Best way to unit-test an abstract model - django

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.

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

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)

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

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)

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)

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

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.

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

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.

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.

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

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
)

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.

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.

Related

use dependency injection with flask-sqlalchemy

I am looking for a way to use simple dependency injection in combination with flask-sqlalchemy
Here's an example setup
extensions.py
db = SQLAlchemy()
my_model.py
from extensions import db
class MyModel(db.Model):
id = db.Column(db.Integer, primary_key=True)
// ... some more fields
my_model_repository.py
from my_model import MyModel
class MyModelRepository:
def get_all():
MyModel.query.all()
Now I would like to somehow inject the db into MyModelRepository so that when I unit test it I can inject a mock or an in-memory database without having to deal with any global config or flags.
Here's an example of how I would want it to look
from my_model import MyModel
class MyModelRepository:
def __init__(self, db):
self.db = db
def get_all():
db.query(MyModel).all()
This however seems to not be possible because MyModel inherits from db.Model and db already is a specific instance of SQLAlchemy.
What is the appropriate way to make the database injectable to any component that depends on it?
For me it worked something like this:
In my_model_repository.py add the inject decorator to the init of your class
from my_model import MyModel
from flask_sqlalchemy import SQLAlchemy
class MyModelRepository:
#inject
def __init__(self, db: SQLAlchemy):
self.db = db
def get_all():
db.query(MyModel).all()
and in your app.py or another source file where you create your flask app add:
import flask_injector
from extensions import db
app = Flask(__name__) #something like this should already be in your code
db.init_app(app)
#this function could potentially be moved to it's own file
def configure_dependencies(binder):
binder.bind(SQLAlchemy, to=db, scope=flask_injector.singleton)
flask_injector.FlaskInjector(app=app, modules=[configure_dependencies])
Data sources:
https://pypi.org/project/Flask-Injector/
https://www.pythonfixing.com/2022/04/fixed-circular-import-of-db-reference.html

Factory Boy test with Wagtail fails: TypeError: Field 'id' expected a number but got {}

I'm trying to set up Factory Boy for a Wagtail site. In order for that to work, the _create method must be able to establish a parent-child-relationship between pages. I override the _create method as described in the docs: https://factoryboy.readthedocs.io/en/latest/reference.html#attributes-and-methods. But the addition of the 'parent' key causes the error.
Have been looking at it for ages, dug into the source files. I suspect it has to do with the way the model_class method passes the kwargs, but I guess I'm blind for any obvious mistakes by now. Any help would greatly be appreciated!
models.py:
from wagtail.core.models import Page
class HomePage(Page):
pass
class SubjectPage(Page):
pass
tests.py:
from .factories import HomePageFactory, SubjectPageFactory
from django.test import TestCase
class TestModels(TestCase):
#classmethod
def setUpTestData(cls):
cls.homepage = HomePageFactory(title='Test page')
cls.subjectpage = SubjectPageFactory(parent=cls.homepage)
def test_dummy(self):
self.assertTrue(True)
factories.py:
import factory
from .models import HomePage, SubjectPage
from wagtail.core.models import Page
class PageFactory(factory.django.DjangoModelFactory):
class Meta:
abstract = True
#classmethod
def _create(cls, model_class, *args, **kwargs):
try:
parent = kwargs.pop('parent')
page = model_class(*args, kwargs)
except KeyError:
parent = Page.get_first_root_node()
page = model_class(*args, **kwargs)
parent.add_child(instance=page)
return page
class HomePageFactory(PageFactory):
class Meta:
model = HomePage
class SubjectPageFactory(PageFactory):
class Meta:
model = SubjectPage
Can I recommend wagtail_factories https://github.com/mvantellingen/wagtail-factories?
To set up:
pip install wagtail-factories
in factories.py
import wagtail_factories
from .models import HomePage, SubjectPage
class HomePageFactory(wagtail_factories.PageFactory):
class Meta:
model = HomePage
class SubjectPageFactory(wagtail_factories.PageFactory):
class Meta:
model = SubjectPage
in tests.py
from django.test import TestCase
class PagesTestCase(TestCase):
def test_create(self):
instance = HomePageFactory.create()
self.assertIsNotNone(instance.pk)
If you want to test pages with some structure, the code below would create a homepage as a root of the site, and create a subject page as a child of the homepage.
from wagtail.core.models import Site
from .factories import HomePageFactory, SubjectPageFactory
class PageTestCase(TestCase):
def setUp(self):
self.homepage = HomePageFactory.create()
self.site = Site.objects.all().first()
self.site.root_page = self.homepage
self.site.save()
self.site.refresh_from_db()
self.subject_page = SubjectPageFactory.create(parent=self.homepage)

How to additional field to Django Oscar Order model?

I want add additional field to Order model. i have already forked order app.
below is the code added in Order model in forked_app Order app.
from django.utils import timezone
from oscar.apps.order.abstract_models import AbstractOrder
from oscar.apps.order.models import * # noqa isort:skip
from django.db import models
class Order(AbstractOrder):
status_update_time = models.CharField(max_length=30)
def save(self, *args, **kwargs):
self.status_update_time = timezone.now()
super(Order, self).save(*args, **kwargs)
Below is the error i get while migrations.
class Order(AbstractOrder):
NameError: name 'AbstractOrder' is not defined
Unhandled exception in thread started by <function check_errors.<locals>.wrapper at 0x7f06737e6d08>
RuntimeError: Conflicting 'order' models in application 'order': <class 'oscar.apps.order.models.Order'> and <class 'forked_apps.order.models.Order'>.
from django.utils import timezone
from oscar.apps.order.abstract_models import AbstractOrder
from django.db import models
class Order(AbstractOrder):
status_update_time = models.CharField(max_length=30)
def save(self, *args, **kwargs):
self.status_update_time = timezone.now()
super(Order, self).save(*args, **kwargs)
at the end of the models.py file
from oscar.apps.order.models import *
then try to do makemigrations and then migrate
In order to customize models, views and urls, you need to fork an Oscar core app in which model/view resides. Then you should be able to override any model/view classes.
Steps to fork/customize an app:
If you are forking an Oscar app for the first time, then you have to create a root apps-folder in which all your forked apps will exists:
$ mkdir yourappsfolder
$ touch yourappsfolder/init.py
Create a python module with the same ‘app-label’ as the Oscar app:
Ex: Customising oscar.apps.catalogue app
$ mkdir yourappsfolder/catalogue
$ touch yourappsfolder/catalogue/__init__.py
If the Oscar app has a models.py, then you have to create a models.py file in your local app.
your custom models go here
from oscar.apps.catalogue.models import *
NOTE: To customise Oscar’s models, you must add your custom one before importing Oscar’s models. Then, your models file will have two models with the same name within an app, Django will only use the first one.
Ex: To add a active field to the product model:
# yourappsfolder/catalogue/models.py
from django.db import models
from oscar.apps.catalogue.abstract_models import AbstractProduct
class Product(AbstractProduct):
active = models.BooleanField(default=False)
from oscar.apps.catalogue.models import *
Create an ‘admin.py’ file in your local app.
yourappsfolder/catalogue/admin.py
from oscar.apps.catalogue.admin import *
Then copy the ‘migrations’ directory from oscar/apps/catalogue and put it into your new local catalogue app.
Added it as Django app by replacing Oscar’s app with your own in INSTALLED_APPS.
settings.py
from oscar import get_core_apps
INSTALLED_APPS = [
...,
# all your non-Oscar apps
] + get_core_apps(['yourappsfolder.catalogue'])
NOTE: get_core_apps([]) will return a list of Oscar core apps or else if you give a list of your custom apps, they will replace the Oscar core apps.
Finally, create migrations using ‘makemigrations’ management command and apply the migrations by using ‘migrate catalogue’ management command. Then, you can see that a new column is been added to the product model.

Django test setup not being used

I am using Django cookiecutter 1.11 for a project.
Trying to write some basic tests for a model. But the setup method is not being used in the test cases.
from django.test import TestCase
from myapp.users.models import User
from ..models import Book
class ModelTests(TestCase):
def setup(self):
self.username = 'john'
self.password = '123'
self.user = User.objects.create(name=self.username,
password=self.password
)
def test_create_book(self):
Book.objects.create(artist=self.user,
title=“An Art Book“,
category=“Art”,
)
self.assertEquals(Book.objects.all().count(), 1)
I get this error message after running manage.py test
Book.objects.create(artist=self.user,
AttributeError: 'ModelTests' object has no attribute 'user'
But it works when I put the lines from setup into the test case.
Did I miss something?
The method should be called setUp, not setup.

Django test model with FilerImageField

All new to Django, I want to write tests for an IndexView and a DetailView in analogy to the Django tutorial.
I have a model that contains a FilerImageField as mandatory field (blank=False).
In order to test my views related to that model, I want to create a model instance programmatically.
I am aware of this question addressing how to create a FilerImageField in code. The problem I run into applying the alleged solution is getting the part right about the image's owner.
def create_exhibitor(name, image_path, active):
filename = 'file'
user = User.objects.get(username='myuser')
with open(image_path) as f:
file_obj = File(f, name=filename)
image = Image.objects.create(
owner=user,
original_filename=filename,
file=file_obj
)
return Exhibitor(name=name, image=image, active=active)
Runnging them tests yields:
Traceback (most recent call last):
...
DoesNotExist: User matching query does not exist.
To me it appears there is no user in the test database.
So my question is twofold really:
Do I need a user there to create an instance of the model containing the FilerImageField?
If so, how do I create one for test purposes?
I'm finally doing it like so:
from django.test import TestCase
from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile
from .models import Exhibitor
class TestCase():
su_username = 'user'
su_password = 'pass'
def setUp(self):
self.superuser = self.create_superuser()
def create_superuser(self):
return User.objects.create_superuser(self.su_username, 'email#example.com', self.su_password)
def create_exhibitor(self):
name = 'eins'
active = True
image_file = SimpleUploadedFile(
'monkey.jpg', b'monkey', content_type="image/jpeg"
)
return Exhibitor(name=name, image=image_file, active=active)