Index: AUTHORS
===================================================================
--- AUTHORS	(revision 16452)
+++ AUTHORS	(working copy)
@@ -211,6 +211,7 @@
     pradeep.gowda@gmail.com
     Collin Grady <collin@collingrady.com>
     Gabriel Grant <g@briel.ca>
+    Martin Green
     Simon Greenhill <dev@simon.net.nz>
     Owen Griffiths
     Espen Grindhaug <http://grindhaug.org/>
Index: docs/topics/testing.txt
===================================================================
--- docs/topics/testing.txt	(revision 16452)
+++ docs/topics/testing.txt	(working copy)
@@ -1496,7 +1496,27 @@
 
     ``errors`` is an error string, or a list of error strings, that are
     expected as a result of form validation.
+ 
+.. method:: TestCase.assertFormsetError(response, formset, form, field, errors, msg_prefix='')
 
+    .. versionadded:: 1.4
+    
+    Asserts that the formset raises the provided list of errors when rendered.
+
+    ``formset`` is the name the ``Formset`` instance was given in the template
+    context.
+
+    ``form`` is the number of the form within the ``Formset``.  If ``form``
+    has a value of ``None``, non-form errors (errors you can access via
+    ``formset.non_form_errors()``) will be checked.
+
+    ``field`` is the name of the field on the form to check. If ``field``
+    has a value of ``None``, non-field errors (errors you can access via
+    ``form.non_field_errors()``) will be checked.
+
+    ``errors`` is an error string, or a list of error strings, that are
+    expected as a result of form validation.
+
 .. method:: TestCase.assertTemplateUsed(response, template_name, msg_prefix='')
 
     Asserts that the template with the given name was used in rendering the
Index: docs/releases/1.4.txt
===================================================================
--- docs/releases/1.4.txt	(revision 16452)
+++ docs/releases/1.4.txt	(working copy)
@@ -192,6 +192,8 @@
 * In the documentation, a helpful :doc:`security overview </topics/security>`
   page.
 
+* Added an `assertFormsetError` into the unittest framework
+
 .. _backwards-incompatible-changes-1.4:
 
 Backwards incompatible changes in 1.4
Index: django/test/testcases.py
===================================================================
--- django/test/testcases.py	(revision 16452)
+++ django/test/testcases.py	(working copy)
@@ -504,6 +504,84 @@
             self.fail(msg_prefix + "The form '%s' was not used to render the"
                       " response" % form)
 
