Python возвращает объект MagicMock вместо return_value

У меня есть файл python a.py, который содержит два класса A и B.

class A(object):
    def method_a(self):
        return "Class A method a"

class B(object):
    def method_b(self):
        a = A()
        print a.method_a()

Я хотел бы unittest method_b в классе B путем издевательства A. Вот содержание файла testa.py для этой цели:

import unittest
import mock
import a


class TestB(unittest.TestCase):

    @mock.patch('a.A')
    def test_method_b(self, mock_a):
        mock_a.method_a.return_value = 'Mocked A'
        b = a.B()
        b.method_b()


if __name__ == '__main__':
    unittest.main()

Я ожидаю получить Mocked A на выходе. Но я получаю:

<MagicMock name='A().method_a()' id='4326621392'>

Где я делаю неправильно?

Ответ 1

Когда вы @mock.patch('a.A'), вы заменяете класс A в тестируемом коде mock_a.

В B.method_b вы установите a = A(), который теперь a = mock_a() - i.e A является return_value of mock_a. Поскольку вы не указали это значение, оно является регулярным MagicMock; это также не настроено, поэтому вы получаете ответ по умолчанию (еще один MagicMock) при вызове методов на нем.

Вместо этого вы хотите настроить return_value of mock_a на соответствующий метод, который вы можете сделать так:

mock_a().method_a.return_value = 'Mocked A' 
    # ^ note parentheses

или, возможно, более явно:

mock_a.return_value.method_a.return_value = 'Mocked A'

Ваш код работал бы в случае a = A (назначая класс, а не создавая экземпляр), так как тогда a.method_a() вызвал бы ваш макетный метод.

Ответ 2

Я предпочитаю pytest с приспособлением пересмешника. Вот тот же тест, использующий pytest и mocker:

import a


class TestB:
    def test_method_b(self, mocker):
        mock_A = mocker.MagicMock(name='A', spec=a.A)
        mocker.patch('a.A', new=mock_A)
        mock_A.return_value.method_a.return_value = 'Mocked A'

        b = a.B()
        b.method_b()

Вы можете найти способ, которым я написал тест, более интересным, чем сам тест - я создал библиотеку Python, чтобы помочь мне с синтаксисом.

Вот как я систематически подходил к вашей проблеме:

Мы начнем с необходимого вам теста и моей вспомогательной библиотеки:

import a

from mock_autogen.pytest_mocker import PytestMocker


class TestB:
    def test_method_b(self, mocker):
        # this would output the mocks we need
        print(PytestMocker(a).mock_classes().prepare_asserts_calls().generate())

        # your original test, without the mocks
        b = a.B()
        b.method_b()

Теперь тест не делает много, но вывод на печать полезен:

# mocked classes
mock_A = mocker.MagicMock(name='A', spec=a.A)
mocker.patch('a.A', new=mock_A)
mock_B = mocker.MagicMock(name='B', spec=a.B)
mocker.patch('a.B', new=mock_B)
# calls to generate_asserts, put this after the 'act'
import mock_autogen
print(mock_autogen.generator.generate_asserts(mock_A, name='mock_A'))
print(mock_autogen.generator.generate_asserts(mock_B, name='mock_B'))

Теперь я размещаю один макет для A до вызова B() и после секции generate_asserts, вот так (нет необходимости в предыдущем отпечатке, поэтому я удалил его):

def test_method_b(self, mocker):
    # mocked classes
    mock_A = mocker.MagicMock(name='A', spec=a.A)
    mocker.patch('a.A', new=mock_A)

    # your original test, without the mocks
    b = a.B()
    b.method_b()

    # calls to generate_asserts, put this after the 'act'
    import mock_autogen
    print(mock_autogen.generator.generate_asserts(mock_A, name='mock_A'))

После этого теста мы получили ценный вклад:

assert 1 == mock_A.call_count
mock_A.assert_called_once_with()
mock_A.return_value.method_a.assert_called_once_with()
mock_A.return_value.method_a.return_value.__str__.assert_called_once_with()

Первые две строки проверяют, что макет A был инициализирован один раз, без параметров. Третья строка подтверждает, что был вызван method_a, в то время как 4-я строка может быть наиболее полезной для вас и могла бы сэкономить вам много времени на самостоятельное выяснение этого:

mock_A.return_value.method_a.return_value.__str__.assert_called_once_with()

Вы видите, что возвращенное значение method_a было применено с str (из-за функции print). заменить его на желаемую строку довольно просто:

mock_A.return_value.method_a.return_value = 'Mocked A'

И вот как я получил полный метод испытаний, упомянутый выше.