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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy