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
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.