【Django】ユニットテストでテスト用URLパターンを追加する方法

Djangoのユニットテストで、テスト用のURLパターンを追加する方法を紹介します。この仕組みを使うと、mixinのテストなどを用意に実行することができます。

この記事では、mixinのテストを通して、Djangoのユニットテスト用にURLパターンを追加する方法を紹介していきます。

テスト用プロジェクト・アプリの構成

説明用のmixinは、myappプロジェクトの、sample1アプリ下に作成します。以下のような構成になります。

- myapp/  :プロジェクト
  - sample1/  :アプリ
    - mixinx.py  :テスト対象のmixin
    - tests.py  :テストケース
  - myapp/
    - test_urls.py  :テスト用URLパターン設定

テスト対象のmixin

今回テストに使うのはLoginRequiredMixinという、単純にログイン済みかをチェックするmixinです。myapp/sample1/mixins.pyの中に定義します。

# coding: utf-8
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator


class LoginRequiredMixin(object):
    """
    ログイン済みかをチェックするmixinです。
    """
    @method_decorator(login_required)
    def dispatch(self, request, *args, **kwds):
        return super().dispatch(request, *args, **kwds)

テスト用URLパターンを追加してテストを実行

テスト用URLパターンを追加

まず、テスト用のURLパターンを追加します。場所はどこでもいいのですが、今回はmyapp/myapp/下に作成します。

# coding: utf-8
from django.conf.urls import patterns, url
from myapp.urls import urlpatterns
from sample1 import tests as sample1_tests

urlpatterns += patterns(
    '',
    url(r'^test/sample1/login_required_mixin1/$',
        sample1_tests.TestLoginRequiredView.as_view(),
        name='test_sample1_login_required_mixin1'),
)

上記test_urls.pyでは、これから作成する「sample1_tests.TestLoginRequiredView.as_view()」用のパターンを定義しています。

ここでの注意点は、「from myapp.urls import urlpatterns」で本番用のURLパターンをインポートし、「urlpatterns += patterns(」で追加している点です。これをしないと本番用のURLパターンが上書きされ、テストが正しく動かないケースがでてきます。

テスト用Class-based viewの作成

test_urls.pyで指定した、TestLoginRequiredViewmyapp/sample1/tests.pyの中に作成します。

LoginRequiredMixinのテストのためのクラスなので、LoginRequiredMixinを継承して作成します。なお、目的はLoginRequiredMixinのテストなので、このget・postメソッドはそのままHttpResponseを返すだけです。

import 省略...
class TestLoginRequiredView(LoginRequiredMixin, View):
    def get(self, request, *args, **kwds):
        return HttpResponse()

    def post(self, request, *args, **kwds):
        return HttpResponse()

テストケース作成

それでは、作成したtest_urls.pyを使ってテストケースを作成します。テストケースは、myapp/sample1/tests.pyの中に作成していきます。テストクラス名はTestLoginRequiredMixin1とします。

まずは、urls属性を指定し、テスト用のURLパターンを使用するように設定しています。そして、テスト用クライアントを使って、擬似的なリクエストを投げ、結果を確認しています。

import 省略...
class TestLoginRequiredMixin1(TestCase):
    # テスト用のURLパターンを使用します。
    urls = 'myapp.test_urls'

    def setUp(self):
        super().setUp()
        self.client = Client()
        # テスト用URL(test/sample1/login_required_mixin1/)を取得します。
        self.path = reverse('test_sample1_login_required_mixin1')

    def test_logged_in(self):
        """ログイン済みのテストです。"""
        # ユーザを作成し、ログインします。
        username = 'foo'
        password = 'secret'
        User.objects.create_user(username=username, password=password)
        self.client.login(username=username, password=password)

        # テスト対象を実行します。
        res = self.client.get(self.path)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 200)

    def test_not_logged_in(self):
        """ログインしていない場合のテストです。"""
        # テスト対象を実行します。
        res = self.client.get(self.path)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 302)

これで、テスト用のURLパターンを追加して、mixinのテストを作成できました!

クラスを定義せずに簡略化

