How to create table during Django tests with managed = False

PythonDjangoUnit Testing

Python Problem Overview


I have a model with managed = False.

class SampleModel(models.Model):
    apple = models.CharField(max_length=30)
    orange = models.CharField(max_length=30)

    class Meta:
        managed = False

I have a unit test which creates a SampleModel, however when I run the test I get:

DatabaseError: no such table: SAMPLE_SAMPLE_MODEL

The django docs - https://docs.djangoproject.com/en/dev/ref/models/options/#managed documents the following:

> For tests involving models with managed=False, it's up to you to > ensure the correct tables are created as part of the test setup.

How can I actually "create" the tables during the test setup? Or alternatively, how can I make it so that when I am running tests, this model has "managed = True" for the duration of the test?

In the real application, this model is actually backed by a view in the database. However for the during of the test, I would like to treat this as a table and be able to insert test data in there.

Python Solutions


Solution 1 - Python

Check out this blog post: http://www.caktusgroup.com/blog/2010/09/24/simplifying-the-testing-of-unmanaged-database-models-in-django/ It describes in detail the creation of a test runner for unmanaged models.

from django.test.simple import DjangoTestSuiteRunner


class ManagedModelTestRunner(DjangoTestSuiteRunner):
    """
    Test runner that automatically makes all unmanaged models in your Django
    project managed for the duration of the test run, so that one doesn't need
    to execute the SQL manually to create them.
    """
    def setup_test_environment(self, *args, **kwargs):
        from django.db.models.loading import get_models
        self.unmanaged_models = [m for m in get_models()
                                 if not m._meta.managed]
        for m in self.unmanaged_models:
            m._meta.managed = True
        super(ManagedModelTestRunner, self).setup_test_environment(*args,
                                                                   **kwargs)

    def teardown_test_environment(self, *args, **kwargs):
        super(ManagedModelTestRunner, self).teardown_test_environment(*args,
                                                                      **kwargs)
        # reset unmanaged models
        for m in self.unmanaged_models:
            m._meta.managed = False

Solution 2 - Python

You can use SchemaEditor in TestCase.setUp method to explicitly create models with managed = False.

# models.py

from django.db import models


class Unmanaged(models.Model):
    foo = models.TextField()

    class Meta:
        # This model is not managed by Django
        managed = False
        db_table = 'unmanaged_table'

And in your tests:

# tests.py

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

from myapp.models import Unmanaged


class ModelsTestCase(TestCase):
    def setUp(self):
        super().setUp()

        with connection.schema_editor() as schema_editor:
            schema_editor.create_model(Unmanaged)

            if (
                Unmanaged._meta.db_table
                not in connection.introspection.table_names()
            ):
                raise ValueError(
                    "Table `{table_name}` is missing in test database.".format(
                        table_name=Unmanaged._meta.db_table
                    )
                )

    def tearDown(self):
        super().tearDown()

        with connection.schema_editor() as schema_editor:
            schema_editor.delete_model(Unmanaged)

    def test_unmanaged_model(self):
        with self.assertNumQueries(num=3):
            self.assertEqual(0, Unmanaged.objects.all().count())
            Unmanaged.objects.create()
            self.assertEqual(1, Unmanaged.objects.all().count())

Solution 3 - Python

Execute raw SQL to create the table in the test setup:

from django.db import connection

class MyTest(unittest.TestCase):
    def setUp(self):
        connection.cursor().execute("CREATE TABLE ...")

    def tearDown(self):
        connection.cursor().execute("DROP TABLE ...")

Solution 4 - Python

Nice plug and play solution. Just paste this before your test class definition. (note: django 1.8 used)

from django.db.models.loading import get_models

def change_managed_settings_just_for_tests():
  """django model managed bit needs to be switched for tests."""    

  unmanaged_models = [m for m in get_models() if not m._meta.managed]
  for m in unmanaged_models:
    m._meta.managed = True

change_managed_settings_just_for_tests()

Solution 5 - Python

A quick fix if you don't have many unmanaged tables:

First add a new variable to the settings.

# settings.py
import sys
UNDER_TEST = (len(sys.argv) > 1 and sys.argv[1] == 'test')

then in the models

# models.py
from django.conf import settings

class SampleModel(models.Model):
    apple = models.CharField(max_length=30)
    orange = models.CharField(max_length=30)

    class Meta:
        managed = getattr(settings, 'UNDER_TEST', False)

Solution 6 - Python

Create your own test runner using this:

from django.test.simple import DjangoTestSuiteRunner

class NoDbTestRunner(DjangoTestSuiteRunner):
  """ A test runner to test without database creation """

  def setup_databases(self, **kwargs):
    """ Override the database creation defined in parent class """
    #set manage=True for that specific database on here

Then on your settings add this class to TEST_RUNNER.

Solution 7 - Python

