调用 python Mock 时,如何运行函数(以获得副作用)



我正在模拟(使用 python Mock)一个函数,我想在其中返回值列表,但在列表中的某些项目中,我希望也发生副作用(在调用模拟函数的位置)。 如何最容易做到这一点? 我正在尝试这样的事情:

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

except 子句(它是内部的 if)我从套接字的内部复制,并希望验证我是否通过代码副本"测试"该路径。 (我不明白套接字如何到达该代码(设置目标同时仍然引发异常,但这不是我关心的问题,只有我验证我可以复制该代码路径。 这就是为什么我希望副作用发生在调用模拟而不是构建列表时。

根据side_effectunittest.mock文档:

如果传入可迭代对象,则用于检索必须 在每次调用时生成一个值。此值可以是例外 要引发的实例,或从调用 模拟(DEFAULT处理与函数情况相同)。

因此,您的socket.create_connection模拟将返回前两次调用的函数oddConnect,然后返回最后一次调用的Mock对象。据我了解,您想模拟create_connection对象实际上将这些函数调用为副作用,而不是返回它们。

我觉得这种行为很奇怪,因为您希望side_effect,在每种情况下都意味着side_effect而不是return_value。我想这样做的原因在于return_value属性的值必须按原样解释。例如,如果您的模拟有return_value=[1, 2, 3],您的模拟会为每次调用返回[1, 2, 3],还是会在第一次调用时返回1

溶液

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

如果您传入一个函数,它将使用与 mock 和除非函数返回DEFAULT单例调用 然后,模拟将返回函数返回的任何内容。如果函数 返回DEFAULT然后模拟将返回其正常值(从 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 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

然后返回"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)

最新更新