【问题标题】:UnitTest ModelForm having ModelChoiceField with Mock DataUnitTest ModelForm 具有带有模拟数据的 ModelChoiceField
【发布时间】:2016-03-05 05:30:02
【问题描述】:

我一直在尝试为我的 ModelForm 编写单元测试,它有一个 ModelChoiceField。我正在使用模拟数据创建 Form 实例。

这是我的模型:

# 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

这是我的测试用例:

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())

这会失败并出现以下错误:

<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>

如何避免抛出该错误。 form.is_valid() 应该是 True 那里。有没有办法可以修补ModelChoiceField's queryset?我尝试修补表单的dish 字段clean() 方法,如下所示:

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())

然后看起来,在_post_clean() 方法中将表单数据保存到模型实例时失败了。这是追溯:

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'

如何避免那部分?我根本不希望它查看instance._state.db

我是否正确地测试了表单?或者我应该不调用form.is_valid(),而是调用form.clean()方法,完全修补super(MenuForm, self).clean()方法,然后检查form.errors

【问题讨论】:

    标签: python django unit-testing modelform


    【解决方案1】:

    我会说调用form.is_valid() 是测试表单的好方法。不过,我不确定是否要模拟模型。

    Internally the form is calling get_limit_choices_to on your dish field(Django 当前正在为您创建)。

    You would need to mock the dish field's .queryset or get_limit_choices_to here,(或调用堆栈中的其他位置使此处的值变得毫无意义)以某种方式实现您想要的。

    另外,在您的测试中创建一个Dish 并让 Django 的内部继续做他们正在做的事情会更简单。

    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())
    

    如果你真的决定不使用 Django 的测试数据库,一种策略可能是模拟 MenuForm.cleanMenuForm._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())
    

    如果您要进行此测试,您需要问问自己您的目标是什么。

    【讨论】:

    • 使用Model.objects.create 似乎比使用mock.Mock 更好。我的意思是,如果没有库也可以轻松完成,为什么还要使用库。
    【解决方案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 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
    

    你的测试:

    class MenuFormTest(TestCase):
        def test_price_threshold(self):
            form = forms.MenuForm()
            self.assertTrue(form._is_price_valid(80))
    

    我同意对于这个例子来说,只是添加一个方法 ta 返回一个简单的比较有点“矫枉过正”,但如果你只是想测试价格阈值而不用打扰 Django 内部的表单验证过程,那么隔离它也不错

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-08-15
      • 1970-01-01
      • 2014-04-20
      • 2015-10-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多