在自动指定时模拟side_effect会为函数提供附加参数

时间:2016-05-27 09:52:59

标签: python unit-testing mocking

所以示例代码非常基础:

@mock.patch.object(BookForm, 'is_valid')
def test_edit(self, mocked_is_valid):
    create_superuser()
    self.client.login(username="test", password="test")

    book = Book()
    book.save()

    mocked_is_valid.side_effect = lambda: True

    self.client.post(reverse('edit', args=[book.pk]), {})

这很有效。但是在模拟中添加autospec关键字:

@mock.patch.object(BookForm, 'is_valid', autospec=True)

导致将附加参数传递给side_effect callable,这显然会导致错误:

TypeError: <lambda>() takes 0 positional arguments but 1 was given

我不明白的是,为什么自动指定会提供额外的论据。我已阅读docs,但仍无法找到此行为的解释。

理论上,它写的是

  

此外,模拟的函数/方法与原始函数/方法具有相同的调用签名,因此如果调用错误,它们会引发TypeError。

所以它没关系(is_validself参数,这可能是这里传递的内容),但另一方面它也写了{{{ 1}}

  

使用与mock相同的参数调用函数,除非它返回DEFAULT,否则此函数的返回值将用作返回值。

据我所知,即使没有自动指定,也应使用side_effect参数调用side_effect。但事实并非如此。

  使用与mock

相同的参数调用

self

因此,如果有人能够向我解释,最好引用文档,我会感激不尽。

1 个答案:

答案 0 :(得分:2)

您误解了文档。如果没有autospec,则在不检查原始声明的情况下,被调用的side_effect实际上就是这样。让我们创建一个更好的最小示例来演示此问题。

class Book(object):
    def __init__(self):
        self.valid = False
    def save(self):
        self.pk = 'saved'
    def make_valid(self):
        self.valid = True

class BookForm(object):
    def __init__(self, book):
        self.book = book
    def is_valid(self):
        return self.book.valid

class Client(object):
    def __init__(self, book):
        self.form = BookForm(book)
    def post(self):
        if self.form.is_valid() is True:  # to avoid sentinel value
            print('Book is valid')
        else:
            print('Book is invalid')

现在您的原始测试应该与一些调整相同

@mock.patch.object(BookForm, 'is_valid')
def test_edit(mocked_is_valid):
    book = Book()
    book.save()
    client = Client(book)
    mocked_is_valid.side_effect = lambda: True
    client.post()

按原样运行测试会导致Book is valid打印到标准输出,即使我们没有通过舞蹈将Book.valid标志设置为true,因为self.form.is_valid正在在Client.post中调用的被替换为被调用的lambda。我们可以通过调试器看到这一点:

> /usr/lib/python3.4/unittest/mock.py(962)_mock_call()
-> ret_val = effect(*args, **kwargs)
(Pdb) pp effect
<function test_edit.<locals>.<lambda> at 0x7f021dee6730>
(Pdb) bt
...
  /tmp/test.py(20)post()
-> if self.form.is_valid():
  /usr/lib/python3.4/unittest/mock.py(896)__call__()
-> return _mock_self._mock_call(*args, **kwargs)
  /usr/lib/python3.4/unittest/mock.py(962)_mock_call()
-> ret_val = effect(*args, **kwargs)

同样在Client.post方法调用的框架内,它不是绑定方法(我们稍后会再回过头来)

(Pdb) self.form.is_valid
<MagicMock name='is_valid' id='140554947029032'>

嗯,我们可能会遇到一个问题:side_effect字面上可以是任何可调用的,可能与现实不同,在我们的例子中是is_valid函数签名(即参数列表) )可能与我们提供的模拟不同。如果BookForm.is_valid方法被修改为接受另一个参数,那该怎么办:

class BookForm(object):
    def __init__(self, book):
        self.book = book
    def is_valid(self, authcode):
        return authcode > 0 and self.book.valid

重新运行我们的测试...即使Client.post仍在调用BookForm.is_valid 任何,我们会看到我们的测试已传递参数。即使您的测试已通过,您的产品也会在生产中失败。这就是引入autospec参数的原因,我们将在第二次测试中应用它而不通过side_effect替换callable:

@mock.patch.object(BookForm, 'is_valid', autospec=True)
def test_edit_autospec(mocked_is_valid):
    book = Book()
    book.save()
    client = Client(book)
    client.post()

现在调用函数

时会发生这种情况
Traceback (most recent call last):
  ...
  File "/tmp/test.py", line 49, in test_edit_autospec
    client.post()
  File "/tmp/test.py", line 20, in post
    if self.form.is_valid():
  ...
  File "/usr/lib/python3.4/inspect.py", line 2571, in _bind
    raise TypeError(msg) from None
TypeError: 'authcode' parameter lacking default value

您想要的是什么以及autospec打算提供什么 - 在调用模拟之前进行检查,并且

  

此外,模拟的函数/方法与原始函数/方法具有相同的调用签名,因此如果调用错误,它们会引发TypeError。

因此,我们必须通过提供大于Client.post的authcode来修复0方法。

    def post(self):
        if self.form.is_valid(123) is True:
            print('Book is valid')
        else:
            print('Book is invalid')

由于我们的测试没有通过is_valid callable模拟side_effect函数,因此该方法最终会打印Book is invalid

现在,如果我们想提供side_effect,则必须匹配相同的签名

@mock.patch.object(BookForm, 'is_valid', autospec=True)
def test_edit_autospec(mocked_is_valid):
    book = Book()
    book.save()
    client = Client(book)
    mocked_is_valid.side_effect = lambda self, authcode: True
    client.post()
现在将再次打印

Book is valid。浏览调试器以检查autospec方法调用的框架内的is_valid&d;以及模拟Client.post对象

(Pdb) self.form.is_valid
<bound method BookForm.is_valid of <__main__.BookForm object at 0x7fd57f43dc88>>

啊,不知何故方法签名不是一个简单的MagicMock对象(回想一下前面提到的<MagicMock name='is_valid' id='140554947029032'>)并且是一个正确绑定的方法,这意味着self参数现在被传递到模拟,解决这个问题:

  

side_effect :每次调用Mock时都要调用的函数。请参阅side_effect属性。用于引发异常或动态更改返回值。使用与mock ...

相同的参数调用该函数

&#34;与mock&#34;相同的参数在这种情况下,意味着与传递到模拟中的任何内容相同。重申一下,第一种情况是self.form.is_valid被裸露的,无界的可调用替换,因此self永远不会通过;在第二种情况下,可调用现在绑定到selfselfauthcode都将被传递到side_effect可调用 - 就像在真实中发生的那样呼叫。这应该与autospec=True的{​​{1}}和mock.patch.object的{​​{1}}交互的感知错误行为进行协调,并为模拟手动定义side_effect