上記やり方でもテストは可能ですが、個人的にはmixinを組み込んだテスト用のクラスを定義するのが面倒です。つまり、わざわざTestLoginRequiredViewを作成するのが面倒です。

それを解消するために、テストクラスTestLoginRequiredMixin2の中にクラスメソッドas_viewを定義して、mixinを組み込んだクラスを返すようにします。

テストクラスの作成

myapp/sample1/tests.pyの中に、テストクラスを作成します。

class TestLoginRequiredMixin2(TestCase):
    # テスト用のURLパターンを使用します。
    urls = 'myapp.test_urls'

    def setUp(self):
        super().setUp()
        self.client = Client()
        # テスト用URL(test/sample1/login_required_mixin2/)を取得します。
        self.path = reverse('test_sample1_login_required_mixin2')

    @classmethod
    def as_view(cls, **initkwds):
        cls = type('TestTempClass', (LoginRequiredMixin, View), {})
        cls.get = lambda self, request, *args, **kwargs: HttpResponse()
        cls.post = lambda self, request, *args, **kwargs: HttpResponse()
        return cls.as_view()

    def test_logged_in(self):
        """ログイン済みのテストです。"""
        # 省略...

    def test_not_logged_in(self):
        """ログインしていない場合のテストです。"""
        # 省略...

テスト用URLパターンを追加

test_urls.pyでは、TestLoginRequiredMixin2のas_viewを指定します。

urlpatterns += patterns(
    url(r'^test/sample1/login_required_mixin2/$',
        sample1_tests.TestLoginRequiredMixin2.as_view(),
        name='test_sample1_login_required_mixin2'),
)

これで少し簡略化できました!

TestViewBaseを使ってさらに簡略化

mixinを継承したクラスの定義を削れ、少し簡略化できました。ただ、毎回as_viewを定義するのも面倒なので、これも削除します。

TestViewBaseの作成

TestViewBaseを作成して、クラスメソッドas_viewをこのクラスの中で定義します。そして、mixin用のテストクラスでは、このTestViewBaseを継承して作成するようにします。

また、テスト対象となるmixinを変更できるようにするため、TestViewBaseに_mixin属性を定義します。そして、as_viewでmixinを継承したクラスを作成するときに、この_mixinを使うようにします。

このTestViewBaseはmyapp/sample1/tests.pyに作成します。ただ、これは説明用に、わかりやすい場所に置いているだけです。実際には、汎用的に使いやすい場所に定義したほうがいいと思います。

class TestViewBase(TestCase):
    urls = 'myapp.test_urls'
    _mixin = None

    def setUp(self):
        super().setUp()
        self.client = Client()

    @classmethod
    def as_view(cls, **initkwds):
        cls = type('TestTempClass', (cls._mixin, View), {})
        cls.get = lambda self, request, *args, **kwargs: HttpResponse()
        cls.post = lambda self, request, *args, **kwargs: HttpResponse()
        return cls.as_view()

テストクラスの作成

myapp/sample1/tests.pyの中に、テストクラスを作成します。このテストクラスは、TestViewBaseを継承して作成します。

class TestLoginRequiredMixin3(TestViewBase):
    # テスト対象のmixinを指定します。
    _mixin = LoginRequiredMixin

    def setUp(self):
        super().setUp()
        # テスト用URL(test/sample1/login_required_mixin3/)を取得します。
        self.path = reverse('test_sample1_login_required_mixin3')

    def test_logged_in(self):
        """ログイン済みのテストです。"""
        # 省略...

    def test_not_logged_in(self):
        """ログインしていない場合のテストです。"""
        # 省略...

テスト用URLパターンを追加

test_urls.pyでは、TestLoginRequiredMixin3のas_viewを指定します。

urlpatterns += patterns(
    '',
    url(r'^test/sample1/login_required_mixin3/$',
        sample1_tests.TestLoginRequiredMixin3.as_view(),
        name='test_sample1_login_required_mixin3'),
)

※2015/6/19 TestLoginRequiredMixin2TestLoginRequiredMixin3に直しました。

これで、大幅に簡略化できました!以降、mixin用テストクラスの中で、as_viewを定義する必要はなくなります!

テストクラスのソース全体

