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
?