Sometimes you might need to test a set of urlconf configurations in your django tests. One such scenario might be if your app supplies several swappable views that the user might choose based on their needs, or you want to provide the user a way to use their own view in place of an app-shipped one. For instance, shuup does this in several places by loading a view based on a setting, and embedding the loaded view in the urlconf.

The other day I needed to test multiple configurations of such dynamically-selected view with the test client - i.e. by making requests and inspecting the responses in the context of a certain usage workflow.

It turns out if you just use the @override_settings decorator and only change the setting that is used to load the view, django will not reload the urlconf and your setting will have no effect. My attemts at forcing django to reload the urlconf manually all failed (clear_url_caches() and the like didn’t appear to have any effect).

However, Django does reload the urls if you change the urlconf using override_settings(ROOT_URLCONF=...). Initially this seemed very inconvenient, as per the docs the setting has to be set to string pointing to a module - and I didn’t want to maintain a dedicated module just for the urlconf per each of the test cases I wanted to have.

But lucklily django is equally as happy with ROOT_URLCONF set to a class (during testing):

# myapp/tests.py

import pytest

from django.http import HttpResponse
from django.urls import include, path, reverse
from django.test import override_settings

from myapp.urls import urlpatterns as app_patterns


def view_one(request):
    return HttpResponse("One")


def view_two(request):
    return HttpResponse("Two")


@pytest.mark.parametrize(
    "view, result", ((view_one, "One"), (view_two, "Two"))
)
def test_dynamic_workflow(client, view, result):
    class TempUrls:
        # App urls
        overwrite_patterns = [
            path("", view, name="index"),
        ] + app_patterns

        # Root urls
        urlpatterns = [
            path("", include((overwrite_patterns, "myapp")))
        ]

    with override_settings(ROOT_URLCONF=TempUrls):
        response = client.get(reverse("myapp:index"))
        assert response.content.decode("utf-8") == result

So we simply define a class (TempUrls) within our test that hooks up a different view for each test case (thanks to pytest’s parametrize functionality, our test gets called twice - with a different view and result arguments each time).

The TempUrls class first defines the app urls by prepending our customized urls to the app’s shipped ones (django uses the first matched entry when resolving urls so duplicates are not a problem). Then the class includes the app urls into the root urlconf with the app’s namespace, so that all reverse() calls down the path work like in real world - with the namespace prefix.

And that’s it - each of your test cases will have a different urlconf!