記事の中で分割して説明してきた、myapp/sample1/tests.pyの全体像を載せておきます。

# coding: utf-8
from django.test import TestCase
from django.test.client import Client
from django.views.generic import View
from django.http import HttpResponse
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from sample1.mixins import LoginRequiredMixin


class TestLoginRequiredView(LoginRequiredMixin, View):
    def get(self, request, *args, **kwds):
        return HttpResponse()

    def post(self, request, *args, **kwds):
        return HttpResponse()


class TestLoginRequiredMixin1(TestCase):
    # テスト用のURLパターンを使用します。
    urls = 'myapp.test_urls'

    def setUp(self):
        super().setUp()
        self.client = Client()
        # テスト用URL(test/sample1/login_required_mixin1/)を取得します。
        self.path = reverse('test_sample1_login_required_mixin1')

    def test_logged_in(self):
        """ログイン済みのテストです。"""
        # ユーザを作成し、ログインします。
        username = 'foo'
        password = 'secret'
        User.objects.create_user(username=username, password=password)
        self.client.login(username=username, password=password)

        # テスト対象を実行します。
        res = self.client.get(self.path)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 200)

    def test_not_logged_in(self):
        """ログインしていない場合のテストです。"""
        # テスト対象を実行します。
        res = self.client.get(self.path)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 302)


class TestLoginRequiredMixin2(TestCase):
    # テスト用のURLパターンを使用します。
    urls = 'myapp.test_urls'

    def setUp(self):
        super().setUp()
        self.client = Client()
        # テスト用URL(test/sample1/login_required_mixin2/)を取得します。
        self.path = reverse('test_sample1_login_required_mixin2')

    @classmethod
    def as_view(cls, **initkwds):
        cls = type('TestTempClass', (LoginRequiredMixin, View), {})
        cls.get = lambda self, request, *args, **kwargs: HttpResponse()
        cls.post = lambda self, request, *args, **kwargs: HttpResponse()
        return cls.as_view()

    def test_logged_in(self):
        """ログイン済みのテストです。"""
        # ユーザを作成し、ログインします。
        username = 'foo'
        password = 'secret'
        User.objects.create_user(username=username, password=password)
        self.client.login(username=username, password=password)

        # テスト対象を実行します。
        res = self.client.get(self.path)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 200)

    def test_not_logged_in(self):
        """ログインしていない場合のテストです。"""
        # テスト対象を実行します。
        res = self.client.get(self.path)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 302)


class TestViewBase(TestCase):
    urls = 'myapp.test_urls'
    _mixin = None

    def setUp(self):
        super().setUp()
        self.client = Client()

    @classmethod
    def as_view(cls, **initkwds):
        cls = type('TestTempClass', (cls._mixin, View), {})
        cls.get = lambda self, request, *args, **kwargs: HttpResponse()
        cls.post = lambda self, request, *args, **kwargs: HttpResponse()
        return cls.as_view()


class TestLoginRequiredMixin3(TestViewBase):
    # テスト対象のmixinを設定します。
    _mixin = LoginRequiredMixin

    def setUp(self):
        super().setUp()
        # テスト用URL(test/sample1/login_required_mixin3/)を取得します。
        self.path = reverse('test_sample1_login_required_mixin3')

    def test_logged_in(self):
        """ログイン済みのテストです。"""
        # ユーザを作成し、ログインします。
        username = 'foo'
        password = 'secret'
        User.objects.create_user(username=username, password=password)
        self.client.login(username=username, password=password)

        # テスト対象を実行します。
        res = self.client.get(self.path)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 200)

    def test_not_logged_in(self):
        """ログインしていない場合のテストです。"""
        # テスト対象を実行します。
        res = self.client.get(self.path)

        # テスト結果を確認します。
        self.assertEqual(res.status_code, 302)

テストを実行すると6個全てOKになります。

$ ./manage.py test
Creating test database for alias 'default'...
......
----------------------------------------------------------------------
Ran 6 tests in 0.325s

OK

また、githubにも公開してあります。 https://github.com/yusukemurayama/blog-samples/tree/master/201506/django_testcase_urls/myapp

この記事が役に立った場合、シェアしていただけると励みになります!!