Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UnitTest ModelForm having ModelChoiceField with Mock Data

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?

like image 414
Rohit Jain Avatar asked Sep 04 '25 03:09

Rohit Jain


1 Answers

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.

like image 114
quaspas Avatar answered Sep 05 '25 21:09

quaspas