From c490e5ac9cf22bd2d779d923ebe7d9770454bdf9 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 21 Dec 2017 23:00:19 -0500 Subject: [PATCH 1/4] Add formalized URLPatternsTestCase --- rest_framework/test.py | 44 +++++++++++++++++++++++++++++++++++++++++- tests/test_testing.py | 29 +++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index ebad19a4e4..3b745bd622 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -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 @@ -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 diff --git a/tests/test_testing.py b/tests/test_testing.py index 1af6ef02e5..7868f724c1 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -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 ) @@ -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 From 44c2a5ce396e1e56b2c9bc2bb819bd05f35c1672 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 21 Dec 2017 23:04:18 -0500 Subject: [PATCH 2/4] Update versioning tests w/ new URLPatternsTestCase --- tests/test_versioning.py | 33 ++++++--------------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/tests/test_versioning.py b/tests/test_versioning.py index e73059c7d7..7e650e2752 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -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}) @@ -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\d+)/$', dummy_pk_view, name='example-detail') @@ -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\d+)/$', dummy_pk_view, name='namespaced'), ] @@ -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\d+)/$', dummy_pk_view, name='nested'), ] From c30a336b578cfb819f279c1470e2907916a77095 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 21 Dec 2017 23:22:53 -0500 Subject: [PATCH 3/4] Cleanup router tests urlpatterns --- tests/test_routers.py | 59 +++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/tests/test_routers.py b/tests/test_routers.py index 5a1cfe8f40..55ccc647b3 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -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() @@ -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): @@ -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/"} @@ -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') @@ -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') @@ -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 @@ -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)) @@ -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 From 683f1f4406dfad29e844f86641b7c56830c43eca Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 21 Dec 2017 23:49:22 -0500 Subject: [PATCH 4/4] Add docs for URLPatternsTestCase --- docs/api-guide/testing.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index caba5cea22..d2ff6e7cb5 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -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`. @@ -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) + self.assertEqual(len(response.data), 1) + +--- + # Testing responses ## Checking the response data