1

my django app has a env var DEMO which, among other thing, dictate what endpoints are declared in my urls.py file.

I want to unit tests these endpoints, I've tried django.test.override_settings but I've found that urls.py is ran only once and not once per unit test.

My code look like this:

# 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)

when running this I get:

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 is ran only once before every test, however I want to test different behavious of urls.py depending on settings

Using a different settings file for testing is not a solution because different tests requires different settings. Directly calling my view in the unit test means that the urls.py code stays uncovered and its behaviour untested so this is also not what I want.

How can I override settings for code ran in urls.py?

Thank you for your time.

2
  • It would probably just be better for the view itself to check whether it should be accessible, based on DEMO state. Commented Jan 9 at 14:16
  • I don't want these views to even exists if DEMO=False, for example drf_spectacular would generate doc on this endpoint even if it's not usable Commented Jan 9 at 14:39

4 Answers 4

2

As you already wrote above, urls.py is loaded once during the app's startup and does not reload between tests unless you explicitly reload it. To test different urls.py behaviors based on settings, you can dynamically import and reload the urls.py module within each test.

Use Python's importlib.reload to re-import and evaluate urls.py after changing the settings.

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)
Sign up to request clarification or add additional context in comments.

3 Comments

Thank you for your time but it does not work docs.python.org/3/library/importlib.html#importlib.reload "Other references to the old objects (such as names external to the module) are not rebound to refer to the new objects and must be updated in each namespace where they occur if that is desired." Importing anew this package does not mean that the previously configured app will go through configuration again the newest urlpatterns
However, I can separate my test in two, one part which checks urlpatterns has the correct value depending on the vars thank to you method, the second part testing view but directly calling them. Thank you
You would probably also need to clear the caches in django.urls.resolvers for reload to work thoroughly.
1

Vitaliy Desyatka's answer nearly works other than the fact that Django caches the URL resolver. This can be seen in Django's code, specifically the function django.urls.resolvers._get_cached_resolver is doing this. You can modify Vitaliy Desyatka's answer to add clearing of the cache to it as well like so:

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()

The above will get your tests to pass with the caveats that:

  • You'll be reloading the URL Conf and hence repeating any side effects (which ideally the file shouldn't have any) that are present in the module.

An alternative would be for you to have a dedicated URLconf for when DEMO = True and instead of overriding DEMO during the tests just override ROOT_URLCONF (or override both). So your settings can look something like:

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'

And then alongside your urls.py file you can have a demo_urls.py file like so:

from .urls import urlpatterns as base_urlpatterns

urlpatterns = base_urlpatterns + [
    # Your demo urls go here
]

Now when you want to test your demo URLs you can override the URLConf as:

@override_settings(ROOT_URLCONF='myproject.demo_urls')
def test_foo(self):
    pass

2 Comments

using ROOT_URLCONF is actually a great idea, this works fine and solution comes from refactoring files. Only downside is, my urls file is actually a sub-app urls file, so that method does not perfectly apply here. But having tested it with great success if find it the best solution, thanks
@JeanBouvattier I've updated the way I was clearing the URL resolver cache, it seems like Django specifically has a clear_url_caches function (which is not private, i.e. doesn't have an _ at the start of its name)
0

I wanted to bring your attention to this interesting warning from django docs:

The settings file contains some settings that are only consulted during initialization of Django internals. If you change them with override_settings, the setting is changed if you access it via the django.conf.settings module, however, Django’s internals access it differently. Effectively, using override_settings() or modify_settings() with these settings is probably not going to do what you expect it to do.

This warning is about half way on this page in django docs. Urls file is one of those places that it's only consulted once during initialization.

If you always want settings.DEMO = True for all your tests, it might be worth creating a separate settings file just for tests. I see you need to test various values - below solution is not ideal, but at least gives you option to test the result, depending on the current setting value:

    from django.conf import settings

    
class MyDemoEndpointTestCase(TestCase):
    def test_endpoint_is_reachable_with_demo_equals_true(self):
        if settings.DEMO is 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)
        else:
            pass

    def test_endpoint_is_not_reachable_with_demo_equals_false(self):
        if settings.DEMO is 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)
        else:
            pass

This may be helpful in a scenario if in some environments your settings.DEMO = True and in other environments it's False. In my case I have pipelines set and to run for each environment. This way I can test if I will get expected behaviour specific to this environment settings.DEMO value.

2 Comments

docs.djangoproject.com/en/5.1/topics/settings/… Django doesn't recommend directly overriding settings, beside that would not resolve the fact that my urls.py is run only once before all my tests.
yes, that is correct, urls are evaluated once before you run tests, in the same way the urls.py are evaluated once when you start server.
0

thanks to Vitaliy Desyatka's answer I was able to write a partial solution to my problem by splitting my tests in two:

  • testing urlpatterns content
  • testing view behaviour without url resolution

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)
        
      

I use DRF APIRequestFactory https://www.django-rest-framework.org/api-guide/testing/#creating-test-requests although you could use Django RequestFactory https://docs.djangoproject.com/en/5.1/topics/testing/advanced/#the-request-factory to test behaviour without urls resolution.

The main advantages of this method were:

  • I was able to cover all branches from urls.py
  • I did not have to modify my urls/settings file structure.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.