Django testing: per test case urlconfs
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!