Как переопределить настройки для кода, выполняемого в urls.py, при юнит-тестировании django
В моем приложении django есть переменная env var DEMO
, которая, помимо прочего, диктует, какие конечные точки будут объявлены в моем urls.py
файле.
Я хочу провести модульное тестирование этих конечных точек, я пробовал django.test.override_settings
, но обнаружил, что urls.py
выполняется только один раз, а не один раз на модульный тест.
Мой код выглядит следующим образом:
# settings.py
DEMO = os.environ.get("DEMO", "false") == "true"
# urls.py
print(f"urls.py: DEMO = {settings.DEMO}")
if settings.DEMO:
urlpatterns += [
path('my_demo_endpoint/',MyDemoAPIView.as_view(),name="my-demo-view")
]
# test.test_my_demo_endpoint.py
class MyDemoEndpointTestCase(TestCase):
@override_settings(DEMO=True)
def test_endpoint_is_reachable_with_demo_equals_true(self):
print(f"test_endpoint_is_reachable_with_demo_equals_true: DEMO = {settings.DEMO}")
response = self.client.get("/my_demo_endpoint/")
# this fails with 404
self.assertEqual(response.status_code, 200)
@override_settings(DEMO=False)
def test_endpoint_is_not_reachable_with_demo_equals_false(self):
print(f"test_endpoint_is_not_reachable_with_demo_equals_false: DEMO = {settings.DEMO}")
response = self.client.get("/my_demo_endpoint/")
self.assertEqual(response.status_code, 404)
При запуске этой программы я получаю:
urls.py: DEMO = False
test_endpoint_is_reachable_with_demo_equals_true: DEMO = True
<test fails with 404>
test_endpoint_is_not_reachable_with_demo_equals_false: DEMO = False
<test succeed>
urls.py запускается только один раз перед каждым тестом, однако я хочу проверить различное поведение urls.py в зависимости от настроек
Использование другого файла настроек для тестирования не является решением, поскольку для разных тестов требуются разные настройки. Прямой вызов моего представления в юнит-тесте означает, что код urls.py остается незакрытым и его поведение не тестируется, так что это тоже не то, чего я хочу.
Как переопределить настройки для кода, выполняемого в urls.py?
Спасибо, что уделили время.
Как вы уже писали выше, модуль urls.py
загружается один раз при запуске приложения и не перезагружается между тестами, если вы не перезагрузите его явно. Чтобы протестировать различные модели поведения urls.py
в зависимости от настроек, вы можете динамически импортировать и перезагружать модуль urls.py
в каждом тесте.
Используйте importlib.reload
Python для повторного импорта и оценки urls.py
после изменения настроек.
import importlib
from django.conf import settings
from django.test import override_settings, TestCase
from django.urls import resolve, reverse
class MyDemoEndpointTestCase(TestCase):
def reload_urls(self):
import myproject.urls # Adjust to your `urls.py` location
importlib.reload(myproject.urls)
@override_settings(DEMO=True)
def test_endpoint_is_reachable_with_demo_equals_true(self):
self.reload_urls()
self.assertTrue(settings.DEMO) # Verify setting was applied
response = self.client.get("/my_demo_endpoint/")
self.assertEqual(response.status_code, 200)
@override_settings(DEMO=False)
def test_endpoint_is_not_reachable_with_demo_equals_false(self):
self.reload_urls()
self.assertFalse(settings.DEMO) # Verify setting was applied
response = self.client.get("/my_demo_endpoint/")
self.assertEqual(response.status_code, 404)
Я хотел обратить ваше внимание на это интересное предупреждение из документации django:
Файл настроек содержит некоторые параметры, к которым обращаются только во время инициализации внутренних компонентов Django. Если вы измените их с помощью override_settings, настройка будет изменена, если вы обратитесь к ней через модуль django.conf.settings, однако внутренние компоненты Django обращаются к нему по-другому. По сути, использование функций override_settings() или modify_settings() с этими настройками, вероятно, не сделает того, что что вы от них ожидаете.
Это предупреждение находится примерно на половине этой страницы в документации по django. Файл Urls - одно из тех мест, к которому обращаются только один раз во время инициализации.
Если вы всегда хотите settings.DEMO = True
для всех ваших тестов, возможно, стоит создать отдельный файл настроек только для тестов. Я вижу, что вам нужно тестировать различные значения - пожалуйста, попробуйте следующее решение
Я использую Pytest, поэтому мой тест будет очень простым. Надеюсь, это поможет. Первым шагом вместо использования декоратора @override_settings
я бы сделал тестовый фикстур. Я бы добавил значение этой переменной в тестовый фикстур и передал его в мой тест. Может быть, вы можете сделать что-то подобное в своей тестовой установке?
from django.conf import settings
@pytest.fixture()
def test_settings_demo_true(settings):
settings.DEMO = True
@pytest.mark.django_db
def test_endpoint_is_reachable_with_demo_equals_true(client, test_settings_demo_true):
print(f"test_endpoint_is_reachable_with_demo_equals_true: DEMO = {settings.DEMO}")
response = client.get("/my_demo_endpoint/")
# this should now pass
assert response.status_code == 200
@pytest.fixture()
def test_settings_demo_false(settings):
settings.DEMO = False
@pytest.mark.django_db
def test_endpoint_is_reachable_with_demo_equals_false(client, test_settings_demo_false):
print(f"test_endpoint_is_not_reachable_with_demo_equals_true: DEMO = {settings.DEMO}")
response = client.get("/my_demo_endpoint/")
# this should now pass
assert response.status_code == 404
Альтернативно вы можете попробовать следующее:
from django.conf import settings
class MyDemoEndpointTestCase(TestCase):
def test_endpoint_is_reachable_with_demo_equals_true(self):
settings.DEMO = True
print(f"test_endpoint_is_reachable_with_demo_equals_true: DEMO = {settings.DEMO}")
response = self.client.get("/my_demo_endpoint/")
# this fails with 404
self.assertEqual(response.status_code, 200)
def test_endpoint_is_not_reachable_with_demo_equals_false(self):
settings.DEMO = False
print(f"test_endpoint_is_not_reachable_with_demo_equals_false: DEMO = {settings.DEMO}")
response = self.client.get("/my_demo_endpoint/")
self.assertEqual(response.status_code, 404)
Ответ Виталия Десятки почти работает, за исключением того, что Django кэширует URL resolver. Это можно увидеть в коде Django, а именно в функции django.urls.resolvers._get_cached_resolver
, которая это делает. Вы можете модифицировать ответ Виталия Десятки и добавить в него очистку кэша следующим образом:
import importlib
from django.urls import clear_url_caches
class MyDemoEndpointTestCase(TestCase):
def reload_urls(self):
import myproject.urls # Adjust to your `urls.py` location
importlib.reload(myproject.urls)
clear_url_caches()
Вышеописанное позволит пройти ваши тесты с оговорками, что:
- Вы будете перезагружать URL Conf и, следовательно, повторять все побочные эффекты (которых в идеале у файла быть не должно), присутствующие в модуле.
Альтернативой может быть создание специального URLconf для случаев, когда DEMO = True
, и вместо переопределения DEMO
во время тестов просто переопределять ROOT_URLCONF
(или переопределять оба). Таким образом, ваши настройки могут выглядеть примерно так:
DEMO = os.environ.get("DEMO", "false") == "true"
ROOT_URLCONF = 'myproject.urls' # Your default URLConf when DEMO is False
if DEMO:
ROOT_URLCONF = 'myproject.demo_urls'
А затем рядом с файлом urls.py
вы можете иметь файл demo_urls.py
, например, так:
from .urls import urlpatterns as base_urlpatterns
urlpatterns = base_urlpatterns + [
# Your demo urls go here
]
Теперь, когда вы хотите протестировать свои демонстрационные URL, вы можете переопределить URLConf следующим образом:
@override_settings(ROOT_URLCONF='myproject.demo_urls')
def test_foo(self):
pass
благодаря ответу Виталия Десяткина я смог написать частичное решение проблемы, разделив тесты на две части:
- тестирование содержания urlpatterns
- тестирование поведения вида без разрешения url
class MyTestCase(TestCase):
def setUp(self):
super().setUp()
self.factory = APIRequestFactory()
self.view = MyAPIView.as_view()
@staticmethod
def get_urls_urlpatterns():
importlib.reload(amphichoix.urls)
return amphichoix.urls.urlpatterns
@override_settings(DEMO=False)
def test_view_is_not_reachable_with_demo_false(self):
urlpatterns = self.get_urls_urlpatterns()
urlpattern = [
urlpattern
for urlpattern in urlpatterns
if urlpattern.name == "my-demo-view"
]
self.assertEqual(len(urlpattern), 0)
@override_settings(DEMO=True)
def test_view_is_reachable_with_demo_true(self):
urlpatterns = self.get_urls_urlpatterns()
urlpattern = [
urlpattern
for urlpattern in urlpatterns
if urlpattern.name == "my-demo-view"
]
self.assertEqual(len(urlpattern), 1)
def test_my_view_behaviour(self):
# testing using request factories
request = self.factory.post("my_demo_url/")
response = self.view(request)
self.assertEqual(response, 200)
Я использую DRF APIRequestFactory https://www.django-rest-framework.org/api-guide/testing/#creating-test-requests, хотя вы можете использовать Django RequestFactory https://docs.djangoproject.com/en/5.1/topics/testing/advanced/#the-request-factory. для тестирования поведения без разрешения урлов.
Основными преимуществами этого метода были:
- Я смог охватить все ветви из urls.py .
- Мне не пришлось изменять структуру файлов urls/settings.