Skip to content

Formalize URLPatternsTestCase #5703

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion docs/api-guide/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ similar way as with `RequestsClient`.

---

# Test cases
# API Test cases

REST framework includes the following test case classes, that mirror the existing Django test case classes, but use `APIClient` instead of Django's default `Client`.

Expand Down Expand Up @@ -324,6 +324,32 @@ You can use any of REST framework's test case classes as you would for the regul

---

# URLPatternsTestCase

REST framework also provides a test case class for isolating `urlpatterns` on a per-class basis. Note that this inherits from Django's `SimpleTestCase`, and will most likely need to be mixed with another test case class.

## Example

from django.urls import include, path, reverse
from rest_framework.test import APITestCase, URLPatternsTestCase


class AccountTests(APITestCase, URLPatternsTestCase):
urlpatterns = [
path('api/', include('api.urls')),
]

def test_create_account(self):
"""
Ensure we can create a new account object.
"""
url = reverse('account-list')
response = self.client.get(url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about using pytest style asserts?

Copy link
Member Author

@rpkilby rpkilby Dec 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistency with the other test case docs. That said, the testing doc does use a mix of both pytest and unittest assertions, although I'd probably opt for the latter given that they're part of the standard library.

self.assertEqual(len(response.data), 1)

---

# Testing responses

## Checking the response data
Expand Down
44 changes: 43 additions & 1 deletion rest_framework/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
from __future__ import unicode_literals

import io
from importlib import import_module

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.handlers.wsgi import WSGIHandler
from django.test import testcases
from django.test import override_settings, testcases
from django.test.client import Client as DjangoClient
from django.test.client import RequestFactory as DjangoRequestFactory
from django.test.client import ClientHandler
Expand Down Expand Up @@ -358,3 +359,44 @@ class APISimpleTestCase(testcases.SimpleTestCase):

class APILiveServerTestCase(testcases.LiveServerTestCase):
client_class = APIClient


class URLPatternsTestCase(testcases.SimpleTestCase):
"""
Isolate URL patterns on a per-TestCase basis. For example,

class ATestCase(URLPatternsTestCase):
urlpatterns = [...]

def test_something(self):
...

class AnotherTestCase(URLPatternsTestCase):
urlpatterns = [...]

def test_something_else(self):
...
"""
@classmethod
def setUpClass(cls):
# Get the module of the TestCase subclass
cls._module = import_module(cls.__module__)
cls._override = override_settings(ROOT_URLCONF=cls.__module__)

if hasattr(cls._module, 'urlpatterns'):
cls._module_urlpatterns = cls._module.urlpatterns

cls._module.urlpatterns = cls.urlpatterns

cls._override.enable()
super(URLPatternsTestCase, cls).setUpClass()

@classmethod
def tearDownClass(cls):
super(URLPatternsTestCase, cls).tearDownClass()
cls._override.disable()

if hasattr(cls, '_module_urlpatterns'):
cls._module.urlpatterns = cls._module_urlpatterns
else:
del cls._module.urlpatterns
59 changes: 34 additions & 25 deletions tests/test_routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
from rest_framework.routers import DefaultRouter, SimpleRouter
from rest_framework.test import APIRequestFactory
from rest_framework.test import APIRequestFactory, URLPatternsTestCase
from rest_framework.utils import json

factory = APIRequestFactory()
Expand Down Expand Up @@ -90,23 +90,10 @@ def regex_url_path_detail(self, request, *args, **kwargs):

empty_prefix_router = SimpleRouter()
empty_prefix_router.register(r'', EmptyPrefixViewSet, base_name='empty_prefix')
empty_prefix_urls = [
url(r'^', include(empty_prefix_router.urls)),
]

regex_url_path_router = SimpleRouter()
regex_url_path_router.register(r'', RegexUrlPathViewSet, base_name='regex')

urlpatterns = [
url(r'^non-namespaced/', include(namespaced_router.urls)),
url(r'^namespaced/', include((namespaced_router.urls, 'example'), namespace='example')),
url(r'^example/', include(notes_router.urls)),
url(r'^example2/', include(kwarged_notes_router.urls)),

url(r'^empty-prefix/', include(empty_prefix_urls)),
url(r'^regex/', include(regex_url_path_router.urls))
]


class BasicViewSet(viewsets.ViewSet):
def list(self, request, *args, **kwargs):
Expand Down Expand Up @@ -156,8 +143,12 @@ def test_link_and_action_decorator(self):
assert route.mapping[method] == endpoint


@override_settings(ROOT_URLCONF='tests.test_routers')
class TestRootView(TestCase):
class TestRootView(URLPatternsTestCase, TestCase):
urlpatterns = [
url(r'^non-namespaced/', include(namespaced_router.urls)),
url(r'^namespaced/', include((namespaced_router.urls, 'namespaced'), namespace='namespaced')),
]

def test_retrieve_namespaced_root(self):
response = self.client.get('/namespaced/')
assert response.data == {"example": "http://testserver/namespaced/example/"}
Expand All @@ -167,11 +158,15 @@ def test_retrieve_non_namespaced_root(self):
assert response.data == {"example": "http://testserver/non-namespaced/example/"}


@override_settings(ROOT_URLCONF='tests.test_routers')
class TestCustomLookupFields(TestCase):
class TestCustomLookupFields(URLPatternsTestCase, TestCase):
"""
Ensure that custom lookup fields are correctly routed.
"""
urlpatterns = [
url(r'^example/', include(notes_router.urls)),
url(r'^example2/', include(kwarged_notes_router.urls)),
]

def setUp(self):
RouterTestModel.objects.create(uuid='123', text='foo bar')
RouterTestModel.objects.create(uuid='a b', text='baz qux')
Expand Down Expand Up @@ -219,12 +214,17 @@ def test_urls_limited_by_lookup_value_regex(self):


@override_settings(ROOT_URLCONF='tests.test_routers')
class TestLookupUrlKwargs(TestCase):
class TestLookupUrlKwargs(URLPatternsTestCase, TestCase):
"""
Ensure the router honors lookup_url_kwarg.

Setup a deep lookup_field, but map it to a simple URL kwarg.
"""
urlpatterns = [
url(r'^example/', include(notes_router.urls)),
url(r'^example2/', include(kwarged_notes_router.urls)),
]

def setUp(self):
RouterTestModel.objects.create(uuid='123', text='foo bar')

Expand Down Expand Up @@ -408,8 +408,11 @@ def test_inherited_list_and_detail_route_decorators(self):
self._test_list_and_detail_route_decorators(SubDynamicListAndDetailViewSet)


@override_settings(ROOT_URLCONF='tests.test_routers')
class TestEmptyPrefix(TestCase):
class TestEmptyPrefix(URLPatternsTestCase, TestCase):
urlpatterns = [
url(r'^empty-prefix/', include(empty_prefix_router.urls)),
]

def test_empty_prefix_list(self):
response = self.client.get('/empty-prefix/')
assert response.status_code == 200
Expand All @@ -422,8 +425,11 @@ def test_empty_prefix_detail(self):
assert json.loads(response.content.decode('utf-8')) == {'uuid': '111', 'text': 'First'}


@override_settings(ROOT_URLCONF='tests.test_routers')
class TestRegexUrlPath(TestCase):
class TestRegexUrlPath(URLPatternsTestCase, TestCase):
urlpatterns = [
url(r'^regex/', include(regex_url_path_router.urls)),
]

def test_regex_url_path_list(self):
kwarg = '1234'
response = self.client.get('/regex/list/{}/'.format(kwarg))
Expand All @@ -438,8 +444,11 @@ def test_regex_url_path_detail(self):
assert json.loads(response.content.decode('utf-8')) == {'pk': pk, 'kwarg': kwarg}


@override_settings(ROOT_URLCONF='tests.test_routers')
class TestViewInitkwargs(TestCase):
class TestViewInitkwargs(URLPatternsTestCase, TestCase):
urlpatterns = [
url(r'^example/', include(notes_router.urls)),
]

def test_suffix(self):
match = resolve('/example/notes/')
initkwargs = match.func.initkwargs
Expand Down
29 changes: 28 additions & 1 deletion tests/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.test import (
APIClient, APIRequestFactory, force_authenticate
APIClient, APIRequestFactory, URLPatternsTestCase, force_authenticate
)


Expand Down Expand Up @@ -283,3 +283,30 @@ def test_empty_request_content_type(self):
content_type='application/json',
)
assert request.META['CONTENT_TYPE'] == 'application/json'


