我正在模拟(使用 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_effect
的unittest.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)