Browse Source

Fixed #33348 -- Changed SimpleTestCase.assertFormError()/assertFormsetErrors() to take form/formset.

Instead of taking a response object and a context name for
the form/formset, the two methods now take the object directly.
Baptiste Mispelon 3 years ago
parent
commit
50e1e7ef8e

+ 1 - 2
django/contrib/admin/helpers.py

@@ -441,9 +441,8 @@ class InlineAdminFormSet:
     def forms(self):
         return self.formset.forms
 
-    @property
     def non_form_errors(self):
-        return self.formset.non_form_errors
+        return self.formset.non_form_errors()
 
     @property
     def is_bound(self):

+ 169 - 90
django/test/testcases.py

@@ -1,5 +1,6 @@
 import asyncio
 import difflib
+import inspect
 import json
 import logging
 import posixpath
@@ -42,6 +43,7 @@ from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
 from django.forms.fields import CharField
 from django.http import QueryDict
 from django.http.request import split_domain_port, validate_host
+from django.http.response import HttpResponseBase
 from django.test.client import AsyncClient, Client
 from django.test.html import HTMLParseError, parse_html
 from django.test.signals import template_rendered
@@ -165,6 +167,127 @@ class _DatabaseFailure:
         raise DatabaseOperationForbidden(self.message)
 
 
+# RemovedInDjango50Warning
+class _AssertFormErrorDeprecationHelper:
+    @staticmethod
+    def assertFormError(self, response, form, field, errors, msg_prefix=""):
+        """
+        Search through all the rendered contexts of the `response` for a form named
+        `form` then dispatch to the new assertFormError() using that instance.
+        If multiple contexts contain the form, they're all checked in order and any
+        failure will abort (this matches the old behavior).
+        """
+        warning_msg = (
+            f"Passing response to assertFormError() is deprecated. Use the form object "
+            f"directly: assertFormError(response.context[{form!r}], {field!r}, ...)"
+        )
+        warnings.warn(warning_msg, RemovedInDjango50Warning, stacklevel=2)
+
+        full_msg_prefix = f"{msg_prefix}: " if msg_prefix else ""
+        contexts = to_list(response.context) if response.context is not None else []
+        if not contexts:
+            self.fail(
+                f"{full_msg_prefix}Response did not use any contexts to render the "
+                f"response"
+            )
+        # Search all contexts for the error.
+        found_form = False
+        for i, context in enumerate(contexts):
+            if form not in context:
+                continue
+            found_form = True
+            self.assertFormError(context[form], field, errors, msg_prefix=msg_prefix)
+        if not found_form:
+            self.fail(
+                f"{full_msg_prefix}The form '{form}' was not used to render the "
+                f"response"
+            )
+
+    @staticmethod
+    def assertFormsetError(
+        self, response, formset, form_index, field, errors, msg_prefix=""
+    ):
+        """
+        Search for a formset named "formset" in the "response" and dispatch to
+        the new assertFormsetError() using that instance. If the name is found
+        in multiple contexts they're all checked in order and any failure will
+        abort the test.
+        """
+        warning_msg = (
+            f"Passing response to assertFormsetError() is deprecated. Use the formset "
+            f"object directly: assertFormsetError(response.context[{formset!r}], "
+            f"{form_index!r}, ...)"
+        )
+        warnings.warn(warning_msg, RemovedInDjango50Warning, stacklevel=2)
+
+        full_msg_prefix = f"{msg_prefix}: " if msg_prefix else ""
+        contexts = to_list(response.context) if response.context is not None else []
+        if not contexts:
+            self.fail(
+                f"{full_msg_prefix}Response did not use any contexts to render the "
+                f"response"
+            )
+        found_formset = False
+        for i, context in enumerate(contexts):
+            if formset not in context or not hasattr(context[formset], "forms"):
+                continue
+            found_formset = True
+            self.assertFormsetError(
+                context[formset], form_index, field, errors, msg_prefix
+            )
+        if not found_formset:
+            self.fail(
+                f"{full_msg_prefix}The formset '{formset}' was not used to render the "
+                f"response"
+            )
+
+    @classmethod
+    def patch_signature(cls, new_method):
+        """
+        Replace the decorated method with a new one that inspects the passed
+        args/kwargs and dispatch to the old implementation (with deprecation
+        warning) when it detects the old signature.
+        """
+
+        @wraps(new_method)
+        def patched_method(self, *args, **kwargs):
+            old_method = getattr(cls, new_method.__name__)
+            old_signature = inspect.signature(old_method)
+            try:
+                old_bound_args = old_signature.bind(self, *args, **kwargs)
+            except TypeError:
+                # If old signature doesn't match then either:
+                # 1) new signature will match
+                # 2) or a TypeError will be raised showing the user information
+                # about the new signature.
+                return new_method(self, *args, **kwargs)
+
+            new_signature = inspect.signature(new_method)
+            try:
+                new_bound_args = new_signature.bind(self, *args, **kwargs)
+            except TypeError:
+                # Old signature matches but not the new one (because of
+                # previous try/except).
+                return old_method(self, *args, **kwargs)
+
+            # If both signatures match, decide on which method to call by
+            # inspecting the first arg (arg[0] = self).
+            assert old_bound_args.args[1] == new_bound_args.args[1]
+            if hasattr(
+                old_bound_args.args[1], "context"
+            ):  # Looks like a response object => old method.
+                return old_method(self, *args, **kwargs)
+            elif isinstance(old_bound_args.args[1], HttpResponseBase):
+                raise ValueError(
+                    f"{old_method.__name__}() is only usable on responses fetched "
+                    f"using the Django test Client."
+                )
+            else:
+                return new_method(self, *args, **kwargs)
+
+        return patched_method
+
+
 class SimpleTestCase(unittest.TestCase):
 
     # The class we'll use for the test client self.client.
@@ -585,22 +708,19 @@ class SimpleTestCase(unittest.TestCase):
 
         self.assertEqual(field_errors, errors, msg_prefix + failure_message)
 
-    def assertFormError(self, response, form, field, errors, msg_prefix=""):
-        """
-        Assert that a form used to render the response has a specific field
-        error.
+    # RemovedInDjango50Warning: When the deprecation ends, remove the
+    # decorator.
+    @_AssertFormErrorDeprecationHelper.patch_signature
+    def assertFormError(self, form, field, errors, msg_prefix=""):
         """
