当调用 python Mock 时,我如何 运行 一个函数(以获得副作用)?

How can I run a function (to get side effects) when a python Mock is called?

我正在模拟(使用 python Mock)一个我想要 return 值列表的函数,但是在列表中的某些项目中我也希望出现副作用(在调用模拟函数的地方)。这是如何最容易做到的?我正在尝试这样的事情:

import mock
import socket

def oddConnect():
  result = mock.MagicMock()  # this is where the return value would go
  raise socket.error  # I want it assigned but also this raised

socket.create_connection = mock(spec=socket.create_connection,
  side_effect=[oddConnect, oddConnect, mock.MagicMock(),])
# what I want: call my function twice, and on the third time return normally
# what I get: two function objects returned and then the normal return

for _ in xrange(3):
  result = None
  try:
    # this is the context in which I want the oddConnect function call
    # to be called (not above when creating the list)
    result = socket.create_connection()
  except socket.error:
    if result is not None:
      # I should get here twice
      result.close()
      result = None
  if result is not None:
    # happy days we have a connection
    # I should get here the third time
    pass

我从 socket 的内部复制了 except 子句(它是内部的 if)并想验证我 "test" 通过我的代码副本的路径。 (我不明白套接字如何获得该代码(设置目标同时仍然引发异常,但这不是我关心的,只是我验证我可以复制该代码路径。)这就是我想要的原因效果在调用模拟时发生,而不是在我构建列表时发生。

根据 unittest.mock documentation for side_effect:

If you pass in an iterable, it is used to retrieve an iterator which must yield a value on every call. This value can either be an exception instance to be raised, or a value to be returned from the call to the mock (DEFAULT handling is identical to the function case).

因此,您的 socket.create_connection mock 将 return function oddConnect 作为第一个两次调用,然后 return 最后一次调用的 Mock 对象。据我了解,您想模拟 create_connection 对象来实际调用这些函数作为副作用,而不是 returning 它们。

我觉得这种行为很奇怪,因为你期望 side_effect 在每种情况下都意味着 side_effect 而不是 return_value .我想这是因为 return_value 属性 的值必须被解释为 as-is。例如,如果你的 Mock 有 return_value=[1, 2, 3],那么你的 Mock return [1, 2, 3] 会为 every 调用,还是 return 1 第一次通话?

解决方案

幸运的是,这个问题有一个解决方案。根据文档,如果您将单个函数传递给 side_effect,那么该函数每次都会被 调用 (不是 returned) 模拟被调用。

If you pass in a function it will be called with same arguments as the mock and unless the function returns the DEFAULT singleton the call to the mock will then return whatever the function returns. If the function returns DEFAULT then the mock will return its normal value (from the return_value).

因此,为了达到预期的效果,您的 side_effect 函数必须在每次调用时执行不同的操作。您可以使用函数中的计数器和一些条件逻辑轻松实现这一点。请注意,为了使其工作,您的计数器必须存在于函数范围之外,因此当函数退出时计数器不会被重置。

import mock
import socket

# You may wish to encapsulate times_called and oddConnect in a class
times_called = 0
def oddConnect():
  times_called += 1
  # We only do something special the first two times oddConnect is called
  if times_called <= 2:
    result = mock.MagicMock()  # this is where the return value would go
    raise socket.error  # I want it assigned but also this raised  

socket.create_connection = mock(spec=socket.create_connection,
  side_effect=oddConnect)
# what I want: call my function twice, and on the third time return normally
# what I get: two function objects returned and then the normal return

for _ in xrange(3):
  result = None
  try:
    # this is the context in which I want the oddConnect function call
    # to be called (not above when creating the list)
    result = socket.create_connection()
  except socket.error:
    if result is not None:
      # I should get here twice
      result.close()
      result = None
  if result is not None:
    # happy days we have a connection
    # I should get here the third time
    pass

我还遇到了希望仅对值列表中的某些项目产生副作用的问题。

在我的例子中,我想在第三次调用我的模拟方法时从 freezegun 调用一个方法。这些答案对我真的很有帮助;我最终写了一个相当通用的包装器 class,我想我会在这里分享它:

class DelayedSideEffect:
    """
    If DelayedSideEffect.side_effect is assigned to a mock.side_effect, allows you to
    delay the first call of callback until after a certain number of iterations.
    """
    def __init__(self, callback, delay_until_call_num: int, return_value=DEFAULT):
        self.times_called = 0
        self.delay_until_call_num = delay_until_call_num
        self.callback = callback
        self.return_value = return_value

    def side_effect(self, *args, **kwargs):
        self.times_called += 1
        if self.times_called >= self.delay_until_call_num:
            self.callback()
        return self.return_value

然后 return "my_default_return_value" 在前三个调用中不调用 lambda 函数:

with freeze_time(datetime.now()) as freezer:
    se = DelayedSideEffect(callback=lambda: freezer.move_to(the_future), 3)
    my_mock = MagicMock(return_value="my_default_return_value", side_effect=se)