Для тестирования view в django есть удобный класс django.test.client.Client и хорошая официальная документация, но что делать, если нам надо протестировать лишь middleware, а не всю фазу от запроса до ответа?
Допустим у нас есть middleware, позволяющее задавать маску по урлам для обертывания в login_required и маску с исключениями в конфигурационном файле:
import re
from django.conf import settings
from django.contrib.auth.decorators import login_required
class RequireLoginMiddleware(object):
"""
Middleware component that wraps the login_required decorator around
matching URL patterns. To use, add the class to MIDDLEWARE_CLASSES and
define LOGIN_REQUIRED_URLS and LOGIN_REQUIRED_URLS_EXCEPTIONS in your
settings.py. For example:
------
LOGIN_REQUIRED_URLS = (
r'/topsecret/(.*)$',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
r'/topsecret/login(.*)$',
r'/topsecret/logout(.*)$',
)
------
LOGIN_REQUIRED_URLS is where you define URL patterns; each pattern must
be a valid regex.
LOGIN_REQUIRED_URLS_EXCEPTIONS is, conversely, where you explicitly
define any exceptions (like login and logout URLs).
"""
def __init__(self):
self.required = tuple([re.compile(url) for url in settings.LOGIN_REQUIRED_URLS])
self.exceptions = tuple([re.compile(url) for url in settings.LOGIN_REQUIRED_URLS_EXCEPTIONS])
def process_view(self, request, view_func, view_args, view_kwargs):
# No need to process URLs if user already logged in
if request.user.is_authenticated(): return None
# An exception match should immediately return None
for url in self.exceptions:
if url.match(request.path): return None
# Requests matching a restricted URL pattern are returned
# wrapped with the login_required decorator
for url in self.required:
if url.match(request.path): return login_required(view_func)(request, *view_args, **view_kwargs)
# Explicitly return None for all non-matching requests
return None |
import re
from django.conf import settings
from django.contrib.auth.decorators import login_required
class RequireLoginMiddleware(object):
"""
Middleware component that wraps the login_required decorator around
matching URL patterns. To use, add the class to MIDDLEWARE_CLASSES and
define LOGIN_REQUIRED_URLS and LOGIN_REQUIRED_URLS_EXCEPTIONS in your
settings.py. For example:
------
LOGIN_REQUIRED_URLS = (
r'/topsecret/(.*)$',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
r'/topsecret/login(.*)$',
r'/topsecret/logout(.*)$',
)
------
LOGIN_REQUIRED_URLS is where you define URL patterns; each pattern must
be a valid regex.
LOGIN_REQUIRED_URLS_EXCEPTIONS is, conversely, where you explicitly
define any exceptions (like login and logout URLs).
"""
def __init__(self):
self.required = tuple([re.compile(url) for url in settings.LOGIN_REQUIRED_URLS])
self.exceptions = tuple([re.compile(url) for url in settings.LOGIN_REQUIRED_URLS_EXCEPTIONS])
def process_view(self, request, view_func, view_args, view_kwargs):
# No need to process URLs if user already logged in
if request.user.is_authenticated(): return None
# An exception match should immediately return None
for url in self.exceptions:
if url.match(request.path): return None
# Requests matching a restricted URL pattern are returned
# wrapped with the login_required decorator
for url in self.required:
if url.match(request.path): return login_required(view_func)(request, *view_args, **view_kwargs)
# Explicitly return None for all non-matching requests
return None
Класс достаточно простой, но в его тестировании есть ряд ньюансов.
Начнем с того, что на вход подается объект request, который неплохо бы как-то получить в подходящем для тестирования виде. Для этого в django есть специальный класс RequestFactory:
from django.test import TestCase
from django.test.client import RequestFactory
class RequireLoginMiddlewareTest(TestCase):
def test_url_is_not_in_login_required(self):
rf = RequestFactory()
request = rf.get('/abc/test') |
from django.test import TestCase
from django.test.client import RequestFactory
class RequireLoginMiddlewareTest(TestCase):
def test_url_is_not_in_login_required(self):
rf = RequestFactory()
request = rf.get('/abc/test')
Возникает вопрос, что делать с масками для URL? Тест не должен полагаться на данные из конфига, которые могут быть в любой момент изменены. В версии 1.4 будет добавлена возможность временно менять значения из настроек в тестах: Overriding settings, но это trunk, а на продакшене стоит последний stable релиз, т.е. 1.3
Решение не столь элегантное, но зато доволное простое: тесты запускаются в single-thread, так что мы можем поменять настройки в setUp и использовать их далее в тестах:
def setUp(self):
self.old_settings_required_urls = settings.LOGIN_REQUIRED_URLS
self.old_settings_exceptions_urls = settings.LOGIN_REQUIRED_URLS_EXCEPTIONS
settings.LOGIN_REQUIRED_URLS = (
r'/test/(.*)$',
)
settings.LOGIN_REQUIRED_URLS_EXCEPTIONS = (
r'/test/exc1(.*)$',
r'/test/exc2(.*)$',
)
self.middleware = RequireLoginMiddleware()
def tearDown(self):
settings.LOGIN_REQUIRED_URLS = self.old_settings_required_urls
settings.LOGIN_REQUIRED_URLS_EXCEPTIONS = self.old_settings_exceptions_urls |
def setUp(self):
self.old_settings_required_urls = settings.LOGIN_REQUIRED_URLS
self.old_settings_exceptions_urls = settings.LOGIN_REQUIRED_URLS_EXCEPTIONS
settings.LOGIN_REQUIRED_URLS = (
r'/test/(.*)$',
)
settings.LOGIN_REQUIRED_URLS_EXCEPTIONS = (
r'/test/exc1(.*)$',
r'/test/exc2(.*)$',
)
self.middleware = RequireLoginMiddleware()
def tearDown(self):
settings.LOGIN_REQUIRED_URLS = self.old_settings_required_urls
settings.LOGIN_REQUIRED_URLS_EXCEPTIONS = self.old_settings_exceptions_urls
Теперь нам осталось добавить атрибут session к request’у, иначе мы получим
AttributeError: ‘WSGIRequest’ object has no attribute ‘session’
так как сессия к request’у добавляется в отдельном SessionMiddleware.
Вместо view_func передадим аргументом пустую анонимную функцию, нам важен только возвращаемый результат: None (если пользователь залогинен или URL не требует аутентификации) или HttpResponseRedirect на форму входа.
def test_url_is_not_in_login_required(self):
rf = RequestFactory()
request = rf.get('/abc/test')
request.session = {}
response = self.middleware.process_view(request, lambda x: x, [], {})
self.assertIsNone(response)
def test_access_excluded_from_login_required_urls(self):
rf = RequestFactory()
request = rf.get('/test/exc1/foo')
request.session = {}
response = self.middleware.process_view(request, lambda x: x, [], {})
self.assertIsNone(response)
def test_redirect_guests(self):
rf = RequestFactory()
request = rf.get('/test/something/')
request.session = {}
response = self.middleware.process_view(request, lambda x: x, [], {})
self.assertIsInstance(response, HttpResponseRedirect)
self.assertEqual(reverse('auth_login') + '?next=/test/something/', response['Location'])
self.assertEqual(response.status_code, 302) |
def test_url_is_not_in_login_required(self):
rf = RequestFactory()
request = rf.get('/abc/test')
request.session = {}
response = self.middleware.process_view(request, lambda x: x, [], {})
self.assertIsNone(response)
def test_access_excluded_from_login_required_urls(self):
rf = RequestFactory()
request = rf.get('/test/exc1/foo')
request.session = {}
response = self.middleware.process_view(request, lambda x: x, [], {})
self.assertIsNone(response)
def test_redirect_guests(self):
rf = RequestFactory()
request = rf.get('/test/something/')
request.session = {}
response = self.middleware.process_view(request, lambda x: x, [], {})
self.assertIsInstance(response, HttpResponseRedirect)
self.assertEqual(reverse('auth_login') + '?next=/test/something/', response['Location'])
self.assertEqual(response.status_code, 302)
Остался последний случай: залогиненные пользователи.
Здесь мы воспользуемся библиотекой Mock (опять из-за сессий на которые опирается django.contrib.auth.login) и “подменить” метод is_authenticated() у user’a чтобы он возвращал True:
def test_logged_user_access(self):
rf = RequestFactory()
request = rf.get('/test/something/')
request.session = {}
request.user.is_authenticated = Mock(return_value=True)
response = self.middleware.process_view(request, lambda x: x, [], {})
self.assertIsNone(response) |
def test_logged_user_access(self):
rf = RequestFactory()
request = rf.get('/test/something/')
request.session = {}
request.user.is_authenticated = Mock(return_value=True)
response = self.middleware.process_view(request, lambda x: x, [], {})
self.assertIsNone(response)