elias Published Jan. 25, 2023 · 2 min read

Testing validation errors in Django

A decorative hero image for this page.

At Negative Epsilon we write a lot of tests and most of our projects are translated into at least another language using Django's excellent internationalization system. However, this means that there are some issues while writing tests that check for specific validation errors (raised by forms and models) using the error message (with assertRaisesRegex or assertRaisesMessage). We wrote a very simple assertion to use the code field instead (which is not supposed to be translated).

django tests validation

Let's say you are writing the clean method for a Django form and need to raise a ValidationError that will be displayed to the user.

raise ValidationError('A helpful error message')

You also want to test that this is working alright so you end up writing a test that contains this assertion:

with self.assertRaisesMessage(ValidationError, 'A helpful error message'):

That looks fine, but if the message is updated later on the test will fail. This is especially a problem when using translations, in which the message would be gettext('A helpful error message') instead of just the raw message. Depending on the context, the test might use the original version or a translation of the error message, which effectively means your translators could break your tests.

We use translation for most of our projects at Negative Epsilon and what we normally do is use a custom assertion that lets us check the code field of a ValidationError instead of the message or string representation. It looks like this:

from contextlib import contextmanager
from django.test import SimpleTestCase
from django.core.exceptions import ValidationError

class ValidationAssertionsMixin(SimpleTestCase):

    def assertRaisesCode(self, code: str):
        with self.assertRaises(ValidationError) as cm:
            yield cm

        self.assertEqual(code, cm.exception.code)

Unlike Django's own assertRaisesMessage or assertRaisesRegex, this assertion only works as a context manager (i.e. with the with construct), although making it work with a callable would be pretty straightforward (check how assertRaisesMessage is implemented). We only ever use these assertions as context managers so it is not a big deal for us.

Then you just have to define a code for any ValidationError you want to test:

raise ValidationError(_('Username must be shorter than 20 characters'), code='username_too_long')

And check for it in your tests (remember to inherit from the mixin we just wrote):

class TestSignUpForm(ValidationAssertionsMixin, TestCase):
    def test_username_length_is_validated(self):
      # ...
      with self.assertRaisesCode('username_too_long'):

Cover image by Chris Ried on Unsplash.

Ready to bring your vision to life?

Get in touch