class TestUrlPatternTestCase(URLPatternsTestCase):
urlpatterns = [
url(r'^$', view),
]

@classmethod
def setUpClass(cls):
assert urlpatterns is not cls.urlpatterns
super(TestUrlPatternTestCase, cls).setUpClass()
assert urlpatterns is cls.urlpatterns

@classmethod
def tearDownClass(cls):
assert urlpatterns is cls.urlpatterns
super(TestUrlPatternTestCase, cls).tearDownClass()
assert urlpatterns is not cls.urlpatterns

def test_urlpatterns(self):
assert self.client.get('/').status_code == 200


class TestExistingPatterns(TestCase):
def test_urlpatterns(self):
# sanity test to ensure that this test module does not have a '/' route
assert self.client.get('/').status_code == 404
33 changes: 6 additions & 27 deletions tests/test_versioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,12 @@
from rest_framework.relations import PKOnlyObject
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.test import APIRequestFactory, APITestCase
from rest_framework.test import (
APIRequestFactory, APITestCase, URLPatternsTestCase
)
from rest_framework.versioning import NamespaceVersioning


@override_settings(ROOT_URLCONF='tests.test_versioning')
class URLPatternsTestCase(APITestCase):
"""
Isolates URL patterns used during testing on the test class itself.
For example:

class MyTestCase(URLPatternsTestCase):
urlpatterns = [
...
]

def test_something(self):
...
"""
def setUp(self):
global urlpatterns
urlpatterns = self.urlpatterns

def tearDown(self):
global urlpatterns
urlpatterns = []


class RequestVersionView(APIView):
def get(self, request, *args, **kwargs):
return Response({'version': request.version})
Expand Down Expand Up @@ -163,7 +142,7 @@ class FakeResolverMatch:
assert response.data == {'version': None}


class TestURLReversing(URLPatternsTestCase):
class TestURLReversing(URLPatternsTestCase, APITestCase):
included = [
url(r'^namespaced/$', dummy_view, name='another'),
url(r'^example/(?P<pk>\d+)/$', dummy_pk_view, name='example-detail')
Expand Down Expand Up @@ -329,7 +308,7 @@ def test_missing_with_default_and_none_allowed(self):
assert response.data == {'version': 'v2'}


class TestHyperlinkedRelatedField(URLPatternsTestCase):
class TestHyperlinkedRelatedField(URLPatternsTestCase, APITestCase):
included = [
url(r'^namespaced/(?P<pk>\d+)/$', dummy_pk_view, name='namespaced'),
]
Expand Down Expand Up @@ -361,7 +340,7 @@ def test_bug_2489(self):
self.field.to_internal_value('/v2/namespaced/3/')


class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(URLPatternsTestCase):
class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(URLPatternsTestCase, APITestCase):
nested = [
url(r'^namespaced/(?P<pk>\d+)/$', dummy_pk_view, name='nested'),
]
Expand Down