具有多次调用方法的 Python Mock 对象
Posted
技术标签:
【中文标题】具有多次调用方法的 Python Mock 对象【英文标题】:Python Mock object with method called multiple times 【发布时间】:2011-12-01 17:09:46 【问题描述】:我有一个正在测试的类,它作为依赖项具有另一个类(其实例被传递给 CUT 的 init 方法)。我想使用 Python Mock 库来模拟这个类。
我所拥有的是这样的:
mockobj = Mock(spec=MyDependencyClass)
mockobj.methodfromdepclass.return_value = "the value I want the mock to return"
assertTrue(mockobj.methodfromdepclass(42), "the value I want the mock to return")
cutobj = ClassUnderTest(mockobj)
这很好,但是“methodfromdepclass”是一个参数化方法,因此我想创建一个模拟对象,根据传递给 methodfromdepclass 的参数,它返回不同的值。
我想要这种参数化行为的原因是我想创建多个包含不同值的 ClassUnderTest 实例(这些值由从 mockobj 返回的内容产生)。
有点像我的想法(这当然行不通):
mockobj = Mock(spec=MyDependencyClass)
mockobj.methodfromdepclass.ifcalledwith(42).return_value = "you called me with arg 42"
mockobj.methodfromdepclass.ifcalledwith(99).return_value = "you called me with arg 99"
assertTrue(mockobj.methodfromdepclass(42), "you called me with arg 42")
assertTrue(mockobj.methodfromdepclass(99), "you called me with arg 99")
cutinst1 = ClassUnderTest(mockobj, 42)
cutinst2 = ClassUnderTest(mockobj, 99)
# now cutinst1 & cutinst2 contain different values
如何实现这种“ifcallwith”语义?
【问题讨论】:
【参考方案1】:试试side_effect
def my_side_effect(*args, **kwargs):
if args[0] == 42:
return "Called with 42"
elif args[0] == 43:
return "Called with 43"
elif kwargs['foo'] == 7:
return "Foo is seven"
mockobj.mockmethod.side_effect = my_side_effect
【讨论】:
太棒了,正是我需要的。不是语法的粉丝,但效果很棒。谢谢! 我想在此处添加一个警告,我在使用此解决方案时运行该解决方案,效果很好,顺便说一句,如果您打算将异常作为副作用,则必须提出它们,而不是退回它们。 Mock 库非常适合让您为 side_effect 分配一个异常并找出它,但使用此方法您必须自己动手。 爱它 :) 所以救命 【参考方案2】:甜一点:
mockobj.method.side_effect = lambda x: 123: 100, 234: 10000[x]
或者对于多个参数:
mockobj.method.side_effect = lambda *x: (123, 234): 100, (234, 345): 10000[x]
或使用默认值:
mockobj.method.side_effect = lambda x: 123: 100, 234: 10000.get(x, 20000)
或两者兼而有之:
mockobj.method.side_effect = lambda *x: (123, 234): 100, (234, 345): 10000.get(x, 20000)
我们一路高歌猛进。
【讨论】:
有点简洁,但我喜欢。如果lambda
返回一个Mock
实例,从而使未解释的行为恢复为mock
库的常规行为,该怎么办?【参考方案3】:
我在进行自己的测试时遇到了这个问题。如果您不关心捕获对您的 methodfromdepclass() 的调用,而只需要它返回一些内容,那么以下可能就足够了:
def makeFakeMethod(mapping=):
def fakeMethod(inputParam):
return mapping[inputParam] if inputParam in mapping else MagicMock()
return fakeMethod
mapping = 42:"Called with 42", 59:"Called with 59"
mockobj.methodfromdepclass = makeFakeMethod(mapping)
这是一个参数化版本:
def makeFakeMethod():
def fakeMethod(param):
return "Called with " + str(param)
return fakeMethod
【讨论】:
+1: 比side_effect
的 if-block 模式更严格的语法,可能返回 Mock() 而不是 none 以保持默认行为?
感谢您的建议。将更新它以返回 MagicMock()。【参考方案4】:
与here 一样,除了在unittest.mock.Mock 中使用side_effect
之外,您还可以将@mock.patch.object
与new_callable
一起使用,这样您就可以使用模拟对象来修补对象的属性。
假设一个模块my_module.py
使用pandas
从数据库中读取数据,我们想通过模拟pd.read_sql_table
方法(以table_name
作为参数)来测试这个模块。
您可以做的是(在您的测试中)创建一个 db_mock
方法,该方法根据提供的参数返回不同的对象:
def db_mock(**kwargs):
if kwargs['table_name'] == 'table_1':
# return some DataFrame
elif kwargs['table_name'] == 'table_2':
# return some other DataFrame
然后在您的测试函数中执行以下操作:
import my_module as my_module_imported
@mock.patch.object(my_module_imported.pd, "read_sql_table", new_callable=lambda: db_mock)
def test_my_module(mock_read_sql_table):
# You can now test any methods from `my_module`, e.g. `foo` and any call this
# method does to `read_sql_table` will be mocked by `db_mock`, e.g.
ret = my_module_imported.foo(table_name='table_1')
# `ret` is some DataFrame returned by `db_mock`
【讨论】:
以上是关于具有多次调用方法的 Python Mock 对象的主要内容,如果未能解决你的问题,请参考以下文章
google mock - 我可以在同一个模拟对象上多次调用 EXPECT_CALL 吗?