+    def assertFormsetError(self, response, formset, form, field, errors, 
+                           msg_prefix=''):
+        """
+        Asserts that a formset used to render the response has a specific error.
+
+        For field errors specify the number of the form and the field.
+        For non-field errors specify the number of the form and the field
+        as None.
+        For non-form errors specify form as None and the field as None.
+        """
+        if msg_prefix:
+            msg_prefix += ": "
+
+        # Put context(s) into a list to simplify processing.
+        contexts = to_list(response.context)
+        if not contexts:
+            self.fail(msg_prefix + 'Response did not use any contexts to '
+                      'render the response')
+
+        # Put error(s) into a list to simplify processing.
+        errors = to_list(errors)
+
+        # Search all contexts for the error.
+        found_formset = False
+        for i,context in enumerate(contexts):
+            if formset not in context:
+                continue
+            found_formset = True
+            for err in errors:
+                if field is not None:
+                    if field in context[formset].forms[form].errors:
+                        field_errors = \
+                                     context[formset].forms[form].errors[field]
+                        self.assertTrue(err in field_errors, 
+                                msg_prefix + "The field '%s' on formset '%s', "
+                                "form %d in context %d does not contain the "
+                                "error '%s' (actual errors: %s)" %
+                                        (field, formset, form, i, err,
+                                        repr(field_errors)))
+                    elif field in context[formset].forms[form].fields:
+                        self.fail(msg_prefix + "The field '%s' "
+                                  "on formset '%s', form %d in "
+                                  "context %d contains no errors" %
+                                        (field, formset, form,i))
+                    else:
+                        self.fail(msg_prefix + "The formset '%s', form %d in "
+                                 "context %d does not contain the field '%s'" %
+                                        (formset, form, i, field))
+                elif form is not None:
+                    non_field_errors = \
+                                context[formset].forms[form].non_field_errors()
+                    self.assertFalse(len(non_field_errors) == 0,
+                                msg_prefix + "The formset '%s', form %d in "
+                                "context %d does not contain any non-field "
+                                "errors." % (formset, form, i))
+                    self.assertTrue(err in non_field_errors,
+                                    msg_prefix + "The formset '%s', form %d "
+                                    "in context %d does not contain the "
+                                    "non-field error '%s' "
+                                    "(actual errors: %s)" %
+                                        (formset, form, i, err, 
+                                         repr(non_field_errors)))
+                else:
+                    non_form_errors = context[formset].non_form_errors()
+                    self.assertFalse(len(non_form_errors) == 0,
+                                     msg_prefix + "The formset '%s' in "
+                                     "context %d does not contain any "
+                                     "non-form errors." % (formset, i))
+                    self.assertTrue(err in non_form_errors,
+                                    msg_prefix + "The formset '%s' in context "
+                                    "%d does not contain the "
+                                    "non-form error '%s' (actual errors: %s)" %
+                                      (formset, i, err, repr(non_form_errors)))
+        if not found_formset:
+            self.fail(msg_prefix + "The formset '%s' was not used to render "
+                      "the response" % formset)
+
+
     def assertTemplateUsed(self, response, template_name, msg_prefix=''):
         """
         Asserts that the template with the provided name was used in rendering
Index: tests/modeltests/test_client/views.py
===================================================================
--- tests/modeltests/test_client/views.py	(revision 16452)
+++ tests/modeltests/test_client/views.py	(working copy)
@@ -4,7 +4,8 @@
 from django.template import Context, Template
 from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound
 from django.contrib.auth.decorators import login_required, permission_required
-from django.forms.forms import Form
+from django.forms.forms import Form, ValidationError
+from django.forms.formsets import formset_factory, BaseFormSet
 from django.forms import fields
 from django.shortcuts import render_to_response
 from django.utils.decorators import method_decorator
@@ -91,6 +92,12 @@
     value = fields.IntegerField()
     single = fields.ChoiceField(choices=TestChoices)
     multi = fields.MultipleChoiceField(choices=TestChoices)
+    def clean(self):
+        cleaned_data = self.cleaned_data
+        if cleaned_data.get("text") == "Raise non-field error":
+            raise ValidationError(u"Non-field error.")
+        # Always return the full collection of cleaned data.
+        return cleaned_data
 
 def form_view(request):
     "A view that tests a simple form"
@@ -127,6 +134,43 @@
         }
     )
 
+class BaseTestFormSet(BaseFormSet):
+    def clean(self):
+        """Checks that no two E-mail addresses are the same."""
+        if any(self.errors):
+               # Don't bother validating the formset unless each form is valid
+               # (form.cleaned_data will not exist)
+               return
+        emails = []
+        for i in range(0, self.total_form_count()):
+            form = self.forms[i]
+
+            email = form.cleaned_data['email']
+            if email in emails:
+                raise ValidationError, \
+                      "Forms in a set must have distinct E-mail addresses."
+            emails.append(email)
+
+TestFormSet = formset_factory(TestForm, BaseTestFormSet)
+
+def formset_view(request):
+    "A view that tests a simple formset"
+    if request.method == 'POST':
+        formset = TestFormSet(request.POST)
+        if formset.is_valid():
+            t = Template('Valid POST data.', name='Valid POST Template')
+            c = Context()
+        else:
+            t = Template('Invalid POST data. {{ my_formset.errors }}', 
+                         name='Invalid POST Template')
+            c = Context({'my_formset': formset})
+    else:
+        formset = TestForm(request.GET)
+        t = Template('Viewing base formset. {{ my_formset }}.', 
+                     name='Formset GET Template')
+        c = Context({'my_formset': formset})
+    return HttpResponse(t.render(c))
+
 def login_protected_view(request):
     "A simple view that is login protected."
     t = Template('This is a login protected test. Username is {{ user.username }}.', name='Login Template')
Index: tests/modeltests/test_client/urls.py
===================================================================
--- tests/modeltests/test_client/urls.py	(revision 16452)
+++ tests/modeltests/test_client/urls.py	(working copy)
@@ -17,6 +17,7 @@
     (r'^bad_view/$', views.bad_view),
     (r'^form_view/$', views.form_view),
     (r'^form_view_with_template/$', views.form_view_with_template),
+    (r'^formset_view/$', views.formset_view),
     (r'^login_protected_view/$', views.login_protected_view),
     (r'^login_protected_method_view/$', views.login_protected_method_view),
     (r'^login_protected_view_custom_redirect/$', views.login_protected_view_changed_redirect),
Index: tests/regressiontests/test_client_regress/models.py
===================================================================
--- tests/regressiontests/test_client_regress/models.py	(revision 16452)
+++ tests/regressiontests/test_client_regress/models.py	(working copy)
@@ -2,6 +2,7 @@
 """
 Regression tests for the Test Client, especially the customized assertions.
 """
+from __future__ import with_statement
 import os
 import warnings
 
@@ -478,6 +479,201 @@
         except AssertionError, e:
             self.assertIn("abc: The form 'form' in context 0 does not contain the non-field error 'Some error.' (actual errors: )", str(e))
 
+class AssertFormsetErrorTests(TestCase):
+    msg_prefixes = [("", {}), ("abc: ", {"msg_prefix": "abc"})]
+    def setUp(self):
+        """Makes response object for testing field and non-field errors"""
+        #For testing field and non-field errors
+        self.response_form_errors = self.getResponse(
+           {'form-TOTAL_FORMS': u'2',
+            'form-INITIAL_FORMS': u'2',
+            'form-0-text': 'Raise non-field error',
+            'form-0-email': 'not an email address',
+            'form-0-value': 37,
+            'form-0-single': 'b',
+            'form-0-multi': ('b','c','e'),
+            'form-1-text': 'Hello World',
+            'form-1-email': 'email@domain.com',
+            'form-1-value': 37,
+            'form-1-single': 'b',
+            'form-1-multi': ('b','c','e'),
+        })
+        #For testing non-form errors
+        self.response_nonform_errors = self.getResponse(
+           {'form-TOTAL_FORMS': u'2',
+            'form-INITIAL_FORMS': u'2',
+            'form-0-text': 'Hello World',
+            'form-0-email': 'email@domain.com',
+            'form-0-value': 37,
+            'form-0-single': 'b',
+            'form-0-multi': ('b','c','e'),
+            'form-1-text': 'Hello World',
+            'form-1-email': 'email@domain.com',
+            'form-1-value': 37,
+            'form-1-single': 'b',
+            'form-1-multi': ('b','c','e'),
+        })
+
+    def getResponse(self, post_data):
+        response = self.client.post('/test_client/formset_view/',
+                                    post_data)
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, "Invalid POST Template")
+        return response
+
+    def test_unknown_formset(self):
+        "An assertion is raised if the formset name is unknown"
+        for prefix, kwargs in self.msg_prefixes:
+            with self.assertRaises(AssertionError) as cm:
+                self.assertFormsetError(self.response_form_errors,
+                                        'wrong_formset',
+                                        0,
+                                        'Some_field',
+                                        'Some error.',
+                                        **kwargs)            
+            self.assertEqual(str(cm.exception),
+                             prefix + "The formset 'wrong_formset' was not "
+                             "used to render the response")
+
+    def test_unknown_field(self):
+        "An assertion is raised if the field name is unknown"
+        for prefix, kwargs in self.msg_prefixes:
+            with self.assertRaises(AssertionError) as cm:
+                self.assertFormsetError(self.response_form_errors,
+                                        'my_formset',
+                                        0,
+                                        'Some_field',
+                                        'Some error.',
+                                        **kwargs)
+            self.assertEqual(str(cm.exception), 
+                             prefix + "The formset 'my_formset', "
+                             "form 0 in context 0 "
+                             "does not contain the field 'Some_field'")
+
+    def test_noerror_field(self):
+        "An assertion is raised if the field doesn't have any errors"
+        for prefix, kwargs in self.msg_prefixes:
+            with self.assertRaises(AssertionError) as cm:
+                self.assertFormsetError(self.response_form_errors,
+                                        'my_formset',
+                                        1,
+                                        'value',
+                                        'Some error.',
+                                        **kwargs)
+            self.assertEqual(str(cm.exception),
+                             prefix + "The field 'value' "
+                             "on formset 'my_formset', form 1 "
+                             "in context 0 contains no errors")
+
+    def test_unknown_error(self):
+        "An assertion is raised if the field doesn't contain the specified error"
+        for prefix, kwargs in self.msg_prefixes:
+            with self.assertRaises(AssertionError) as cm:
+                self.assertFormsetError(self.response_form_errors,
+                                        'my_formset',
+                                        0,
+                                        'email',
+                                        'Some error.',
+                                        **kwargs)
+            self.assertEqual(str(cm.exception),
+                             prefix + "The field 'email' "
+                             "on formset 'my_formset', form 0 "
+                             "in context 0 does not contain the error "
+                             "'Some error.' (actual errors: "
+                             "[u'Enter a valid e-mail address.'])")
+
+    def test_field_error(self):
+        "No assertion is raised if the field contains the provided error"
+        for prefix, kwargs in self.msg_prefixes:
+            self.assertFormsetError(self.response_form_errors,
+                                'my_formset',
+                                0,
+                                'email',
+                                'Enter a valid e-mail address.',
+                                **kwargs)
+
+    def test_no_nonfield_error(self):
+        "An assertion is raised if the formsets non-field errors doesn't contain any errors."
+        for prefix, kwargs in self.msg_prefixes:
+            with self.assertRaises(AssertionError) as cm:
+                self.assertFormsetError(self.response_form_errors,
+                                        'my_formset',
+                                        1,
+                                        None,
+                                        'Some error.',
+                                        **kwargs)
+            self.assertEqual(str(cm.exception),
+                             prefix + "The formset 'my_formset', form 1 in "
+                             "context 0 does not contain any non-field errors."
+                             )
+
+    def test_unknown_nonfield_error(self):
+        "An assertion is raised if the formsets non-field errors doesn't contain the provided error."
+        for prefix, kwargs in self.msg_prefixes:
+            with self.assertRaises(AssertionError) as cm:
+                self.assertFormsetError(self.response_form_errors,
+                                        'my_formset',
+                                        0,
+                                        None,
+                                        'Some error.',
+                                        **kwargs)
+            self.assertEqual(str(cm.exception),
+                             prefix + "The formset 'my_formset', form 0 in "
+                             "context 0 does not contain the non-field error "
+                             "'Some error.' (actual errors: "
+                             "[u'Non-field error.'])")
+
+    def test_nonfield_error(self):
+        "No assertion is raised if the formsets non-field errors contains the provided error."
+        for prefix, kwargs in self.msg_prefixes:
+            self.assertFormsetError(self.response_form_errors,
+                                'my_formset',
+                                0,
+                                None,
+                                'Non-field error.',
+                                **kwargs)
+
+    def test_no_nonform_error(self):
+        "An assertion is raised if the formsets non-form errors doesn't contain any errors."
+        for prefix, kwargs in self.msg_prefixes:
+            with self.assertRaises(AssertionError) as cm:
+                self.assertFormsetError(self.response_form_errors,
+                                        'my_formset',
+                                        None,
+                                        None,
+                                        'Some error.',
+                                        **kwargs)
+            self.assertEqual(str(cm.exception), prefix + 
+                             "The formset 'my_formset' in context 0 "
+                             "does not contain any non-form errors.")
+
+    def test_unknown_nonform_error(self):
+        "An assertion is raised if the formsets non-form errors doesn't contain the provided error."
+        for prefix, kwargs in self.msg_prefixes:
+            with self.assertRaises(AssertionError) as cm:
+                self.assertFormsetError(self.response_nonform_errors,
+                                        'my_formset',
+                                        None,
+                                        None,
+                                        'Some error.',
+                                        **kwargs)
+            self.assertEqual(str(cm.exception), prefix + 
+                             "The formset 'my_formset' in context 0 does not "
+                             "contain the non-form error 'Some error.' "
+                             "(actual errors: [u'Forms in a set must have "
+                             "distinct E-mail addresses.'])")
+
+    def test_nonform_error(self):
+        "No assertion is raised if the formsets non-form errors contains the provided error."
+        for prefix, kwargs in self.msg_prefixes:
+            self.assertFormsetError(self.response_nonform_errors,
+                                    'my_formset',
+                                    None,
+                                    None,
+                                    'Forms in a set must have distinct E-mail '
+                                    'addresses.',
+                                    **kwargs)
+
 class LoginTests(TestCase):
     fixtures = ['testdata']
 
