I've been trying to write unit test for my ModelForm, that has a ModelChoiceField. I'm creating the Form instance with mock data.
Here's my model:
# models.py
class Menu(models.Model):
dish = models.ForeignKey(Dish, default=None)
price = models.DecimalField(max_digits=7, decimal_places=2)
# forms.py
class MenuForm(forms.ModelForm):
class Meta:
model = Menu
fields = ('dish', 'price',)
def clean(self):
cleaned_data = super(MenuForm, self).clean()
price = cleaned_data.get('price', None)
dish = cleaned_data.get('dish', None)
# Some validation below
if price < 70:
self.add_error('price', 'Min price threshold')
return cleaned_data
Here's my test case:
class MenuFormTest(TestCase):
def test_price_threshold(self):
mock_dish = mock.Mock(spec=Dish)
form_data = {
'dish': mock_dish,
'price': 80,
}
form = forms.MenuForm(data=form_data)
self.assertTrue(form.is_valid())
This fails with the following error:
<ul class="errorlist"><li>dish<ul class="errorlist"><li>Select a valid choice. That choice is not one of the available choices.</li></ul></li></ul>
How to make that avoid throw that error. form.is_valid()
should have been True
there. Is there a way I can patch the ModelChoiceField's
queryset
? I tried to patch form's dish
field clean()
method like below:
form = forms.MenuForm(data=form_data)
dish_clean_patcher = mock.patch.object(form.fields['dish'], 'clean')
dish_clean_patch = dish_clean_patcher.start()
dish_clean_patch.return_value = mock_dish
self.assertTrue(form.is_valid())
Then it looks like, it fails while saving form data to model instance in _post_clean()
method. Here's the Traceback:
Traceback (most recent call last):
File "/home/vagrant/venv/local/lib/python2.7/site-packages/mock/mock.py", line 1305, in patched
return func(*args, **keywargs)
File "/vagrant/myapp/tests/test_forms.py", line 51, in test_price_threshold
self.assertFalse(form.is_valid())
File "/home/vagrant/venv/local/lib/python2.7/site-packages/django/forms/forms.py", line 185, in is_valid
return self.is_bound and not self.errors
File "/home/vagrant/venv/local/lib/python2.7/site-packages/django/forms/forms.py", line 177, in errors
self.full_clean()
File "/home/vagrant/venv/local/lib/python2.7/site-packages/django/forms/forms.py", line 396, in full_clean
self._post_clean()
File "/home/vagrant/venv/local/lib/python2.7/site-packages/django/forms/models.py", line 427, in _post_clean
self.instance = construct_instance(self, self.instance, opts.fields, construct_instance_exclude)
File "/home/vagrant/venv/local/lib/python2.7/site-packages/django/forms/models.py", line 62, in construct_instance
f.save_form_data(instance, cleaned_data[f.name])
File "/home/vagrant/venv/local/lib/python2.7/site-packages/django/db/models/fields/__init__.py", line 874, in save_form_data
setattr(instance, self.name, data)
File "/home/vagrant/venv/local/lib/python2.7/site-packages/django/db/models/fields/related.py", line 632, in __set__
instance._state.db = router.db_for_write(instance.__class__, instance=value)
File "/home/vagrant/venv/local/lib/python2.7/site-packages/django/db/utils.py", line 300, in _route_db
if instance is not None and instance._state.db:
File "/home/vagrant/venv/local/lib/python2.7/site-packages/mock/mock.py", line 716, in __getattr__
raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute '_state'
How do I avoid that part? I don't want it to look into instance._state.db
at all.
Am I testing the form correctly? Or should I instead of calling form.is_valid()
, just call form.clean()
method, by patching the super(MenuForm, self).clean()
method completely, and check form.errors
?
I would say calling form.is_valid()
is a good way to test a form. I am not sure about mocking the model though.
Internally the form is calling get_limit_choices_to
on your dish
field (Which Django is currently creating for you).
You would need to mock the dish
field's .queryset
or get_limit_choices_to
here, (or somewhere else in the call stack that makes the values here meaningless) in some way to achieve what you want.
Alternatively it would be much simpler to create a Dish
inside your test and let Django's internals keep doing what they are doing.
class MenuFormTest(TestCase):
def test_price_threshold(self):
dish = Dish.objects.create(
# my values here
)
form_data = {
'dish': dish.id,
'price': 80,
}
form = MenuForm(data=form_data)
self.assertTrue(form.is_valid())
If you are really set on not using Django's test database, one strategy could be to mock the MenuForm.clean
and MenuForm._post_clean
:
class MenuFormTest(TestCase):
def test_price_threshold(self):
mock_dish = mock.Mock(spec=Dish)
form_data = {
'dish': 1,
'price': 80,
}
form = MenuForm(data=form_data)
form.fields['dish'].clean = lambda _: mock_dish
form._post_clean = lambda : None
self.assertTrue(form.is_valid())
You will need to ask yourself what your goal is with this test if you are going to do this.
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