Объект 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'