Как переопределить настройки для кода, выполняемого в 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.
Вернуться на верх