Django の Abstract Model を py.test でテストする
※これは Python Advent Calendar 2015 2日目のエントリです。
各モデルが共通のカラムや振る舞いを持つようになってきたら
Django
でアプリケーションを開発していると、各モデルに共通のカラムを持たせたり、共通の振る舞いをさせたりしたいということが起こると思います。
そのような場合は、各モデルに対して同じような実装を施すのではなく、Abstract Model を定義して各モデルがそれを継承するようにするのが自然でしょう。
例えば以下の様な感じです。
from django.db import models class CommonAbstractModel(models.Model): created_datetime = models.DateTimeField(auto_now_add=True) updated_datetime = models.DateTimeField(auto_now=True) class Meta: abstract = True
from common.models import CommonAbstractModel class User(CommonAbstractModel): name = models.models.CharField(max_length=100)
このように実装することによって、User
モデルに直接 created_datetime
, updated_datetime
を定義する必要がなくなるので便利です。
ではテストをどうするか
CommonAbstractModel
を継承したモデルが確実に created_datetime
, updated_datetime
を持っているかということをテストした気持ちになるのが自然ですよね。
ただ abstract = True
なモデルは manage.py makemigrations
でマイグレーションが作成されないため、通常のモデルのように普通にテストを書くことがかないません。
とはいえ、子クラスである User
に対してそれらテストを行うのは、あくまで User
のテストであって CommonAbstractModel
をテストするということにはならないです(なるかも知れないけど、例えば CommonAbstractModel
に論理削除の実装が入ったりしたらそれも子クラスでテストするんですか?という話になりますね) 。
py.test
を使った例
実はこの例(継承したモデルが確実に created_datetime
, updated_datetime
を持っているかというテスト)だと、擬似的なマイグレーションを行う必要はまったくなく、擬似的な子クラスだけがあれば良かったりします。。。
Abstract Model に論理削除とかを実装したくなったときには必要ですが。
import pytest import datetime from common.models import CommonAbstractModel from django.db import connection from django.core.management.color import no_style @pytest.fixture(scope='module') def fx_TestInheritedModel(): class TestInheritedModel(CommonAbstractModel): class Meta: app_label = 'common' return TestInheritedModel @pytest.yield_fixture(scope='function') @pytest.mark.django_db def fx_test_model_cls(request, fx_TestInheritedModel): cursor = connection.cursor() statements, pending = connection.creation.sql_create_model(fx_TestInheritedModel, no_style()) for sql in statements: cursor.execute(sql) yield fx_TestInheritedModel statements = connection.creation.sql_destroy_model(fx_TestInheritedModel, (), no_style()) for sql in statements: cursor.execute(sql) @pytest.mark.django_db class TestCommonAbstractModel: @pytest.mark.parametrize(('attr_name'), [ 'created_datetime', 'updated_datetime', ]) def test_inherited_model_has_proper_fields(self, fx_TestInheritedModel, attr_name): inherited_model = fx_TestInheritedModel() assert hasattr(inherited_model, attr_name), 'the inherited model must have %s field' % (attr_name, )
ちょっとだけ解説
テスト内部で擬似クラスを作成してそれに CommonAbstractModel
を継承させて、それを擬似マイグレーションをさせてテストしてしまおうという戦略です。
擬似クラス
フィクスチャをこのように定義します。
app_label = 'common'
がめっちゃ重要で、これを定義しておかないと擬似マイグレーションさせることが出来ません。
@pytest.fixture(scope='module') def fx_TestInheritedModel(): class TestInheritedModel(CommonAbstractModel): class Meta: app_label = 'common' return TestInheritedModel
擬似マイグレーション
各テストメソッドが実行される前にテーブルが作成され、終わったら消されます。
yield_fixture
を使うのがポイントです(scope='function'
はデフォルトなのであえて指定する必要はないかもしれませんが、Explicit is better than implicit.
です。)
@pytest.yield_fixture(scope='function') @pytest.mark.django_db def fx_test_model_cls(request, fx_TestInheritedModel): cursor = connection.cursor() statements, pending = connection.creation.sql_create_model(fx_TestInheritedModel, no_style()) for sql in statements: cursor.execute(sql) yield fx_TestInheritedModel statements = connection.creation.sql_destroy_model(fx_TestInheritedModel, (), no_style()) for sql in statements: cursor.execute(sql)
あとは...
実際に fx_test_model_cls
を利用するテストをガシガシ書いていくだけです!
django.test.TestCase
を使いたい場合
普通に setUp
, tearDown
を使って同じようなことすれば良さそう。
EOF