Skip to content

Commit b659677

Browse files
Ryan P Kilbycarltongibson
authored andcommitted
Formalize URLPatternsTestCase (#5703)
* Add formalized URLPatternsTestCase * Update versioning tests w/ new URLPatternsTestCase * Cleanup router tests urlpatterns * Add docs for URLPatternsTestCase
1 parent 6b0bf72 commit b659677

File tree

5 files changed

+138
-55
lines changed

5 files changed

+138
-55
lines changed

docs/api-guide/testing.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ similar way as with `RequestsClient`.
292292

293293
---
294294

295-
# Test cases
295+
# API Test cases
296296

297297
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`.
298298

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

325325
---
326326

327+
# URLPatternsTestCase
328+
329+
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.
330+
331+
## Example
332+
333+
from django.urls import include, path, reverse
334+
from rest_framework.test import APITestCase, URLPatternsTestCase
335+
336+
337+
class AccountTests(APITestCase, URLPatternsTestCase):
338+
urlpatterns = [
339+
path('api/', include('api.urls')),
340+
]
341+
342+
def test_create_account(self):
343+
"""
344+
Ensure we can create a new account object.
345+
"""
346+
url = reverse('account-list')
347+
response = self.client.get(url, format='json')
348+
self.assertEqual(response.status_code, status.HTTP_200_OK)
349+
self.assertEqual(len(response.data), 1)
350+
351+
---
352+
327353
# Testing responses
328354

329355
## Checking the response data

rest_framework/test.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
from __future__ import unicode_literals
66

77
import io
8+
from importlib import import_module
89

910
from django.conf import settings
1011
from django.core.exceptions import ImproperlyConfigured
1112
from django.core.handlers.wsgi import WSGIHandler
12-
from django.test import testcases
13+
from django.test import override_settings, testcases
1314
from django.test.client import Client as DjangoClient
1415
from django.test.client import RequestFactory as DjangoRequestFactory
1516
from django.test.client import ClientHandler
@@ -358,3 +359,44 @@ class APISimpleTestCase(testcases.SimpleTestCase):
358359

359360
class APILiveServerTestCase(testcases.LiveServerTestCase):
360361
client_class = APIClient
362+
363+
364+
class URLPatternsTestCase(testcases.SimpleTestCase):
365+
"""
366+
Isolate URL patterns on a per-TestCase basis. For example,
367+
368+
class ATestCase(URLPatternsTestCase):
369+
urlpatterns = [...]
370+
371+
def test_something(self):
372+
...
373+
374+
class AnotherTestCase(URLPatternsTestCase):
375+
urlpatterns = [...]
376+
377+
def test_something_else(self):
378+
...
379+
"""
380+
@classmethod
381+
def setUpClass(cls):
382+
# Get the module of the TestCase subclass
383+
cls._module = import_module(cls.__module__)
384+
cls._override = override_settings(ROOT_URLCONF=cls.__module__)
385+
386+
if hasattr(cls._module, 'urlpatterns'):
387+
cls._module_urlpatterns = cls._module.urlpatterns
388+
389+
cls._module.urlpatterns = cls.urlpatterns
390+
391+
cls._override.enable()
392+
super(URLPatternsTestCase, cls).setUpClass()
393+
394+
@classmethod
395+
def tearDownClass(cls):
396+
super(URLPatternsTestCase, cls).tearDownClass()
397+
cls._override.disable()
398+
399+
if hasattr(cls, '_module_urlpatterns'):
400+
cls._module.urlpatterns = cls._module_urlpatterns
401+
else:
402+
del cls._module.urlpatterns

tests/test_routers.py

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from rest_framework.decorators import detail_route, list_route
1515
from rest_framework.response import Response
1616
from rest_framework.routers import DefaultRouter, SimpleRouter
17-
from rest_framework.test import APIRequestFactory
17+
from rest_framework.test import APIRequestFactory, URLPatternsTestCase
1818
from rest_framework.utils import json
1919

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

9191
empty_prefix_router = SimpleRouter()
9292
empty_prefix_router.register(r'', EmptyPrefixViewSet, base_name='empty_prefix')
93-
empty_prefix_urls = [
94-
url(r'^', include(empty_prefix_router.urls)),
95-
]
9693

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

100-
urlpatterns = [
101-
url(r'^non-namespaced/', include(namespaced_router.urls)),
102-
url(r'^namespaced/', include((namespaced_router.urls, 'example'), namespace='example')),
103-
url(r'^example/', include(notes_router.urls)),
104-
url(r'^example2/', include(kwarged_notes_router.urls)),
105-
106-
url(r'^empty-prefix/', include(empty_prefix_urls)),
107-
url(r'^regex/', include(regex_url_path_router.urls))
108-
]
109-
11097

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

158145

159-
@override_settings(ROOT_URLCONF='tests.test_routers')
160-
class TestRootView(TestCase):
146+
class TestRootView(URLPatternsTestCase, TestCase):
147+
urlpatterns = [
148+
url(r'^non-namespaced/', include(namespaced_router.urls)),
149+
url(r'^namespaced/', include((namespaced_router.urls, 'namespaced'), namespace='namespaced')),
150+
]
151+
161152
def test_retrieve_namespaced_root(self):
162153
response = self.client.get('/namespaced/')
163154
assert response.data == {"example": "http://testserver/namespaced/example/"}
@@ -167,11 +158,15 @@ def test_retrieve_non_namespaced_root(self):
167158
assert response.data == {"example": "http://testserver/non-namespaced/example/"}
168159

169160

170-
@override_settings(ROOT_URLCONF='tests.test_routers')
171-
class TestCustomLookupFields(TestCase):
161+
class TestCustomLookupFields(URLPatternsTestCase, TestCase):
172162
"""
173163
Ensure that custom lookup fields are correctly routed.
174164
"""
165+
urlpatterns = [
166+
url(r'^example/', include(notes_router.urls)),
167+
url(r'^example2/', include(kwarged_notes_router.urls)),
168+
]
169+
175170
def setUp(self):
176171
RouterTestModel.objects.create(uuid='123', text='foo bar')
177172
RouterTestModel.objects.create(uuid='a b', text='baz qux')
@@ -219,12 +214,17 @@ def test_urls_limited_by_lookup_value_regex(self):
219214

220215

221216
@override_settings(ROOT_URLCONF='tests.test_routers')
222-
class TestLookupUrlKwargs(TestCase):
217+
class TestLookupUrlKwargs(URLPatternsTestCase, TestCase):
223218
"""
224219
Ensure the router honors lookup_url_kwarg.
225220
226221
Setup a deep lookup_field, but map it to a simple URL kwarg.
227222
"""
223+
urlpatterns = [
224+
url(r'^example/', include(notes_router.urls)),
225+
url(r'^example2/', include(kwarged_notes_router.urls)),
226+
]
227+
228228
def setUp(self):
229229
RouterTestModel.objects.create(uuid='123', text='foo bar')
230230

@@ -408,8 +408,11 @@ def test_inherited_list_and_detail_route_decorators(self):
408408
self._test_list_and_detail_route_decorators(SubDynamicListAndDetailViewSet)
409409

410410

411-
@override_settings(ROOT_URLCONF='tests.test_routers')
412-
class TestEmptyPrefix(TestCase):
411+
class TestEmptyPrefix(URLPatternsTestCase, TestCase):
412+
urlpatterns = [
413+
url(r'^empty-prefix/', include(empty_prefix_router.urls)),
414+
]
415+
413416
def test_empty_prefix_list(self):
414417
response = self.client.get('/empty-prefix/')
415418
assert response.status_code == 200
@@ -422,8 +425,11 @@ def test_empty_prefix_detail(self):
422425
assert json.loads(response.content.decode('utf-8')) == {'uuid': '111', 'text': 'First'}
423426

424427

425-
@override_settings(ROOT_URLCONF='tests.test_routers')
426-
class TestRegexUrlPath(TestCase):
428+
class TestRegexUrlPath(URLPatternsTestCase, TestCase):
429+
urlpatterns = [
430+
url(r'^regex/', include(regex_url_path_router.urls)),
431+
]
432+
427433
def test_regex_url_path_list(self):
428434
kwarg = '1234'
429435
response = self.client.get('/regex/list/{}/'.format(kwarg))
@@ -438,8 +444,11 @@ def test_regex_url_path_detail(self):
438444
assert json.loads(response.content.decode('utf-8')) == {'pk': pk, 'kwarg': kwarg}
439445

440446

441-
@override_settings(ROOT_URLCONF='tests.test_routers')
442-
class TestViewInitkwargs(TestCase):
447+
class TestViewInitkwargs(URLPatternsTestCase, TestCase):
448+
urlpatterns = [
449+
url(r'^example/', include(notes_router.urls)),
450+
]
451+
443452
def test_suffix(self):
444453
match = resolve('/example/notes/')
445454
initkwargs = match.func.initkwargs

tests/test_testing.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from rest_framework.decorators import api_view
1313
from rest_framework.response import Response
1414
from rest_framework.test import (
15-
APIClient, APIRequestFactory, force_authenticate
15+
APIClient, APIRequestFactory, URLPatternsTestCase, force_authenticate
1616
)
1717

1818

@@ -283,3 +283,30 @@ def test_empty_request_content_type(self):
283283
content_type='application/json',
284284
)
285285
assert request.META['CONTENT_TYPE'] == 'application/json'
286+
287+
288+
class TestUrlPatternTestCase(URLPatternsTestCase):
289+
urlpatterns = [
290+
url(r'^$', view),
291+
]
292+
293+
@classmethod
294+
def setUpClass(cls):
295+
assert urlpatterns is not cls.urlpatterns
296+
super(TestUrlPatternTestCase, cls).setUpClass()
297+
assert urlpatterns is cls.urlpatterns
298+
299+
@classmethod
300+
def tearDownClass(cls):
301+
assert urlpatterns is cls.urlpatterns
302+
super(TestUrlPatternTestCase, cls).tearDownClass()
303+
assert urlpatterns is not cls.urlpatterns
304+
305+
def test_urlpatterns(self):
306+
assert self.client.get('/').status_code == 200
307+
308+
309+
class TestExistingPatterns(TestCase):
310+
def test_urlpatterns(self):
311+
# sanity test to ensure that this test module does not have a '/' route
312+
assert self.client.get('/').status_code == 404

tests/test_versioning.py

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,12 @@
77
from rest_framework.relations import PKOnlyObject
88
from rest_framework.response import Response
99
from rest_framework.reverse import reverse
10-
from rest_framework.test import APIRequestFactory, APITestCase
10+
from rest_framework.test import (
11+
APIRequestFactory, APITestCase, URLPatternsTestCase
12+
)
1113
from rest_framework.versioning import NamespaceVersioning
1214

1315

14-
@override_settings(ROOT_URLCONF='tests.test_versioning')
15-
class URLPatternsTestCase(APITestCase):
16-
"""
17-
Isolates URL patterns used during testing on the test class itself.
18-
For example:
19-
20-
class MyTestCase(URLPatternsTestCase):
21-
urlpatterns = [
22-
...
23-
]
24-
25-
def test_something(self):
26-
...
27-
"""
28-
def setUp(self):
29-
global urlpatterns
30-
urlpatterns = self.urlpatterns
31-
32-
def tearDown(self):
33-
global urlpatterns
34-
urlpatterns = []
35-
36-
3716
class RequestVersionView(APIView):
3817
def get(self, request, *args, **kwargs):
3918
return Response({'version': request.version})
@@ -163,7 +142,7 @@ class FakeResolverMatch:
163142
assert response.data == {'version': None}
164143

165144

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

331310

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

363342

364-
class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(URLPatternsTestCase):
343+
class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(URLPatternsTestCase, APITestCase):
365344
nested = [
366345
url(r'^namespaced/(?P<pk>\d+)/$', dummy_pk_view, name='nested'),
367346
]

0 commit comments

Comments
 (0)