I have a model and I'm trying to test validation without invoking the database layer. Rather than describe with words I'll just post up some example code. The issue here is the ForeignKey relationship to Bar, which isn't relevant to what I'm trying to test but is stopping me from running the test that I want.
First, myapp/models.py
:
from django.core.exceptions import ValidationError
from django.db import models
class BadFooError(ValidationError):
pass
class Bar(models.Model):
description = models.CharField(max_length=20)
class Foo(models.Model):
bar = models.ForeignKey(Bar)
a_value = models.IntegerField()
b_value = models.BooleanField()
def clean(self):
super(Foo, self).clean()
if self.b_value and self.a_value > 50:
raise BadFooError("No good")
Next, myapp/tests.py
:
from unittest import TestCase
from mock import MagicMock
from . import models
class SimpleTest(TestCase):
def test_avalue_bvalue_validation(self):
foo = models.Foo()
foo.a_value = 30
foo.b_value = True
foo.bar = MagicMock(spec=models.Bar)
self.assertRaises(models.BadFooError, foo.full_clean)
def test_method_2(self):
foo = models.Foo()
foo.a_value = 30
foo.b_value = True
foo.bar = MagicMock()
foo.__class__ = models.Bar
self.assertRaises(models.BadFooError, foo.full_clean)
def test_method_3(self):
foo = models.Foo()
foo.a_value = 30
foo.b_value = True
# ignore it and it will go away ...??
self.assertRaises(models.BadFooError, foo.full_clean)
Lastly, the output of python manage.py test myapp
EEE
======================================================================
ERROR: test_avalue_bvalue_validation (myapp.tests.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "~/sandbox/myapp/tests.py", line 14, in test_avalue_bvalue_validation
foo.bar = MagicMock(spec=models.Bar)
File "~/dsbx/local/lib/python2.7/site-packages/django/db/models/fields/related.py", line 408, in __set__
instance._state.db = router.db_for_write(instance.__class__, instance=value)
File "~/dsbx/local/lib/python2.7/site-packages/django/db/utils.py", line 142, in _route_db
return hints['instance']._state.db or DEFAULT_DB_ALIAS
File "~/dsbx/local/lib/python2.7/site-packages/mock.py", line 658, in __getattr__
raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute '_state'
======================================================================
ERROR: test_method_2 (myapp.tests.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "~/sandbox/myapp/tests.py", line 21, in test_method_2
foo.bar = MagicMock()
File "~/dsbx/local/lib/python2.7/site-packages/django/db/models/fields/related.py", line 405, in __set__
self.field.name, self.field.rel.to._meta.object_name))
ValueError: Cannot assign "<MagicMock id='31914832'>": "Foo.bar" must be a "Bar" instance.
======================================================================
ERROR: test_method_3 (myapp.tests.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "~/sandbox/myapp/tests.py", line 29, in test_method_3
self.assertRaises(models.BadFooError, foo.full_clean)
File "/usr/lib/python2.7/unittest/case.py", line 471, in assertRaises
callableObj(*args, **kwargs)
File "~/dsbx/local/lib/python2.7/site-packages/django/db/models/base.py", line 926, in full_clean
raise ValidationError(errors)
ValidationError: {'bar': [u'This field cannot be null.']}
----------------------------------------------------------------------
Ran 3 tests in 0.003s
FAILED (errors=3)
Creating test database for alias 'default'...
Destroying test database for alias 'default'...
So my question is... wat do?
In my unit tests, I simply assign _state
to a new Mock instance, as in this small change to your first example unit test:
def test_avalue_bvalue_validation(self):
foo = models.Foo()
foo.a_value = 30
foo.b_value = True
bar = Mock(spec=models.Bar)
bar._state = Mock()
foo.bar = bar
self.assertRaises(models.BadFooError, foo.full_clean)
However, to test your validation as a black box, I would pull the validation code into a separate method on your model which I would call from inside the clean()
method. You can then unit test this validation code specifically. You will still need to do the _stage = Mock()
assignment so you can create your instance of Foo, but at least you will be minimising the calls into Django.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With