How to create table during Django tests with managed = False

Question:

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.

Asked By: Derek Kwok

||

Answers:

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 ...")
Answered By: codeape

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.

Answered By: mohi666

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
Answered By: Mark Lavin

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)
Answered By: dnozay

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()
Answered By: WojtylaCz

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 Testing django application with several legacy databases)

MIGRATION_MODULES = {
    'news': 'news.test_migrations',
    'economist': 'economist.test_migrations'
}
Answered By: Akavir

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

Answered By: illagrenan

Simply move your unmanaged models to a dedicated app and delete migrations folder. Details in my answer in Multi db and unmanged models – Test case are failing

Answered By: Michel Samia

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)
Answered By: tarikki

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.

Answered By: Vinicius Chan
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.