diff --git a/AUTHORS b/AUTHORS
index 94f60449261..d3346d2a1d0 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -53,6 +53,7 @@ answer newbie questions, and generally made Django that much better:
Shannon -jj Behrens
Esdras Beleza
James Bennett
+ Ben
Paul Bissex
Simon Blanchard
Andrew Brehaut
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 7700ec7d7aa..023f9b43be7 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -4,6 +4,7 @@ from django.contrib.sites.models import Site
from django.template import Context, loader
from django.core import validators
from django import oldforms
+from django.utils.translation import gettext as _
class UserCreationForm(oldforms.Manipulator):
"A form that creates a user, with no privileges, from the given username and password."
diff --git a/django/test/client.py b/django/test/client.py
index f66b1808672..ca1a04e659d 100644
--- a/django/test/client.py
+++ b/django/test/client.py
@@ -1,7 +1,9 @@
+import sys
from cStringIO import StringIO
from django.conf import settings
from django.core.handlers.base import BaseHandler
from django.core.handlers.wsgi import WSGIRequest
+from django.core.signals import got_request_exception
from django.dispatch import dispatcher
from django.http import urlencode, SimpleCookie
from django.test import signals
@@ -100,6 +102,14 @@ class Client:
self.defaults = defaults
self.cookies = SimpleCookie()
self.session = {}
+ self.exc_info = None
+
+ def store_exc_info(self, *args, **kwargs):
+ """
+ Utility method that can be used to store exceptions when they are
+ generated by a view.
+ """
+ self.exc_info = sys.exc_info()
def request(self, **request):
"""
@@ -128,6 +138,9 @@ class Client:
on_template_render = curry(store_rendered_templates, data)
dispatcher.connect(on_template_render, signal=signals.template_rendered)
+ # Capture exceptions created by the handler
+ dispatcher.connect(self.store_exc_info, signal=got_request_exception)
+
response = self.handler(environ)
# Add any rendered template detail to the response
@@ -142,6 +155,11 @@ class Client:
else:
setattr(response, detail, None)
+ # Look for a signalled exception and reraise it
+ if self.exc_info:
+ raise self.exc_info[1], None, self.exc_info[2]
+
+ # Update persistent cookie and session data
if response.cookies:
self.cookies.update(response.cookies)
diff --git a/docs/testing.txt b/docs/testing.txt
index 99067497232..cab31ed63b5 100644
--- a/docs/testing.txt
+++ b/docs/testing.txt
@@ -291,6 +291,17 @@ for testing purposes:
.. _RFC2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+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...catch``
+block, or ``unittest.TestCase.assertRaises()`` to test for exceptions.
+
+The only exceptions that are not visible in a Test Case are ``Http404``,
+``PermissionDenied`` and ``SystemExit``. Django catches these exceptions
+internally and converts them into the appropriate HTTP responses codes.
+
Persistent state
~~~~~~~~~~~~~~~~
diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py
index b6c3fbd5b13..74f5e9700ce 100644
--- a/tests/modeltests/test_client/models.py
+++ b/tests/modeltests/test_client/models.py
@@ -114,4 +114,14 @@ class ClientTest(unittest.TestCase):
# Check that the session was modified
self.assertEquals(self.client.session['tobacconist'], 'hovercraft')
-
\ No newline at end of file
+
+ def test_view_with_exception(self):
+ "Request a page that is known to throw an error"
+ self.assertRaises(KeyError, self.client.get, "/test_client/broken_view/")
+
+ #Try the same assertion, a different way
+ try:
+ self.client.get('/test_client/broken_view/')
+ self.fail('Should raise an error')
+ except KeyError:
+ pass
diff --git a/tests/modeltests/test_client/urls.py b/tests/modeltests/test_client/urls.py
index 5c77260c924..96da4ec34e2 100644
--- a/tests/modeltests/test_client/urls.py
+++ b/tests/modeltests/test_client/urls.py
@@ -6,5 +6,6 @@ urlpatterns = patterns('',
(r'^post_view/$', views.post_view),
(r'^redirect_view/$', views.redirect_view),
(r'^login_protected_view/$', views.login_protected_view),
- (r'^session_view/$', views.session_view)
+ (r'^session_view/$', views.session_view),
+ (r'^broken_view/$', views.broken_view)
)
diff --git a/tests/modeltests/test_client/views.py b/tests/modeltests/test_client/views.py
index 406a814dec1..b1f2739845a 100644
--- a/tests/modeltests/test_client/views.py
+++ b/tests/modeltests/test_client/views.py
@@ -36,11 +36,13 @@ login_protected_view = login_required(login_protected_view)
def session_view(request):
"A view that modifies the session"
-
request.session['tobacconist'] = 'hovercraft'
t = Template('This is a view that modifies the session.',
name='Session Modifying View Template')
c = Context()
return HttpResponse(t.render(c))
-
\ No newline at end of file
+
+def broken_view(request):
+ """A view which just raises an exception, simulating a broken view."""
+ raise KeyError("Oops! Looks like you wrote some bad code.")