Reputation: 213351
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
?
Upvotes: 4
Views: 1574
Reputation: 8686
If think your test is not "unit" enought. You seem to want to test the price treshold right ? Maybe you could do something like that :
# 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 not self._is_price_valid(price):
self.add_error('price', 'Min price threshold')
return cleaned_data
def _is_price_valid(self, price):
return price >= 70
And your test :
class MenuFormTest(TestCase):
def test_price_threshold(self):
form = forms.MenuForm()
self.assertTrue(form._is_price_valid(80))
I agree that for this example it's a little "overkill" to just add a method ta returns a simple comparison but if you just want to test the price threshold without bothering with form validation process internal to Django it's not bad to isolate it
Upvotes: 0
Reputation: 1351
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.
Upvotes: 7