今回はDjangoのAbstract Modelを直接テストする方法を解説する。

次のようなモデル定義を想定とする。 Postがモデルとなるが、その基底クラスとしてPostBaseを定義している。 今回はこの基底クラスを単体でテストしたいとする。

blog/models.py

from django.db import models
from django.utils import timezone

from django.contrib.auth import get_user_model

UserModel = get_user_model()


class PostBase(models.Model):
    class Meta:
        abstract = True

    user = models.ForeignKey(UserModel, on_delete=models.CASCADE)
    published = models.BooleanField(blank=True, null=True, default=False)

    def mark_publish(self):
        self.published = True


class Post(PostBase):
    title = models.CharField(max_length=200)

Abstract Model自体はテーブルを持たないため、単体ではDBの操作を行えない。 そこでテストクラス内で一時的なモデルを作成する。 そのために以下の関数を準備した。 create_test_model()は一時的なモデルを生成する。 register_test_model()は生成した一時的なモデルをDjangoに登録する。 unregister_test_model()はDjangoに登録した一時的なモデルの登録を削除する。

blog/tests/testing.py

from django.db import connection
from django.db.models.base import ModelBase


def create_test_model(abstract_model_class):
    return ModelBase(
        "__TestModel__" + abstract_model_class.__name__,
        (abstract_model_class,),
        {"__module__": abstract_model_class.__module__},
    )


def register_test_model(model):
    with connection.schema_editor() as schema_editor:
        schema_editor.create_model(model)


def unregister_test_model(model):
    """
    登録したテスト用のモデルを削除するために作成したがForeignKeyなどの
    リレーションがある場合リレーション先のモデルのメタ情報にフィールド
    が残ってしまいレコードの削除に失敗するようになった。
    """
    with connection.schema_editor() as schema_editor:
        # del model._meta.apps.app_configs["blog"].models["__testmodel__postbase"]
        # schema_editor.remove_field(model, field)
        for field in model._meta.get_fields():
            schema_editor.remove_field(model, field)
        schema_editor.delete_model(model)
    pass

前述の関数を以下のようにテストの実行前に呼び出すようにテストを実装する。

blog/tests/test_models.py

from blog.models import PostBase
from django.contrib.auth import get_user_model
from django.test import TestCase

from .testing import create_test_model, register_test_model

UserModel = get_user_model()


class PostBaseTest(TestCase):
    @classmethod
    def setUpClass(cls):
        cls.model = create_test_model(PostBase)
        register_test_model(cls.model)

    @classmethod
    def tearDownClass(cls):
        pass

    def test_it(self):
        user = UserModel.objects.create()
        obj = self.model.objects.create(user=user)
        obj.mark_publish()
        self.assertTrue(obj.published)

テストを実行するとテストはパスする。

python manage.py test
テストを実行する
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.003s

OK
Destroying test database for alias 'default'...
実行結果

以下、テストで作成したアプリケーションのファイル構成を貼っておく。

.
|-- blog
|   |-- __init__.py
|   |-- admin.py
|   |-- apps.py
|   |-- migrations
|   |   |-- 0001_initial.py
|   |   `-- __init__.py
|   |-- models.py
|   |-- tests
|   |   |-- __init__.py
|   |   |-- test_models.py
|   |   `-- testing.py
|   `-- views.py
|-- manage.py
`-- service_api
    |-- __init__.py
    |-- asgi.py
    |-- settings.py
    |-- urls.py
    `-- wsgi.py
ファイル構成