mchfrnc
mchfrnc

Reputation: 5753

How to indicate which model's field raised a ValidationError?

Let's say I have a simple Django model:

class Transaction(models.Model):    
    description = models.CharField('description', max_length=150,
                                   validators=[MinLengthValidator(2, 'Description\'s min length is 2'), ])
    amount = models.DecimalField('amount', max_digits=10, decimal_places=2, 
                                 validators=[MinValueValidator(1, 'Min value is 1'), ])
    user = models.ForeignKey(User)

    # to trigger model fields' validation
    def clean(self, *args, **kwargs):
        super(Transaction, self).clean(*args, **kwargs)

    def save(self, *args, **kwargs):
        self.full_clean()
        super(Transaction, self).save(*args, **kwargs)

And I'd like to have a unit test, which precisely checks whether ValidationError is raised by description field and not by amount field (or any other).

So I have this piece of test, which in a primitive way checks if description field is present in e.exception:

def test_model_requires_description_min_2_characters(self):
    with self.assertRaises(ValidationError) as e:
        Transaction.objects.create(description='a', amount="50", user=self.user1)
    err_dict = eval(str(e.exception))
    self.assertIn('description', err_dict.keys())

But I don't really like to use eval() and I believe there is more elegant way to indicate the source of ValidationError. How can I do this?

EDIT: my model class also includes overriden clean() and save() methods, so validators are running fine.

Upvotes: 1

Views: 127

Answers (2)

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 477210

Not all ValidationError objects have an error_dict. We can derive that from the implementation of the constructor of the ValidationError [GitHub]. It depends on whether the message (the first parameter of the construct) is a dictionary.

What we can do however is use getattr(..) [Python-doc] for this, with a fallback value, like:

def test_model_requires_description_min_2_characters(self):
    with self.assertRaises(ValidationError) as e:
        Transaction.objects.create(description='a', amount="50", user=self.user1)
    self.assertIn('description', getattr(e.exception, 'err_dict', {}))

So given the error_dict does not exists, we will let getattr(..) return an empty dictionary, and hence the assertIn fails.

We can also implement a utility function for this, like:

_singleton = object()

class SomeTestCase(TestCase):

    def assertKeyInErrorDict(self, key, error):
        error_dict = getattr(error, 'err_dict', _singleton)
        if error_dict is _singleton:
            self.fail('The error {} has no error_dict'.format(error))
        else:
            self.assertIn(key, error_dict)

    def test_model_requires_description_min_2_characters(self):
        with self.assertRaises(ValidationError) as e:
            Transaction.objects.create(description='a', amount="50", user=self.user1)
        self.assertKeyInErrorDict('description', e.exception)

You can thus ass such assertKeyInErrorDict in a utility class that provides extra assert functions, and then use it in all subclasses, removing a lot of boilerplate code.

Upvotes: 0

Iain Shelvington
Iain Shelvington

Reputation: 32284

I would do something like this, ValidationError has an attribute error_dict that we can already use to test for this

def test_model_requires_description_min_2_characters(self):
    try:
        Transaction.objects.create(description='a', amount="50", user=self.user1)
    except ValidationError as e:
        # A ValidationError was raised, now we test to see if our field is in it
        self.assertIn('description', e.error_dict.keys())
    else:
        # No exception was raised, raise our own exception
        raise Exception('The test failed')

Upvotes: 2

Related Questions