-        self._check_test_client_response(response, "context", "assertFormError")
-        if msg_prefix:
-            msg_prefix += ": "
+        Assert that a field named "field" on the given form object has specific
+        errors.
 
-        # Put context(s) into a list to simplify processing.
-        contexts = [] if response.context is None else to_list(response.context)
-        if not contexts:
-            self.fail(
-                msg_prefix + "Response did not use any contexts to render the response"
-            )
+        errors can be either a single error message or a list of errors
+        messages. Using errors=[] test that the field has no errors.
 
+        You can pass field=None to check the form's non-field errors.
+        """
         if errors is None:
             warnings.warn(
                 "Passing errors=None to assertFormError() is deprecated, use "
@@ -609,47 +729,25 @@ class SimpleTestCase(unittest.TestCase):
                 stacklevel=2,
             )
             errors = []
-        # Put error(s) into a list to simplify processing.
-        errors = to_list(errors)
 
-        # Search all contexts for the error.
-        found_form = False
-        for i, context in enumerate(contexts):
-            if form in context:
-                found_form = True
-                self._assert_form_error(
-                    context[form], field, errors, msg_prefix, "form %r" % context[form]
-                )
-        if not found_form:
-            self.fail(
-                msg_prefix + "The form '%s' was not used to render the response" % form
-            )
-
-    def assertFormsetError(
-        self, response, formset, form_index, field, errors, msg_prefix=""
-    ):
-        """
-        Assert that a formset used to render the response has a specific error.
-
-        For field errors, specify the ``form_index`` and the ``field``.
-        For non-field errors, specify the ``form_index`` and the ``field`` as
-        None.
-        For non-form errors, specify ``form_index`` as None and the ``field``
-        as None.
-        """
-        self._check_test_client_response(response, "context", "assertFormsetError")
-        # Add punctuation to msg_prefix
         if msg_prefix:
             msg_prefix += ": "
+        errors = to_list(errors)
+        self._assert_form_error(form, field, errors, msg_prefix, f"form {form!r}")
 
-        # Put context(s) into a list to simplify processing.
-        contexts = [] if response.context is None else to_list(response.context)
-        if not contexts:
-            self.fail(
-                msg_prefix + "Response did not use any contexts to "
-                "render the response"
-            )
+    # RemovedInDjango50Warning: When the deprecation ends, remove the
+    # decorator.
+    @_AssertFormErrorDeprecationHelper.patch_signature
+    def assertFormsetError(self, formset, form_index, field, errors, msg_prefix=""):
+        """
+        Similar to assertFormError() but for formsets.
 
