Для тестирования 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 |
Класс достаточно простой, но в его тестировании есть ряд ньюансов.
Начнем с того, что на вход подается объект 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') |
Возникает вопрос, что делать с масками для 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 |
Теперь нам осталось добавить атрибут 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) |
Остался последний случай: залогиненные пользователи.
Здесь мы воспользуемся библиотекой 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) |
Tags: django, middleware, mock, unittest