From e1816669732c54c40122c8a22dcba42f6ee3c326 Mon Sep 17 00:00:00 2001 From: Dan Palmer Date: Sat, 18 Aug 2018 13:15:24 +0100 Subject: [PATCH] Fixed #29687 -- Allowed the test client to serialize list/tuple as JSON. --- AUTHORS | 1 + django/test/client.py | 6 +++--- docs/releases/2.2.txt | 4 ++++ docs/topics/testing/tools.txt | 14 ++++++++++---- tests/test_client/tests.py | 19 +++++++++++++------ tests/test_client/views.py | 4 ++-- 6 files changed, 33 insertions(+), 15 deletions(-) diff --git a/AUTHORS b/AUTHORS index 1da5bc4d87..3eebd6075e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -206,6 +206,7 @@ answer newbie questions, and generally made Django that much better: Daniel Wiesmann Danilo Bargen Dan Johnson + Dan Palmer Dan Poirier Dan Stephenson Dan Watson diff --git a/django/test/client.py b/django/test/client.py index 4f826841cc..0845f0fa4b 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -317,10 +317,10 @@ class RequestFactory: def _encode_json(self, data, content_type): """ - Return encoded JSON if data is a dict and content_type is - application/json. + Return encoded JSON if data is a dict, list, or tuple and content_type + is application/json. """ - should_encode = JSON_CONTENT_TYPE_RE.match(content_type) and isinstance(data, dict) + should_encode = JSON_CONTENT_TYPE_RE.match(content_type) and isinstance(data, (dict, list, tuple)) return json.dumps(data, cls=self.json_encoder) if should_encode else data def _get_path(self, parsed): diff --git a/docs/releases/2.2.txt b/docs/releases/2.2.txt index 4ca3bb9662..ff692151ed 100644 --- a/docs/releases/2.2.txt +++ b/docs/releases/2.2.txt @@ -224,6 +224,10 @@ Tests URL, ignoring the ordering of the query string. :meth:`~.SimpleTestCase.assertRedirects` uses the new assertion. +* The test :class:`~.django.test.Client` now supports automatic JSON + serialization of list and tuple ``data`` when + ``content_type='application/json'``. + URLs ~~~~ diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 92e96e8c98..38b437e18c 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -213,10 +213,11 @@ Use the ``django.test.Client`` class to make requests. name=fred&passwd=secret - If you provide ``content_type`` as :mimetype:`application/json`, a - ``data`` dictionary is serialized using :func:`json.dumps` with - :class:`~django.core.serializers.json.DjangoJSONEncoder`. You can - change the encoder by providing a ``json_encoder`` argument to + If you provide ``content_type`` as :mimetype:`application/json`, the + ``data`` is serialized using :func:`json.dumps` if it's a dict, list, + or tuple. Serialization is performed with + :class:`~django.core.serializers.json.DjangoJSONEncoder` by default, + and can be overridden by providing a ``json_encoder`` argument to :class:`Client`. This serialization also happens for :meth:`put`, :meth:`patch`, and :meth:`delete` requests. @@ -226,6 +227,11 @@ Use the ``django.test.Client`` class to make requests. you can call :func:`json.dumps` on ``data`` before passing it to ``post()`` to achieve the same thing. + .. versionchanged:: 2.2 + + The JSON serialization was extended to support lists and tuples. In + older versions, only dicts are serialized. + If you provide any other ``content_type`` (e.g. :mimetype:`text/xml` for an XML payload), the contents of ``data`` are sent as-is in the POST request, using ``content_type`` in the HTTP ``Content-Type`` diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index b39d5f5e09..22e28be198 100644 --- a/tests/test_client/tests.py +++ b/tests/test_client/tests.py @@ -95,14 +95,21 @@ class ClientTest(TestCase): def test_json_serialization(self): """The test client serializes JSON data.""" methods = ('post', 'put', 'patch', 'delete') + tests = ( + ({'value': 37}, {'value': 37}), + ([37, True], [37, True]), + ((37, False), [37, False]), + ) for method in methods: with self.subTest(method=method): - client_method = getattr(self.client, method) - method_name = method.upper() - response = client_method('/json_view/', {'value': 37}, content_type='application/json') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['data'], 37) - self.assertContains(response, 'Viewing %s page.' % method_name) + for data, expected in tests: + with self.subTest(data): + client_method = getattr(self.client, method) + method_name = method.upper() + response = client_method('/json_view/', data, content_type='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['data'], expected) + self.assertContains(response, 'Viewing %s page.' % method_name) def test_json_encoder_argument(self): """The test Client accepts a json_encoder.""" diff --git a/tests/test_client/views.py b/tests/test_client/views.py index 60a0b765d4..2d076fafaf 100644 --- a/tests/test_client/views.py +++ b/tests/test_client/views.py @@ -83,14 +83,14 @@ def post_view(request): def json_view(request): """ A view that expects a request with the header 'application/json' and JSON - data with a key named 'value'. + data, which is deserialized and included in the context. """ if request.META.get('CONTENT_TYPE') != 'application/json': return HttpResponse() t = Template('Viewing {} page. With data {{ data }}.'.format(request.method)) data = json.loads(request.body.decode('utf-8')) - c = Context({'data': data['value']}) + c = Context({'data': data}) return HttpResponse(t.render(c))