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

c29*_*301 6 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]), {})
Run Code Online (Sandbox Code Playgroud)

这很好用.但是在mock中添加autospec关键字:

@mock.patch.object(BookForm, 'is_valid', autospec=True)
Run Code Online (Sandbox Code Playgroud)

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

TypeError: <lambda>() takes 0 positional arguments but 1 was given
Run Code Online (Sandbox Code Playgroud)

我不理解的是,为什么自动指定提供了额外的论据.我已经阅读了文档,但仍然找不到这种行为的解释.

从理论上讲,它是写的

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

所以它没关系(is_validself争论,这可能是这里传递的东西),但另一方面它也写了关于side_effect那个

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

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

使用与mock相同的参数调用

if form.is_valid():  # the mock is_valid is called with the self argument, isn't it?
Run Code Online (Sandbox Code Playgroud)

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

met*_*ter 3

您误解了文档。如果没有autospecside_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')
Run Code Online (Sandbox Code Playgroud)

现在,您的原始测试应该与进行一些调整大致相同

@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()
Run Code Online (Sandbox Code Playgroud)

按原样运行测试将导致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)
Run Code Online (Sandbox Code Playgroud)

同样在方法调用的框架内Client.post,它不是绑定方法(我们稍后会回到这一点)

(Pdb) self.form.is_valid
<MagicMock name='is_valid' id='140554947029032'>
Run Code Online (Sandbox Code Playgroud)

所以嗯,我们可能在这里遇到一个问题: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
Run Code Online (Sandbox Code Playgroud)

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

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

现在调用函数时会发生这种情况

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
Run Code Online (Sandbox Code Playgroud)

这就是您想要的以及autospec打算提供的内容 - 在调用模拟之前进行检查,以及

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

因此,我们必须Client.post通过提供大于的授权码来修复该方法0

    def post(self):
        if self.form.is_valid(123) is True:
            print('Book is valid')
        else:
            print('Book is invalid')
Run Code Online (Sandbox Code Playgroud)

由于我们的测试没有is_valid通过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()
Run Code Online (Sandbox Code Playgroud)

Book is valid现在将再次打印。通过调试器检查方法调用框架内的autospec模拟对象is_validClient.post

(Pdb) self.form.is_valid
<bound method BookForm.is_valid of <__main__.BookForm object at 0x7fd57f43dc88>>
Run Code Online (Sandbox Code Playgroud)

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

side_effect:每当调用 Mock 时都会调用的函数。查看side_effect属性。对于引发异常或动态更改返回值很有用。使用与模拟相同的参数调用该函数...

在这种情况下,“与模拟相同的参数”意味着与传递到模拟中的任何内容相同。重申一下,第一个案例被self.form.is_valid替换为裸露的、无界的可调用,因此self永远不会被通过;在第二种情况下,可调用对象现在绑定到self,两个selfANDauthcode都将传递到可side_effect调用对象中 - 就像实际调用中会发生的情况一样。autospec=True这应该可以协调与for交互的感知不当行为mock.patch.object以及side_effect为模拟手动定义的可调用项。