Just to add :django.db.models.loading.get_models will be removed in Django 1.9 (see https://github.com/BertrandBordage/django-cachalot/issues/33).

Below is an updated one for Django 1.10:

class UnManagedModelTestRunner(DiscoverRunner):
    '''
    Test runner that automatically makes all unmanaged models in your Django
    project managed for the duration of the test run.
    Many thanks to the Caktus Group 
    '''

    def setup_test_environment(self, *args, **kwargs):
        from django.apps  import apps
        self.unmanaged_models = [m for m in apps.get_models() if not m._meta.managed]
        for m in self.unmanaged_models:
            m._meta.managed = True
        super(UnManagedModelTestRunner, self).setup_test_environment(*args, **kwargs)

    def teardown_test_environment(self, *args, **kwargs):
        super(UnManagedModelTestRunner, self).teardown_test_environment(*args, **kwargs)
        # reset unmanaged models
        for m in self.unmanaged_models:
            m._meta.managed = False 

Note you also need to take care migrations(see https://stackoverflow.com/questions/36986369/testing-django-application-with-several-legacy-databases)

MIGRATION_MODULES = {
    'news': 'news.test_migrations',
    'economist': 'economist.test_migrations'
}

Solution 8 - Python

Using pytest and pytest-django

To make this work (has been tested with django 3.0.2, pytest 5.3.5 and pytest-django 3.8.0):

  1. Run your pytest with the additional argument --no-migrations.
  2. Put the following code in your conftest.py. This has an unfortunate amount of copypasta, but I could not figure out how to first make my models unmanaged and then call the original django_db_setup. The issue of not being able to call pytest fixtures directly is discussed here: https://github.com/pytest-dev/pytest/issues/3950

conftest.py

# example file
import pytest
from pytest_django.fixtures import _disable_native_migrations


@pytest.fixture(scope="session")
def django_db_setup(
        request,
        django_test_environment,
        django_db_blocker,
        django_db_use_migrations,
        django_db_keepdb,
        django_db_createdb,
        django_db_modify_db_settings,
):
    # make unmanaged models managed
    from django.apps import apps
    unmanaged_models = []
    for app in apps.get_app_configs():
        unmanaged_models = [m for m in app.get_models()
                            if not m._meta.managed]
    for m in unmanaged_models:
        m._meta.managed = True

    # copypasta fixture code

    """Top level fixture to ensure test databases are available"""
    from pytest_django.compat import setup_databases, teardown_databases

    setup_databases_args = {}

    if not django_db_use_migrations:
        _disable_native_migrations()

    if django_db_keepdb and not django_db_createdb:
        setup_databases_args["keepdb"] = True

    with django_db_blocker.unblock():
        db_cfg = setup_databases(
            verbosity=request.config.option.verbose,
            interactive=False,
            **setup_databases_args
        )

    def teardown_database():
        with django_db_blocker.unblock():
            try:
                teardown_databases(db_cfg, verbosity=request.config.option.verbose)
            except Exception as exc:
                request.node.warn(
                    pytest.PytestWarning(
                        "Error when trying to teardown test databases: %r" % exc
                    )
                )

    if not django_db_keepdb:
        request.addfinalizer(teardown_database)

Solution 9 - Python

After spending a few hours testing and researching ways to test my django unmanaged models, I finally came up with a solution that worked for me.
My implementation is in this below snippet. It's working great with local tests using db.sqlite3.

# Example classes Implementation

from django.db import models, connection
from django.db.models.base import ModelBase as DjangoModelBase
from django.db.utils import OperationalError
from django.test import TransactionTestCase


class AbstractModel(models.Model):
    name = models.TextField(db_column="FIELD_NAME", blank=True, null=True)

    class Meta:
        managed = False
        abstract = True


class TestModel(AbstractModel):
    test_field = models.TextField(db_column="TEST_FIELD", blank=True, null=True)

    def test_method(self):
        print("just testing")

    class Meta(AbstractModel.Meta):
        db_table = "MY_UNMANAGED_TABLE_NAME"



# My Custom Django TestCases Implementation for my tests

def create_database(model):
    with connection.schema_editor() as schema_editor:
        try:
            schema_editor.create_model(model)
        except OperationalError:
            pass


def drop_database(model):
    with connection.schema_editor() as schema_editor:
        try:
            schema_editor.delete_model(model)
        except OperationalError:
            pass


class BaseModelTestCase(TransactionTestCase):
    """Custom TestCase for testing models not managed by django."""

    Model = DjangoModelBase

    def setUp(self):
        super().setUp()
        create_database(self.Model)

    def tearDown(self):
        super().tearDown()
        drop_database(self.Model)


class AbstractBaseModelTestCase(TransactionTestCase):
    """Custom TestCase for testing abstract django models."""

    Model = DjangoModelBase

    def setUp(self):
        # this is necessary for testing an abstract class
        self.Model = DjangoModelBase(
            "__TestModel__" + self.Model.__name__,
            (self.Model,),
            {"__module__": self.Model.__module__},
        )
        create_database(self.Model)

    def tearDown(self):
        drop_database(self.Model)



# Example of usage

class TestModelTestCase(BaseModelTestCase):
    Model = TestModel

    def setUp(self):
        super().setUp()
        self.instance = TestModel.objects.create()
        self.assertIsInstance(self.instance, TestModel)


class AbstractModelTestCase(AbstractBaseModelTestCase):
    Model = AbstractModel

    def setUp(self):
        super().setUp()
        self.instance = AbstractModel.objects.create()
        self.assertIsInstance(self.instance, AbstractModel)

p.s. I am not using any Django migrations as soon as my models are managed=False. So this was not necessary for me.

Solution 10 - Python

Simply move your unmanaged models to a dedicated app and delete migrations folder. Details in my answer in https://stackoverflow.com/questions/58171340/multi-db-and-unmanged-models-test-case-are-failing/58432212#58432212

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDerek KwokView Question on Stackoverflow
Solution 1 - PythonMark LavinView Answer on Stackoverflow
Solution 2 - PythonillagrenanView Answer on Stackoverflow
Solution 3 - PythoncodeapeView Answer on Stackoverflow
Solution 4 - PythonWojtylaCzView Answer on Stackoverflow
Solution 5 - PythondnozayView Answer on Stackoverflow
Solution 6 - Pythonmohi666View Answer on Stackoverflow
Solution 7 - PythonAkavirView Answer on Stackoverflow
Solution 8 - PythontarikkiView Answer on Stackoverflow
Solution 9 - PythonVinicius ChanView Answer on Stackoverflow
Solution 10 - PythonMichel SamiaView Answer on Stackoverflow