From 7feddd878cd0d4ad6cc98f0da2b597d603a211a7 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 20 Feb 2019 03:16:10 -0800 Subject: [PATCH] Fixed #18707 -- Added support for the test client to return 500 responses. --- django/test/client.py | 7 +++++-- docs/releases/3.0.txt | 8 +++++++- docs/topics/testing/tools.txt | 37 ++++++++++++++++++++++++++++++++--- tests/test_client/tests.py | 14 +++++++++++++ 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/django/test/client.py b/django/test/client.py index afc803789e..c8d8508272 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -440,9 +440,10 @@ class Client(RequestFactory): contexts and templates produced by a view, rather than the HTML rendered to the end-user. """ - def __init__(self, enforce_csrf_checks=False, **defaults): + def __init__(self, enforce_csrf_checks=False, raise_request_exception=True, **defaults): super().__init__(**defaults) self.handler = ClientHandler(enforce_csrf_checks) + self.raise_request_exception = raise_request_exception self.exc_info = None def store_exc_info(self, **kwargs): @@ -497,10 +498,12 @@ class Client(RequestFactory): # exception data, then re-raise the signalled exception. # Also make sure that the signalled exception is cleared from # the local cache! + response.exc_info = self.exc_info if self.exc_info: _, exc_value, _ = self.exc_info self.exc_info = None - raise exc_value + if self.raise_request_exception: + raise exc_value # Save the client and request that stimulated the response. response.client = self diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index f036969c92..77bace661b 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -187,7 +187,13 @@ Templates Tests ~~~~~ -* ... +* The new test :class:`~django.test.Client` argument + ``raise_request_exception`` allows controlling whether or not exceptions + raised during the request should also be raised in the test. The value + defaults to ``True`` for backwards compatibility. If it is ``False`` and an + exception occurs, the test client will return a 500 response with the + attribute :attr:`~django.test.Response.exc_info`, a tuple providing + information of the exception that occurred. URLs ~~~~ diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 4ffe84dd04..ea01bd026b 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -128,6 +128,14 @@ Use the ``django.test.Client`` class to make requests. The ``json_encoder`` argument allows setting a custom JSON encoder for the JSON serialization that's described in :meth:`post`. + The ``raise_request_exception`` argument allows controlling whether or not + exceptions raised during the request should also be raised in the test. + Defaults to ``True``. + + .. versionadded:: 3.0 + + The ``raise_request_exception`` argument was added. + Once you have a ``Client`` instance, you can call any of the following methods: @@ -476,6 +484,23 @@ Specifically, a ``Response`` object has the following attributes: :attr:`~django.template.response.SimpleTemplateResponse.context_data` may be a suitable alternative on responses with that attribute. + .. attribute:: exc_info + + .. versionadded:: 3.0 + + A tuple of three values that provides information about the unhandled + exception, if any, that occurred during the view. + + The values are (type, value, traceback), the same as returned by + Python's :func:`sys.exc_info`. Their meanings are: + + - *type*: The type of the exception. + - *value*: The exception instance. + - *traceback*: A traceback object which encapsulates the call stack at + the point where the exception originally occurred. + + If no exception occurred, then ``exc_info`` will be ``None``. + .. method:: json(**kwargs) The body of the response, parsed as JSON. Extra keyword arguments are @@ -544,9 +569,10 @@ content type of a response using ``response['Content-Type']``. Exceptions ---------- -If you point the test client at a view that raises an exception, that exception -will be visible in the test case. You can then use a standard ``try ... except`` -block or :meth:`~unittest.TestCase.assertRaises` to test for exceptions. +If you point the test client at a view that raises an exception and +``Client.raise_request_exception`` is ``True``, that exception will be visible +in the test case. You can then use a standard ``try ... except`` block or +:meth:`~unittest.TestCase.assertRaises` to test for exceptions. The only exceptions that are not visible to the test client are :class:`~django.http.Http404`, @@ -555,6 +581,11 @@ The only exceptions that are not visible to the test client are exceptions internally and converts them into the appropriate HTTP response codes. In these cases, you can check ``response.status_code`` in your test. +If ``Client.raise_request_exception`` is ``False``, the test client will return a +500 response as would be returned to a browser. The response has the attribute +:attr:`~Response.exc_info` to provide information about the unhandled +exception. + Persistent state ---------------- diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index 432865328f..02f5113890 100644 --- a/tests/test_client/tests.py +++ b/tests/test_client/tests.py @@ -759,6 +759,20 @@ class ClientTest(TestCase): with self.assertRaises(KeyError): self.client.get("/broken_view/") + def test_exc_info(self): + client = Client(raise_request_exception=False) + response = client.get("/broken_view/") + self.assertEqual(response.status_code, 500) + exc_type, exc_value, exc_traceback = response.exc_info + self.assertIs(exc_type, KeyError) + self.assertIsInstance(exc_value, KeyError) + self.assertEqual(str(exc_value), "'Oops! Looks like you wrote some bad code.'") + self.assertIsNotNone(exc_traceback) + + def test_exc_info_none(self): + response = self.client.get("/get_view/") + self.assertIsNone(response.exc_info) + def test_mail_sending(self): "Mail is redirected to a dummy outbox during test setup" response = self.client.get('/mail_sending_view/')