+        Use form_index=None to check the formset's non-form errors (in that
+        case, you must also use field=None).
+        Otherwise use an integer to check the formset's n-th form for errors.
+
+        Other parameters are the same as assertFormError().
+        """
         if errors is None:
             warnings.warn(
                 "Passing errors=None to assertFormsetError() is deprecated, "
@@ -662,50 +760,31 @@ class SimpleTestCase(unittest.TestCase):
         if form_index is None and field is not None:
             raise ValueError("You must use field=None with form_index=None.")
 
-        # Put error(s) into a list to simplify processing.
+        if msg_prefix:
+            msg_prefix += ": "
         errors = to_list(errors)
 
-        # Search all contexts for the error.
-        found_formset = False
-        for i, context in enumerate(contexts):
-            if formset not in context or not hasattr(context[formset], "forms"):
-                continue
-            formset_repr = repr(context[formset])
-            if not context[formset].is_bound:
-                self.fail(
-                    f"{msg_prefix}The formset {formset_repr} is not bound, it will "
-                    f"never have any errors."
-                )
-            found_formset = True
-            if form_index is not None:
-                form_count = context[formset].total_form_count()
-                if form_index >= form_count:
-                    form_or_forms = "forms" if form_count > 1 else "form"
-                    self.fail(
-                        f"{msg_prefix}The formset {formset_repr} only has "
-                        f"{form_count} {form_or_forms}."
-                    )
-            if form_index is not None:
-                form_repr = f"form {form_index} of formset {formset_repr}"
-                self._assert_form_error(
-                    context[formset].forms[form_index],
-                    field,
-                    errors,
-                    msg_prefix,
-                    form_repr,
-                )
-            else:
-                failure_message = (
-                    f"{msg_prefix}The non-form errors of formset {formset_repr} don't "
-                    f"match."
-                )
-                self.assertEqual(
-                    context[formset].non_form_errors(), errors, failure_message
-                )
-        if not found_formset:
+        if not formset.is_bound:
             self.fail(
-                msg_prefix
-                + "The formset '%s' was not used to render the response" % formset
+                f"{msg_prefix}The formset {formset!r} is not bound, it will never have "
+                f"any errors."
+            )
+        if form_index is not None and form_index >= formset.total_form_count():
+            form_count = formset.total_form_count()
+            form_or_forms = "forms" if form_count > 1 else "form"
+            self.fail(
+                f"{msg_prefix}The formset {formset!r} only has {form_count} "
+                f"{form_or_forms}."
+            )
+        if form_index is not None:
+            form_repr = f"form {form_index} of formset {formset!r}"
+            self._assert_form_error(
+                formset.forms[form_index], field, errors, msg_prefix, form_repr
+            )
+        else:
+            failure_message = f"The non-form errors of formset {formset!r} don't match."
+            self.assertEqual(
+                formset.non_form_errors(), errors, msg_prefix + failure_message
             )
 
     def _get_template_used(self, response, template_name, msg_prefix, method_name):

+ 4 - 0
docs/internals/deprecation.txt

@@ -97,6 +97,10 @@ details on these changes.
 * The ``django.utils.timezone.utc`` alias to ``datetime.timezone.utc`` will be
   removed.
 
+* Passing a response object and a form/formset name to
+  ``SimpleTestCase.assertFormError()`` and ``assertFormsetError()`` will no
+  longer be allowed.
+
 .. _deprecation-removed-in-4.1:
 
 4.1

+ 16 - 0
docs/releases/4.1.txt

@@ -327,6 +327,10 @@ Tests
 * A nested atomic block marked as durable in :class:`django.test.TestCase` now
   raises a ``RuntimeError``, the same as outside of tests.
 
+* :meth:`.SimpleTestCase.assertFormError` and
+  :meth:`~.SimpleTestCase.assertFormsetError` now support passing a
+  form/formset object directly.
+
 URLs
 ~~~~
 
@@ -449,6 +453,9 @@ Miscellaneous
 
 * The admin log out UI now uses ``POST`` requests.
 
+* The undocumented ``InlineAdminFormSet.non_form_errors`` property is replaced
+  by the ``non_form_errors()`` method. This is consistent with ``BaseFormSet``.
+
 .. _deprecated-features-4.1:
 
 Features deprecated in 4.1
@@ -552,6 +559,15 @@ Miscellaneous
 * The :data:`django.utils.timezone.utc` alias to :attr:`datetime.timezone.utc`
   is deprecated. Use :attr:`datetime.timezone.utc` directly.
 
+* Passing a response object and a form/formset name to
+  ``SimpleTestCase.assertFormError()`` and ``assertFormsetError()`` is
+  deprecated. Use::
+
+    assertFormError(response.context['form_name'], …)
+    assertFormsetError(response.context['formset_name'], …)
+
+  or pass the form/formset object directly instead.
+
 Features removed in 4.1
 =======================
 

+ 45 - 27
docs/topics/testing/tools.txt

@@ -1473,47 +1473,65 @@ your test suite.
 
         self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': ['Enter a valid email address.']})
 
-.. method:: SimpleTestCase.assertFormError(response, form, field, errors, msg_prefix='')
+.. method:: SimpleTestCase.assertFormError(form, field, errors, msg_prefix='')
 
-    Asserts that a field on a form raises the provided list of errors when
-    rendered on the form.
+    Asserts that a field on a form raises the provided list of errors.
 
-    ``response`` must be a response instance returned by the
-    :class:`test client <django.test.Response>`.
+    ``form`` is a ``Form`` instance. The form must be
+    :ref:`bound <ref-forms-api-bound-unbound>` but not necessarily
+    validated (``assertFormError()`` will automatically call ``full_clean()``
+    on the form).
+
+    ``field`` is the name of the field on the form to check. To check the form's
+    :meth:`non-field errors <django.forms.Form.non_field_errors>`, use
+    ``field=None``.
+
+    ``errors`` is a list of all the error strings that the field is expected to
+    have. You can also pass a single error string if you only expect one error
+    which means that ``errors='error message'`` is the same as
+    ``errors=['error message']``.
 
-    ``form`` is the name the ``Form`` instance was given in the template
-    context of the response.
+    .. versionchanged:: 4.1
 
-    ``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
-    :meth:`form.non_field_errors() <django.forms.Form.non_field_errors>`) will
-    be checked.
+        In older versions, using an empty error list with ``assertFormError()``
+        would always pass, regardless of whether the field had any errors or
+        not. Starting from Django 4.1, using ``errors=[]`` will only pass if
+        the field actually has no errors.
 
-    ``errors`` is an error string, or a list of error strings, that are
-    expected as a result of form validation.
+        Django 4.1 also changed the behavior of ``assertFormError()`` when a
+        field has multiple errors. In older versions, if a field had multiple
+        errors and you checked for only some of them, the test would pass.
+        Starting from Django 4.1, the error list must be an exact match to the
+        field's actual errors.
 
-.. method:: SimpleTestCase.assertFormsetError(response, formset, form_index, field, errors, msg_prefix='')
+    .. deprecated:: 4.1
+
+        Support for passing a response object and a form name to
+        ``assertFormError()`` is deprecated and will be removed in Django 5.0.
+        Use the form instance directly instead.
+
+.. method:: SimpleTestCase.assertFormsetError(formset, form_index, field, errors, msg_prefix='')
 
     Asserts that the ``formset`` raises the provided list of errors when
     rendered.
 
-    ``response`` must be a response instance returned by the
-    :class:`test client <django.test.Response>`.
+    ``formset`` is a ``Formset`` instance. The formset must be bound but not
+    necessarily validated (``assertFormsetError()`` will automatically call the
+    ``full_clean()`` on the formset).
 
-    ``formset`` is the name the ``Formset`` instance was given in the template
-    context of the response.
+    ``form_index`` is the number of the form within the ``Formset`` (starting
+    from 0). Use ``form_index=None`` to check the formset's non-form errors,
+    i.e. the errors you get when calling ``formset.non_form_errors()``. In that
+    case you must also use ``field=None``.
 
-    ``form_index`` is the number of the form within the ``Formset``.  If
-    ``form_index`` has a value of ``None``, non-form errors (errors you can
-    access via ``formset.non_form_errors()``) will be checked.
+    ``field`` and ``errors`` have the same meaning as the parameters to
+    ``assertFormError()``.
 
-    ``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
-    :meth:`form.non_field_errors() <django.forms.Form.non_field_errors>`) will
-    be checked.
+    .. deprecated:: 4.1
 
-    ``errors`` is an error string, or a list of error strings, that are
-    expected as a result of form validation.
+        Support for passing a response object and a formset name to
+        ``assertFormsetError()`` is deprecated and will be removed in Django
+        5.0. Use the formset instance directly instead.
 
 .. method:: SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False)
 

+ 17 - 12
tests/admin_views/tests.py

@@ -2128,7 +2128,9 @@ class AdminViewPermissionsTest(TestCase):
         self.assertEqual(response.status_code, 302)
         login = self.client.post(login_url, self.no_username_login)
         self.assertEqual(login.status_code, 200)
-        self.assertFormError(login, "form", "username", ["This field is required."])
+        self.assertFormError(
+            login.context["form"], "username", ["This field is required."]
+        )
 
     def test_login_redirect_for_direct_get(self):
         """
@@ -6711,10 +6713,9 @@ class UserAdminTest(TestCase):
             },
         )
         self.assertEqual(response.status_code, 200)
-        self.assertFormError(response, "adminform", "password1", [])
+        self.assertFormError(response.context["adminform"], "password1", [])
         self.assertFormError(
-            response,
-            "adminform",
+            response.context["adminform"],
             "password2",
             ["The two password fields didn’t match."],
         )
@@ -7836,12 +7837,13 @@ class AdminViewOnSiteTests(TestCase):
             reverse("admin:admin_views_parentwithdependentchildren_add"), post_data
         )
         self.assertFormError(
-            response, "adminform", "some_required_info", ["This field is required."]
+            response.context["adminform"],
+            "some_required_info",
+            ["This field is required."],
         )
