Объект Python Mock с методом, называемым несколько раз
У меня есть класс, который я тестирую, который имеет зависимость другого класса (экземпляр которого передается методу 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 - это параметризованный метод, и как таковой я хочу создать один макетный объект, в котором в зависимости от того, какие аргументы передаются методуfromdepclass, он возвращает разные значения.
Я хочу, чтобы это параметризованное поведение состояло в том, что я хочу создать несколько экземпляров 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
Как достичь этой "семантики" с помощью "с учетом"?
Ответы
Ответ 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 kwarg['foo'] == 7:
return "Foo is seven"
mockobj.mockmethod.side_effect = my_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)
и весело на высокой скорости.
Ответ 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
Ответ 4
Как здесь, помимо использования side_effect
в unittest.mock.Mock, вы также можете использовать @mock.patch.object
с new_callable
, что позволяет вам связывать атрибут объекта с фиктивным объектом.
Допустим, модуль my_module.py
использует pandas
для чтения из базы данных, и мы хотели бы протестировать этот модуль с pd.read_sql_table
метода 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'