読者です 読者をやめる 読者になる 読者になる

Studio3104::BLOG.new

uninitialized constant Studio3104 (NameError)

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