メモ。
Django の urls.py で urlpattern に正規表現とか使っていると、正しく書けているかどうか確認したいことがある。
たとえば myproject
というプロジェクトに myapp
というアプリケーションを作成した場合、
myproject
および myapp
の urls.py は以下のようになる。
myproject/urls.py
from django.conf.urls import include, url from django.contrib import admin urlpatterns = [ url(r'^myapp/', include('myapp.urls')), url(r'^admin/', admin.site.urls), ]
myapp/urls.py
from django.conf.urls import patterns, url from django.views.generic import TemplateView from .views import HelloView urlpatterns = patterns('', url(r'^hello/(?P<user_id>\d{4}-\d{4})', HelloView.as_view()), url(r'^$', TemplateView.as_view(template_name='myapp/index.html')), )
特定の URL (/myapp/hello/0000-0000
とか) で期待通りビューが呼ばれているかどうかというユニットテストを書きたい。
基本的なテストの書き方
django.core.urlresolvers モジュールにある resolve() というメソッドを使う。
テストクラスは以下のようになる。(py.test で書いた)
# -*- coding: utf-8 -*- import pytest from django.core.urlresolvers import resolve, Resolver404 class TestUrl(object): app = '/myapp' @pytest.mark.parametrize(('url', 'expected'), [ ('/', { 'func_name': 'TemplateView', 'kwargs': {}}), ('/hello/0000-0000', { 'func_name': 'HelloView', 'kwargs': {'user_id': '0000-0000'}}), ]) def test_valid(self, url, expected): func, args, kwargs = resolve(self.app + url) assert func.__name__ == expected['func_name'] for k, v in expected['kwargs'].items(): assert kwargs[k] == v @pytest.mark.parametrize(('url',), [ ('/hello/',), ('/hello/000-000',), ]) def test_invalid(self, url): u"""404 になるケース""" with pytest.raises(Resolver404): func, args, kwargs = resolve(self.app + url)
resolve()
は引数としてパスを受け取り、
- パスに対応する関数(Class-based View の場合、
XXXView.as_view()
) - 位置指定引数 positional arguments
- キーワード引数 keyword arguments
を返す。
URL の正規表現の中に P<XXX>
とかを指定していて、正しくパラメータを渡せたかどうかは3つめの戻り値を検証すれば良い。
また、正しいビューが呼ばれたかどうか、ここではビュー名で検証しているが
これについてはもっと良い方法があるかもしれない。
ちょっとだけ改善
ユニットテストの書き方としてはだいたいOKだが、これだと myapp/urls.py のユニットテストが include している大元の myproject/urls.py の設定に依存している ことになる。
(たとえば、URL の /myapp
という部分を変更するとテストが失敗する)
そのため、myapp/urls.py のテストを書くときはプロジェクトの urls.py との依存関係を極力取り除きたい。
具体的にどうすれば良いかというと、テストごとに ROOT_URLCONF
がアプリケーションのルート URL (ここでは /myapp
以下) を指すように切り替えられると良い。
そのためには以下のようにする。まずは Django の TestCase を使用した場合。
# -*- coding: utf-8 -*- from django.core.urlresolvers import resolve, Resolver404 from django.test import TestCase, override_settings class TestUrl(TestCase): @override_settings(ROOT_URLCONF='myapp.urls') def test_valid(self): func, args, kwargs = resolve('/0000-0000') @override_settings(ROOT_URLCONF='myapp.urls') def test_invalid(self): with self.assertRaises(Resolver404): func, args, kwargs = resolve('/000-000/')
テストメソッドに @override_settings
アノテーションをつけて ROOT_URLCONF
を置き換えることができる。
実は、クラスに urls = 'myapp.urls'
という変数を持たせると同じように ROOT_CONF
を置き換えることができるんだけど
Django1.8 から deprecated みたいなので @override_settings
を使うようにする。
参考:https://docs.djangoproject.com/en/1.9/topics/testing/tools/#django.test.SimpleTestCase.urls
続いて、py.test を使用した場合。
ちょっと試してみた感じだと @override_settings
と @pytest.mark.parametrize
は併用できないっぽいので次のようにする。
# -*- coding: utf-8 -*- import pytest from django.conf import settings from django.core.urlresolvers import resolve, clear_url_caches, Resolver404 class TestUrl(object): urls = 'myapp.urls' @pytest.mark.parametrize(('url', 'expected'), [ ('/', { 'func_name': 'TemplateView', 'kwargs': {}}), ('/hello/0000-0000', { 'func_name': 'HelloView', 'kwargs': {'user_id': '0000-0000'}}), ]) def test_valid(self, url, expected): func, args, kwargs = resolve(url) assert func.__name__ == expected['func_name'] for k, v in expected['kwargs'].items(): assert kwargs[k] == v @pytest.mark.parametrize(('url',), [ ('/hello/',), ('/hello/000-000',), ]) def test_invalid(self, url): u"""404 になるケース""" with pytest.raises(Resolver404): func, args, kwargs = resolve(url) # ref. django.test.testcase @classmethod def setup_class(self): if hasattr(self, 'urls'): self._old_root_urlconf = settings.ROOT_URLCONF settings.ROOT_URLCONF = self.urls clear_url_caches() @classmethod def teardown_class(self): if hasattr(self, '_old_root_urlconf'): settings.ROOT_URLCONF = self._old_root_urlconf clear_url_caches()
テストクラスに urls
という変数があった場合は ROOT_URLCONF
に置き換える、という処理を行っている。
setup_class
、teardown_class
はクラス単位でテストの実行前後に処理を挟むための py.test のメソッドである。
参考:https://pytest.org/latest/xunit_setup.html#class-level-setup-teardown
この処理は(先ほど deprecated であると言った)Django の urls
変数による ROOT_URLCONF
の置き換え処理を参考にした。
このあたり。
https://github.com/django/django/blob/1.9.1/django/test/testcases.py#L233-L243