-        self.assertFormError(response, "adminform", None, [])
+        self.assertFormError(response.context["adminform"], None, [])
         self.assertFormsetError(
-            response,
-            "inline_admin_formset",
+            response.context["inline_admin_formset"],
             0,
             None,
             [
@@ -7849,7 +7851,9 @@ class AdminViewOnSiteTests(TestCase):
                 "contrived test case"
             ],
         )
-        self.assertFormsetError(response, "inline_admin_formset", None, None, [])
+        self.assertFormsetError(
+            response.context["inline_admin_formset"], None, None, []
+        )
 
     def test_change_view_form_and_formsets_run_validation(self):
         """
@@ -7879,11 +7883,12 @@ class AdminViewOnSiteTests(TestCase):
             post_data,
         )
         self.assertFormError(
-            response, "adminform", "some_required_info", ["This field is required."]
+            response.context["adminform"],
+            "some_required_info",
+            ["This field is required."],
         )
         self.assertFormsetError(
-            response,
-            "inline_admin_formset",
+            response.context["inline_admin_formset"],
             0,
             None,
             [

+ 14 - 10
tests/test_client/tests.py

@@ -437,10 +437,10 @@ class ClientTest(TestCase):
         response = self.client.post("/form_view/", post_data)
         self.assertContains(response, "This field is required.", 3)
         self.assertTemplateUsed(response, "Invalid POST Template")
-
-        self.assertFormError(response, "form", "email", "This field is required.")
-        self.assertFormError(response, "form", "single", "This field is required.")
-        self.assertFormError(response, "form", "multi", "This field is required.")
+        form = response.context["form"]
+        self.assertFormError(form, "email", "This field is required.")
+        self.assertFormError(form, "single", "This field is required.")
+        self.assertFormError(form, "multi", "This field is required.")
 
     def test_form_error(self):
         "POST erroneous data to a form"
@@ -455,7 +455,9 @@ class ClientTest(TestCase):
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "Invalid POST Template")
 
-        self.assertFormError(response, "form", "email", "Enter a valid email address.")
+        self.assertFormError(
+            response.context["form"], "email", "Enter a valid email address."
+        )
 
     def test_valid_form_with_template(self):
         "POST valid data to a form using multiple templates"
@@ -480,10 +482,10 @@ class ClientTest(TestCase):
         self.assertTemplateUsed(response, "form_view.html")
         self.assertTemplateUsed(response, "base.html")
         self.assertTemplateNotUsed(response, "Invalid POST Template")
-
-        self.assertFormError(response, "form", "email", "This field is required.")
-        self.assertFormError(response, "form", "single", "This field is required.")
-        self.assertFormError(response, "form", "multi", "This field is required.")
+        form = response.context["form"]
+        self.assertFormError(form, "email", "This field is required.")
+        self.assertFormError(form, "single", "This field is required.")
+        self.assertFormError(form, "multi", "This field is required.")
 
     def test_form_error_with_template(self):
         "POST erroneous data to a form using multiple templates"
@@ -500,7 +502,9 @@ class ClientTest(TestCase):
         self.assertTemplateUsed(response, "base.html")
         self.assertTemplateNotUsed(response, "Invalid POST Template")
 
-        self.assertFormError(response, "form", "email", "Enter a valid email address.")
+        self.assertFormError(
+            response.context["form"], "email", "Enter a valid email address."
+        )
 
     def test_unknown_page(self):
         "GET an invalid URL"

+ 296 - 148
tests/test_utils/tests.py

@@ -1373,6 +1373,7 @@ class TestFormset(formset_factory(TestForm)):
 
 
 class AssertFormErrorTests(SimpleTestCase):
+    @ignore_warnings(category=RemovedInDjango50Warning)
     def test_non_client_response(self):
         msg = (
             "assertFormError() is only usable on responses fetched using the "
@@ -1380,8 +1381,9 @@ class AssertFormErrorTests(SimpleTestCase):
         )
         response = HttpResponse()
         with self.assertRaisesMessage(ValueError, msg):
-            self.assertFormError(response, "formset", 0, "field", "invalid value")
+            self.assertFormError(response, "form", "field", "invalid value")
 
+    @ignore_warnings(category=RemovedInDjango50Warning)
     def test_response_with_no_context(self):
         msg = "Response did not use any contexts to render the response"
         response = mock.Mock(context=[])
@@ -1397,6 +1399,7 @@ class AssertFormErrorTests(SimpleTestCase):
                 msg_prefix=msg_prefix,
             )
 
+    @ignore_warnings(category=RemovedInDjango50Warning)
     def test_form_not_in_context(self):
         msg = "The form 'form' was not used to render the response"
         response = mock.Mock(context=[{}])
@@ -1408,18 +1411,32 @@ class AssertFormErrorTests(SimpleTestCase):
                 response, "form", "field", "invalid value", msg_prefix=msg_prefix
             )
 
+    def test_single_error(self):
+        self.assertFormError(TestForm.invalid(), "field", "invalid value")
+
+    def test_error_list(self):
+        self.assertFormError(TestForm.invalid(), "field", ["invalid value"])
+
+    def test_empty_errors_valid_form(self):
+        self.assertFormError(TestForm.valid(), "field", [])
+
+    def test_empty_errors_valid_form_non_field_errors(self):
+        self.assertFormError(TestForm.valid(), None, [])
+
     def test_field_not_in_form(self):
         msg = (
             "The form <TestForm bound=True, valid=False, fields=(field)> does not "
             "contain the field 'other_field'."
         )
-        response = mock.Mock(context=[{"form": TestForm.invalid()}])
         with self.assertRaisesMessage(AssertionError, msg):
-            self.assertFormError(response, "form", "other_field", "invalid value")
+            self.assertFormError(TestForm.invalid(), "other_field", "invalid value")
         msg_prefix = "Custom prefix"
         with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
             self.assertFormError(
-                response, "form", "other_field", "invalid value", msg_prefix=msg_prefix
+                TestForm.invalid(),
+                "other_field",
+                "invalid value",
+                msg_prefix=msg_prefix,
             )
 
     def test_field_with_no_errors(self):
@@ -1427,14 +1444,13 @@ class AssertFormErrorTests(SimpleTestCase):
             "The errors of field 'field' on form <TestForm bound=True, valid=True, "
             "fields=(field)> don't match."
         )
-        response = mock.Mock(context=[{"form": TestForm.valid()}])
         with self.assertRaisesMessage(AssertionError, msg) as ctx:
-            self.assertFormError(response, "form", "field", "invalid value")
+            self.assertFormError(TestForm.valid(), "field", "invalid value")
         self.assertIn("[] != ['invalid value']", str(ctx.exception))
         msg_prefix = "Custom prefix"
         with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
             self.assertFormError(
-                response, "form", "field", "invalid value", msg_prefix=msg_prefix
+                TestForm.valid(), "field", "invalid value", msg_prefix=msg_prefix
             )
 
     def test_field_with_different_error(self):
@@ -1442,99 +1458,62 @@ class AssertFormErrorTests(SimpleTestCase):
             "The errors of field 'field' on form <TestForm bound=True, valid=False, "
             "fields=(field)> don't match."
         )
-        response = mock.Mock(context=[{"form": TestForm.invalid()}])
         with self.assertRaisesMessage(AssertionError, msg) as ctx:
-            self.assertFormError(response, "form", "field", "other error")
+            self.assertFormError(TestForm.invalid(), "field", "other error")
         self.assertIn("['invalid value'] != ['other error']", str(ctx.exception))
         msg_prefix = "Custom prefix"
         with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
             self.assertFormError(
-                response, "form", "field", "other error", msg_prefix=msg_prefix
+                TestForm.invalid(), "field", "other error", msg_prefix=msg_prefix
             )
 
-    def test_basic_positive_assertion(self):
-        response = mock.Mock(context=[{"form": TestForm.invalid()}])
-        self.assertFormError(response, "form", "field", "invalid value")
-
-    def test_basic_positive_assertion_multicontext(self):
-        response = mock.Mock(context=[{}, {"form": TestForm.invalid()}])
-        self.assertFormError(response, "form", "field", "invalid value")
-
-    def test_empty_errors_unbound_form(self):
+    def test_unbound_form(self):
         msg = (
             "The form <TestForm bound=False, valid=Unknown, fields=(field)> is not "
             "bound, it will never have any errors."
         )
-        response = mock.Mock(context=[{"form": TestForm()}])
         with self.assertRaisesMessage(AssertionError, msg):
-            self.assertFormError(response, "form", "field", [])
+            self.assertFormError(TestForm(), "field", [])
         msg_prefix = "Custom prefix"
         with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
-            self.assertFormError(response, "form", "field", [], msg_prefix=msg_prefix)
-
-    def test_empty_errors_valid_form(self):
-        response = mock.Mock(context=[{"form": TestForm.valid()}])
-        self.assertFormError(response, "form", "field", [])
+            self.assertFormError(TestForm(), "field", [], msg_prefix=msg_prefix)
 
     def test_empty_errors_invalid_form(self):
         msg = (
             "The errors of field 'field' on form <TestForm bound=True, valid=False, "
             "fields=(field)> don't match."
         )
-        response = mock.Mock(context=[{"form": TestForm.invalid()}])
         with self.assertRaisesMessage(AssertionError, msg) as ctx:
-            self.assertFormError(response, "form", "field", [])
+            self.assertFormError(TestForm.invalid(), "field", [])
         self.assertIn("['invalid value'] != []", str(ctx.exception))
 
     def test_non_field_errors(self):
-        response = mock.Mock(context=[{"form": TestForm.invalid(nonfield=True)}])
-        self.assertFormError(response, "form", None, "non-field error")
+        self.assertFormError(TestForm.invalid(nonfield=True), None, "non-field error")
 
     def test_different_non_field_errors(self):
-        response = mock.Mock(context=[{"form": TestForm.invalid(nonfield=True)}])
         msg = (
             "The non-field errors of form <TestForm bound=True, valid=False, "
             "fields=(field)> don't match."
         )
         with self.assertRaisesMessage(AssertionError, msg) as ctx:
-            self.assertFormError(response, "form", None, "other non-field error")
+            self.assertFormError(
+                TestForm.invalid(nonfield=True), None, "other non-field error"
+            )
         self.assertIn(
             "['non-field error'] != ['other non-field error']", str(ctx.exception)
         )
         msg_prefix = "Custom prefix"
         with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
             self.assertFormError(
-                response, "form", None, "other non-field error", msg_prefix=msg_prefix
+                TestForm.invalid(nonfield=True),
+                None,
+                "other non-field error",
+                msg_prefix=msg_prefix,
             )
 
-    @ignore_warnings(category=RemovedInDjango50Warning)
-    def test_errors_none(self):
-        msg = (
-            "The errors of field 'field' on form <TestForm bound=True, valid=False, "
-            "fields=(field)> don't match."
-        )
-        response = mock.Mock(context=[{"form": TestForm.invalid()}])
-        with self.assertRaisesMessage(AssertionError, msg):
-            self.assertFormError(response, "form", "field", None)
-
-    def test_errors_none_warning(self):
-        response = mock.Mock(context=[{"form": TestForm.valid()}])
-        msg = (
-            "Passing errors=None to assertFormError() is deprecated, use "
-            "errors=[] instead."
-        )
-        with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
-            self.assertFormError(response, "form", "field", None)
-
 
 class AssertFormsetErrorTests(SimpleTestCase):
-    def _get_formset_data(self, field_value):
-        return {
-            "form-TOTAL_FORMS": "1",
-            "form-INITIAL_FORMS": "0",
-            "form-0-field": field_value,
-        }
-
+    @ignore_warnings(category=RemovedInDjango50Warning)
     def test_non_client_response(self):
         msg = (
             "assertFormsetError() is only usable on responses fetched using "
@@ -1544,12 +1523,14 @@ class AssertFormsetErrorTests(SimpleTestCase):
         with self.assertRaisesMessage(ValueError, msg):
             self.assertFormsetError(response, "formset", 0, "field", "invalid value")
 
+    @ignore_warnings(category=RemovedInDjango50Warning)
     def test_response_with_no_context(self):
         msg = "Response did not use any contexts to render the response"
         response = mock.Mock(context=[])
         with self.assertRaisesMessage(AssertionError, msg):
             self.assertFormsetError(response, "formset", 0, "field", "invalid value")
 
+    @ignore_warnings(category=RemovedInDjango50Warning)
     def test_formset_not_in_context(self):
         msg = "The formset 'formset' was not used to render the response"
         response = mock.Mock(context=[{}])
@@ -1561,25 +1542,41 @@ class AssertFormsetErrorTests(SimpleTestCase):
                 response, "formset", 0, "field", "invalid value", msg_prefix=msg_prefix
             )
 
+    def test_single_error(self):
+        self.assertFormsetError(TestFormset.invalid(), 0, "field", "invalid value")
+
+    def test_error_list(self):
+        self.assertFormsetError(TestFormset.invalid(), 0, "field", ["invalid value"])
+
+    def test_empty_errors_valid_formset(self):
+        self.assertFormsetError(TestFormset.valid(), 0, "field", [])
+
+    def test_multiple_forms(self):
+        formset = TestFormset(
+            {
+                "form-TOTAL_FORMS": "2",
+                "form-INITIAL_FORMS": "0",
+                "form-0-field": "valid",
+                "form-1-field": "invalid",
+            }
+        )
+        formset.full_clean()
+        self.assertFormsetError(formset, 0, "field", [])
+        self.assertFormsetError(formset, 1, "field", ["invalid value"])
+
     def test_field_not_in_form(self):
         msg = (
             "The form 0 of formset <TestFormset: bound=True valid=False total_forms=1> "
             "does not contain the field 'other_field'."
         )
-        response = mock.Mock(context=[{"formset": TestFormset.invalid()}])
         with self.assertRaisesMessage(AssertionError, msg):
             self.assertFormsetError(
-                response,
-                "formset",
-                0,
-                "other_field",
-                "invalid value",
+                TestFormset.invalid(), 0, "other_field", "invalid value"
             )
         msg_prefix = "Custom prefix"
         with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
             self.assertFormsetError(
-                response,
-                "formset",
+                TestFormset.invalid(),
                 0,
                 "other_field",
                 "invalid value",
@@ -1591,14 +1588,13 @@ class AssertFormsetErrorTests(SimpleTestCase):
             "The errors of field 'field' on form 0 of formset <TestFormset: bound=True "
             "valid=True total_forms=1> don't match."
         )
-        response = mock.Mock(context=[{"formset": TestFormset.valid()}])
         with self.assertRaisesMessage(AssertionError, msg) as ctx:
-            self.assertFormsetError(response, "formset", 0, "field", "invalid value")
+            self.assertFormsetError(TestFormset.valid(), 0, "field", "invalid value")
         self.assertIn("[] != ['invalid value']", str(ctx.exception))
         msg_prefix = "Custom prefix"
         with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
             self.assertFormsetError(
-                response, "formset", 0, "field", "invalid value", msg_prefix=msg_prefix
+                TestFormset.valid(), 0, "field", "invalid value", msg_prefix=msg_prefix
             )
 
     def test_field_with_different_error(self):
@@ -1606,67 +1602,45 @@ class AssertFormsetErrorTests(SimpleTestCase):
             "The errors of field 'field' on form 0 of formset <TestFormset: bound=True "
             "valid=False total_forms=1> don't match."
         )
-        response = mock.Mock(context=[{"formset": TestFormset.invalid()}])
         with self.assertRaisesMessage(AssertionError, msg) as ctx:
-            self.assertFormsetError(response, "formset", 0, "field", "other error")
+            self.assertFormsetError(TestFormset.invalid(), 0, "field", "other error")
         self.assertIn("['invalid value'] != ['other error']", str(ctx.exception))
         msg_prefix = "Custom prefix"
         with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
             self.assertFormsetError(
-                response, "formset", 0, "field", "other error", msg_prefix=msg_prefix
+                TestFormset.invalid(), 0, "field", "other error", msg_prefix=msg_prefix
             )
 
-    def test_basic_positive_assertion(self):
-        response = mock.Mock(context=[{"formset": TestFormset.invalid()}])
-        self.assertFormsetError(response, "formset", 0, "field", "invalid value")
-
-    def test_basic_positive_assertion_multicontext(self):
-        response = mock.Mock(context=[{}, {"formset": TestFormset.invalid()}])
-        self.assertFormsetError(response, "formset", 0, "field", "invalid value")
-
-    def test_empty_errors_unbound_formset(self):
+    def test_unbound_formset(self):
         msg = (
             "The formset <TestFormset: bound=False valid=Unknown total_forms=1> is not "
             "bound, it will never have any errors."
         )
-        response = mock.Mock(context=[{"formset": TestFormset()}])
         with self.assertRaisesMessage(AssertionError, msg):
-            self.assertFormsetError(response, "formset", 0, "field", [])
-
-    def test_empty_errors_valid_formset(self):
-        response = mock.Mock(context=[{}, {"formset": TestFormset.valid()}])
-        self.assertFormsetError(response, "formset", 0, "field", [])
+            self.assertFormsetError(TestFormset(), 0, "field", [])
 
     def test_empty_errors_invalid_formset(self):
         msg = (
             "The errors of field 'field' on form 0 of formset <TestFormset: bound=True "
             "valid=False total_forms=1> don't match."
         )
-        response = mock.Mock(context=[{}, {"formset": TestFormset.invalid()}])
         with self.assertRaisesMessage(AssertionError, msg) as ctx:
-            self.assertFormsetError(response, "formset", 0, "field", [])
+            self.assertFormsetError(TestFormset.invalid(), 0, "field", [])
         self.assertIn("['invalid value'] != []", str(ctx.exception))
 
     def test_non_field_errors(self):
-        response = mock.Mock(
-            context=[
-                {},
-                {"formset": TestFormset.invalid(nonfield=True)},
-            ]
+        self.assertFormsetError(
+            TestFormset.invalid(nonfield=True), 0, None, "non-field error"
         )
-        self.assertFormsetError(response, "formset", 0, None, "non-field error")
 
     def test_different_non_field_errors(self):
-        response = mock.Mock(
-            context=[{}, {"formset": TestFormset.invalid(nonfield=True)}],
-        )
         msg = (
             "The non-field errors of form 0 of formset <TestFormset: bound=True "
             "valid=False total_forms=1> don't match."
         )
         with self.assertRaisesMessage(AssertionError, msg) as ctx:
             self.assertFormsetError(
-                response, "formset", 0, None, "other non-field error"
+                TestFormset.invalid(nonfield=True), 0, None, "other non-field error"
             )
         self.assertIn(
             "['non-field error'] != ['other non-field error']", str(ctx.exception)
@@ -1674,8 +1648,7 @@ class AssertFormsetErrorTests(SimpleTestCase):
         msg_prefix = "Custom prefix"
         with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
             self.assertFormsetError(
-                response,
-                "formset",
+                TestFormset.invalid(nonfield=True),
                 0,
                 None,
                 "other non-field error",
@@ -1683,80 +1656,74 @@ class AssertFormsetErrorTests(SimpleTestCase):
             )
 
     def test_no_non_field_errors(self):
-        response = mock.Mock(context=[{}, {"formset": TestFormset.invalid()}])
         msg = (
             "The non-field errors of form 0 of formset <TestFormset: bound=True "
             "valid=False total_forms=1> don't match."
         )
         with self.assertRaisesMessage(AssertionError, msg) as ctx:
-            self.assertFormsetError(response, "formset", 0, None, "non-field error")
+            self.assertFormsetError(TestFormset.invalid(), 0, None, "non-field error")
         self.assertIn("[] != ['non-field error']", str(ctx.exception))
         msg_prefix = "Custom prefix"
         with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
             self.assertFormsetError(
-                response, "formset", 0, None, "non-field error", msg_prefix=msg_prefix
+                TestFormset.invalid(), 0, None, "non-field error", msg_prefix=msg_prefix
             )
 
     def test_non_form_errors(self):
-        response = mock.Mock(
-            context=[
-                {},
-                {"formset": TestFormset.invalid(nonform=True)},
-            ]
-        )
-        self.assertFormsetError(response, "formset", None, None, "error")
+        self.assertFormsetError(TestFormset.invalid(nonform=True), None, None, "error")
 
     def test_different_non_form_errors(self):
-        response = mock.Mock(
-            context=[{}, {"formset": TestFormset.invalid(nonform=True)}],
-        )
         msg = (
             "The non-form errors of formset <TestFormset: bound=True valid=False "
             "total_forms=0> don't match."
         )
         with self.assertRaisesMessage(AssertionError, msg) as ctx:
-            self.assertFormsetError(response, "formset", None, None, "other error")
+            self.assertFormsetError(
+                TestFormset.invalid(nonform=True), None, None, "other error"
+            )
         self.assertIn("['error'] != ['other error']", str(ctx.exception))
         msg_prefix = "Custom prefix"
         with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
             self.assertFormsetError(
-                response, "formset", None, None, "other error", msg_prefix=msg_prefix
+                TestFormset.invalid(nonform=True),
+                None,
+                None,
+                "other error",
+                msg_prefix=msg_prefix,
             )
 
     def test_no_non_form_errors(self):
-        response = mock.Mock(context=[{}, {"formset": TestFormset.invalid()}])
         msg = (
             "The non-form errors of formset <TestFormset: bound=True valid=False "
             "total_forms=1> don't match."
         )
         with self.assertRaisesMessage(AssertionError, msg) as ctx:
-            self.assertFormsetError(response, "formset", None, None, "error")
+            self.assertFormsetError(TestFormset.invalid(), None, None, "error")
         self.assertIn("[] != ['error']", str(ctx.exception))
         msg_prefix = "Custom prefix"
         with self.assertRaisesMessage(AssertionError, f"{msg_prefix}: {msg}"):
             self.assertFormsetError(
-                response, "formset", None, None, "error", msg_prefix=msg_prefix
+                TestFormset.invalid(),
+                None,
+                None,
+                "error",
+                msg_prefix=msg_prefix,
             )
 
     def test_non_form_errors_with_field(self):
-        response = mock.Mock(
-            context=[
-                {},
-                {"formset": TestFormset.invalid(nonform=True)},
-            ]
-        )
         msg = "You must use field=None with form_index=None."
         with self.assertRaisesMessage(ValueError, msg):
-            self.assertFormsetError(response, "formset", None, "field", "error")
+            self.assertFormsetError(
+                TestFormset.invalid(nonform=True), None, "field", "error"
+            )
 
     def test_form_index_too_big(self):
         msg = (
             "The formset <TestFormset: bound=True valid=False total_forms=1> only has "
             "1 form."
         )
-        response = mock.Mock(context=[{}, {"formset": TestFormset.invalid()}])
         with self.assertRaisesMessage(AssertionError, msg):
-            self.assertFormsetError(response, "formset", 2, "field", "error")
+            self.assertFormsetError(TestFormset.invalid(), 2, "field", "error")
 
     def test_form_index_too_big_plural(self):
         formset = TestFormset(
@@ -1772,40 +1739,221 @@ class AssertFormsetErrorTests(SimpleTestCase):
             "The formset <TestFormset: bound=True valid=True total_forms=2> only has 2 "
             "forms."
         )
-        response = mock.Mock(context=[{}, {"formset": formset}])
         with self.assertRaisesMessage(AssertionError, msg):
-            self.assertFormsetError(response, "formset", 2, "field", "error")
-
-    def test_formset_named_form(self):
-        formset = TestFormset.invalid()
-        # The mocked context emulates the template-based rendering of the
-        # formset.
-        response = mock.Mock(
-            context=[
-                {"form": formset},
-                {"form": formset.management_form},
-            ]
+            self.assertFormsetError(formset, 2, "field", "error")
+
+
+# RemovedInDjango50Warning
+class AssertFormErrorDeprecationTests(SimpleTestCase):
+    """
+    Exhaustively test all possible combinations of args/kwargs for the old
+    signature.
+    """
+
+    @ignore_warnings(category=RemovedInDjango50Warning)
+    def test_assert_form_error_errors_none(self):
+        msg = (
+            "The errors of field 'field' on form <TestForm bound=True, valid=False, "
+            "fields=(field)> don't match."
+        )
+        with self.assertRaisesMessage(AssertionError, msg):
+            self.assertFormError(TestForm.invalid(), "field", None)
+
+    def test_assert_form_error_errors_none_warning(self):
+        msg = (
+            "Passing errors=None to assertFormError() is deprecated, use "
+            "errors=[] instead."
+        )
+        with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
+            self.assertFormError(TestForm.valid(), "field", None)
+
+    def _assert_form_error_old_api_cases(self, form, field, errors, msg_prefix):
+        response = mock.Mock(context=[{"form": TestForm.invalid()}])
+        return (
+            ((response, form, field, errors), {}),
+            ((response, form, field, errors, msg_prefix), {}),
+            ((response, form, field, errors), {"msg_prefix": msg_prefix}),
+            ((response, form, field), {"errors": errors}),
+            ((response, form, field), {"errors": errors, "msg_prefix": msg_prefix}),
+            ((response, form), {"field": field, "errors": errors}),
+            (
+                (response, form),
+                {"field": field, "errors": errors, "msg_prefix": msg_prefix},
+            ),
+            ((response,), {"form": form, "field": field, "errors": errors}),
+            (
+                (response,),
+                {
+                    "form": form,
+                    "field": field,
+                    "errors": errors,
+                    "msg_prefix": msg_prefix,
+                },
+            ),
+            (
+                (),
+                {"response": response, "form": form, "field": field, "errors": errors},
+            ),
+            (
+                (),
+                {
+                    "response": response,
+                    "form": form,
+                    "field": field,
+                    "errors": errors,
+                    "msg_prefix": msg_prefix,
+                },
+            ),
+        )
+
+    def test_assert_form_error_old_api(self):
+        deprecation_msg = (
+            "Passing response to assertFormError() is deprecated. Use the form object "
+            "directly: assertFormError(response.context['form'], 'field', ...)"
         )
-        self.assertFormsetError(response, "form", 0, "field", "invalid value")
+        for args, kwargs in self._assert_form_error_old_api_cases(
+            form="form",
+            field="field",
+            errors=["invalid value"],
+            msg_prefix="Custom prefix",
+        ):
+            with self.subTest(args=args, kwargs=kwargs):
+                with self.assertWarnsMessage(RemovedInDjango50Warning, deprecation_msg):
+                    self.assertFormError(*args, **kwargs)
+
+    @ignore_warnings(category=RemovedInDjango50Warning)
+    def test_assert_form_error_old_api_assertion_error(self):
+        for args, kwargs in self._assert_form_error_old_api_cases(
+            form="form",
+            field="field",
+            errors=["other error"],
+            msg_prefix="Custom prefix",
+        ):
+            with self.subTest(args=args, kwargs=kwargs):
+                with self.assertRaises(AssertionError):
+                    self.assertFormError(*args, **kwargs)
 
     @ignore_warnings(category=RemovedInDjango50Warning)
-    def test_errors_none(self):
+    def test_assert_formset_error_errors_none(self):
         msg = (
             "The errors of field 'field' on form 0 of formset <TestFormset: bound=True "
             "valid=False total_forms=1> don't match."
         )
-        response = mock.Mock(context=[{"formset": TestFormset.invalid()}])
         with self.assertRaisesMessage(AssertionError, msg):
-            self.assertFormsetError(response, "formset", 0, "field", None)
+            self.assertFormsetError(TestFormset.invalid(), 0, "field", None)
 
-    def test_errors_none_warning(self):
-        response = mock.Mock(context=[{"formset": TestFormset.valid()}])
+    def test_assert_formset_error_errors_none_warning(self):
         msg = (
             "Passing errors=None to assertFormsetError() is deprecated, use "
             "errors=[] instead."
         )
         with self.assertWarnsMessage(RemovedInDjango50Warning, msg):
-            self.assertFormsetError(response, "formset", 0, "field", None)
+            self.assertFormsetError(TestFormset.valid(), 0, "field", None)
+
+    def _assert_formset_error_old_api_cases(
+        self, formset, form_index, field, errors, msg_prefix
+    ):
+        response = mock.Mock(context=[{"formset": TestFormset.invalid()}])
+        return (
+            ((response, formset, form_index, field, errors), {}),
+            ((response, formset, form_index, field, errors, msg_prefix), {}),
+            (
+                (response, formset, form_index, field, errors),
+                {"msg_prefix": msg_prefix},
+            ),
+            ((response, formset, form_index, field), {"errors": errors}),
+            (
+                (response, formset, form_index, field),
+                {"errors": errors, "msg_prefix": msg_prefix},
+            ),
+            ((response, formset, form_index), {"field": field, "errors": errors}),
+            (
+                (response, formset, form_index),
+                {"field": field, "errors": errors, "msg_prefix": msg_prefix},
+            ),
+            (
+                (response, formset),
+                {"form_index": form_index, "field": field, "errors": errors},
+            ),
+            (
+                (response, formset),
+                {
+                    "form_index": form_index,
+                    "field": field,
+                    "errors": errors,
+                    "msg_prefix": msg_prefix,
+                },
+            ),
+            (
+                (response,),
+                {
+                    "formset": formset,
+                    "form_index": form_index,
+                    "field": field,
+                    "errors": errors,
+                },
+            ),
+            (
+                (response,),
+                {
+                    "formset": formset,
+                    "form_index": form_index,
+                    "field": field,
+                    "errors": errors,
+                    "msg_prefix": msg_prefix,
+                },
+            ),
+            (
+                (),
+                {
+                    "response": response,
+                    "formset": formset,
+                    "form_index": form_index,
+                    "field": field,
+                    "errors": errors,
+                },
+            ),
+            (
+                (),
+                {
+                    "response": response,
+                    "formset": formset,
+                    "form_index": form_index,
+                    "field": field,
+                    "errors": errors,
+                    "msg_prefix": msg_prefix,
+                },
+            ),
+        )
+
+    def test_assert_formset_error_old_api(self):
+        deprecation_msg = (
+            "Passing response to assertFormsetError() is deprecated. Use the formset "
+            "object directly: assertFormsetError(response.context['formset'], 0, ...)"
+        )
+        for args, kwargs in self._assert_formset_error_old_api_cases(
+            formset="formset",
+            form_index=0,
+            field="field",
+            errors=["invalid value"],
+            msg_prefix="Custom prefix",
+        ):
+            with self.subTest(args=args, kwargs=kwargs):
+                with self.assertWarnsMessage(RemovedInDjango50Warning, deprecation_msg):
+                    self.assertFormsetError(*args, **kwargs)
+
+    @ignore_warnings(category=RemovedInDjango50Warning)
+    def test_assert_formset_error_old_api_assertion_error(self):
+        for args, kwargs in self._assert_formset_error_old_api_cases(
+            formset="formset",
+            form_index=0,
+            field="field",
+            errors=["other error"],
+            msg_prefix="Custom prefix",
+        ):
+            with self.subTest(args=args, kwargs=kwargs):
+                with self.assertRaises(AssertionError):
+                    self.assertFormsetError(*args, **kwargs)
 
 
 class FirstUrls: