Testing validation errors in Django
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'):
form.is_valid()
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):
@contextmanager
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'):
form.is_valid()
Cover image by Chris Ried on Unsplash.