Python mock - исправление метода без препятствия реализации

Есть ли чистый способ для исправления объекта, чтобы вы получили помощники assert_call* в вашем тестовом примере, без фактического удаления действия?

Например, как я могу изменить строку @patch, чтобы получить следующую тестовую передачу:

from unittest import TestCase
from mock import patch


class Potato(object):
    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):

    @patch.object(Potato, 'foo')
    def test_something(self, mock):
        spud = Potato()
        forty_two = spud.foo(n=40)
        mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

Я мог бы взломать это вместе, используя side_effect, но я надеялся, что будет более удобный способ, который работает одинаково со всеми функциями, методами класса, staticmethods, несвязанными методами и т.д.

Ответ 1

Аналогичное решение с вашим, но с помощью wraps:

def test_something(self):
    spud = Potato()
    with patch.object(Potato, 'foo', wraps=spud.foo) as mock:
        forty_two = spud.foo(n=40)
        mock.assert_called_once_with(n=40)
    self.assertEqual(forty_two, 42)

Согласно документации:

wraps: элемент для обманного объекта. Если обертки не являются None, тогда вызов Mock передаст вызов через обернутый объект (возврат реального результата). Атрибут доступа к макету будет возвращен объект Mock, который обертывает соответствующий атрибут завернутого object (поэтому попытка доступа к атрибуту, который не существует, будет поднять AttributeError).


class Potato(object):

    def spam(self, n):
        return self.foo(n=n)

    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2


class PotatoTest(TestCase):

    def test_something(self):
        spud = Potato()
        with patch.object(Potato, 'foo', wraps=spud.foo) as mock:
            forty_two = spud.spam(n=40)
            mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)

Ответ 2

В этом ответе укажите дополнительное требование, указанное в награде от пользователя Quuxplusone:

Важным моментом для моего использования является то, что он работает с @patch.mock, т.е. что мне не требуется вставлять какой-либо код между моим конструированием экземпляра Potato (spud в этом примере) и мое призвание spud.foo. Мне нужно spud создать с помощью mocked-out foo метода из get-go, потому что я не контролирую место создания spud.

Вариант использования, описанный выше, может быть достигнут без особых проблем с помощью декоратора:

import unittest
import unittest.mock  # Python 3

def spy_decorator(method_to_decorate):
    mock = unittest.mock.MagicMock()
    def wrapper(self, *args, **kwargs):
        mock(*args, **kwargs)
        return method_to_decorate(self, *args, **kwargs)
    wrapper.mock = mock
    return wrapper

def spam(n=42):
    spud = Potato()
    return spud.foo(n=n)

class Potato(object):

    def foo(self, n):
        return self.bar(n)

    def bar(self, n):
        return n + 2

class PotatoTest(unittest.TestCase):

    def test_something(self):
        foo = spy_decorator(Potato.foo)
        with unittest.mock.patch.object(Potato, 'foo', foo):
            forty_two = spam(n=40)
        foo.mock.assert_called_once_with(n=40)
        self.assertEqual(forty_two, 42)


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

Если замененный метод принимает изменяемые аргументы, которые изменяются под тестом, вы можете инициализировать CopyingMock вместо MagicMock внутри spy_decorator.