Testing unmanaged models - creating DB tables with SchemaEditor

Hi everyone!

I’d like to get your thoughts on something.

Unmanaged models means that Django no longer handles creating and managing schema at the database level (hence the name).
When running tests, this means these tables aren’t created, and we can’t run queries against that model. The general solution I found is to monkey-patch the TestSuiteRunner to temporarily treat models as managed.

Doing a bit of research I however came up with a solution using SchemaEditor, to create the model tables directly, viz:

"""
A cleaner approach to temporarily creating unmanaged model db tables for tests
"""

from unittest import TestCase

from django.db import connections, models

class create_unmanaged_model_tables:
    """
    Create db tables for unmanaged models for tests
    Adapted from: https://stackoverflow.com/a/49800437
    Examples:
        with create_unmanaged_model_tables(UnmanagedModel):
            ...
        @create_unmanaged_model_tables(UnmanagedModel, FooModel)
        def test_generate_data():
            ...
       
        @create_unmanaged_model_tables(UnmanagedModel, FooModel)
        def MyTestCase(unittest.TestCase):
            ...
    """

    def __init__(self, unmanaged_models: list[ModelBase], db_alias: str = "default"):
        """
        :param str db_alias: Name of the database to connect to, defaults to "default"
        """
        self.unmanaged_models = unmanaged_models
        self.db_alias = db_alias
        self.connection = connections[db_alias]

    def __call__(self, obj):
        if issubclass(obj, TestCase):
            return self.decorate_class(obj)
        return self.decorate_callable(obj)

    def __enter__(self):
        self.start()

    def __exit__(self, exc_type, exc_value, traceback):
        self.stop()

    def start(self):
        with self.connection.schema_editor() as schema_editor:
            for model in self.unmanaged_models:
                schema_editor.create_model(model)

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

    def stop(self):
        with self.connection.schema_editor() as schema_editor:
            for model in self.unmanaged_models:
                schema_editor.delete_model(model)

    def copy(self):
        return self.__class__(
            unmanaged_models=self.unmanaged_models, db_alias=self.db_alias
        )

    def decorate_class(self, klass):
        # Modify setUpClass and tearDownClass
        orig_setUpClass = klass.setUpClass
        orig_tearDownClass = klass.tearDownClass

        # noinspection PyDecorator
        @classmethod
        def setUpClass(cls):
            self.start()
            if orig_setUpClass is not None:
                orig_setUpClass()


        # noinspection PyDecorator
        @classmethod
        def tearDownClass(cls):
            if orig_tearDownClass is not None:
                orig_tearDownClass()
            self.stop()

        klass.setUpClass = setUpClass
        klass.tearDownClass = tearDownClass

        return klass

    def decorate_callable(self, callable_obj):
        @functools.wraps(callable_obj)
        def wrapper(*args, **kwargs):
            with self.copy():
                return callable_obj(*args, **kwargs)

        return wrapper

Would this make a good addition to django.test.utils?

I just responded to your copy of this post on the django-developers mailing list: https://groups.google.com/g/django-developers/c/_Nt263pPXgM/m/ptnrnpgfAQAJ

1 Like