Python mock: метод экземпляра обертки

Что мне нужно: Убедитесь, что все экземпляры Foo, созданные внутри оператора with, имеют foo метод экземпляра, завернутый в MagicMock через wraps=Foo.foo. Причина, по которой я хочу этого, заключается в том, чтобы я мог отслеживать call_count в методе foo для всех созданных экземпляров Foo. Теперь, когда я так говорю, это кажется невозможным ...

>>> from mock import patch
...
... class Foo(object):
...
...     def foo(self):
...         return "foo"
...
... with patch("__main__.Foo.foo", wraps=Foo.foo) as m:
...     foo = Foo()
...     print(foo.foo())

Traceback (most recent call last):
  File "a.py", line 12, in <module>
    print(foo.foo())
  File "/disk/software/lib/python27/mock/mock.py", line 1062, in __call__
    return _mock_self._mock_call(*args, **kwargs)
  File "/disk/software/lib/python27/mock/mock.py", line 1132, in _mock_call
    return self._mock_wraps(*args, **kwargs)
TypeError: unbound method foo() must be called with Foo instance as first argument (got nothing instead)

Проблема. Поддельный foo метод не привязан к foo экземпляру, созданному с помощью foo = Foo(), потому что он является оболочкой для несвязанного метода Foo.foo. Кто-нибудь знает, как гарантировать, что издеваемый метод привязан к экземпляру?

Что я уже знаю:

>>> foo = Foo()
... with patch.object(foo, "foo", wraps=foo.foo) as m:
...     print(foo.foo())
"foo"

Но это не удовлетворяет мое ограничение, согласно которому объект должен быть создан внутри контекста patch.


person Filip Kilibarda    schedule 26.06.2017    source источник
comment
Вот метод, который работает, но он использует try..inally, а не фиктивную библиотеку: stackoverflow.com/questions/45584656/   -  person guettli    schedule 09.08.2017
comment
Я просто потратил несколько часов, пытаясь решить эту же проблему. Вы когда-нибудь находили что-нибудь полезное? Предложение @guettli по-прежнему полагается на наличие дескриптора экземпляра, поэтому оно бесполезно, если мои тесты не создают экземпляр напрямую.   -  person Colin    schedule 03.08.2018
comment
@Colin написал относительно простое решение ниже.   -  person Filip Kilibarda    schedule 04.08.2018


Ответы (2)


Проблема с предложенным мной и неправильным решением выше

with patch("__main__.Foo.foo", wraps=Foo.foo) as m:
    ...

заключается в том, что метод foo на Foo имитируется, так что он обертывает несвязанный метод Foo.foo, который, естественно, не работает, потому что несвязанный метод Foo.foo не знает, к какому экземпляру он присоединен, когда вызывается позже.

Самое простое решение, которое я мог придумать

from mock import patch, MagicMock

class Foo:

    def foo(self):
        return "foo"

class MockFoo(Foo):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Every instance of MockFoo will now have its `foo` method 
        # wrapped in a MagicMock
        self.foo = MagicMock(wraps=self.foo)

with patch("__main__.Foo", MockFoo) as m:
    foo = Foo()
    print(foo.foo())
    assert foo.foo.call_count == 1
person Filip Kilibarda    schedule 03.08.2018
comment
Я ушел от этой проблемы, но это решение очень интригующее! Я попробую! Спасибо за идею @Filip Kilibarda! - person Colin; 13.09.2018
comment
Вам все еще нужен экземпляр. Чем это отличается от кода OP? - person nitely; 27.12.2018
comment
@nito Да, вам все еще нужен экземпляр для проверки call_count, но вам не нужен экземпляр для применения патча. Это полезно в случаях, когда экземпляр класса создается глубоко внутри тестируемого кода, и экземпляр возвращается в тестовый код. При возврате к коду теста тест может проверить call_count этого конкретного экземпляра. Если вы хотите отслеживать общую сумму call_count всех экземпляров в блоке исправлений без доступа к самим экземплярам, ​​то это решение не для вас. - person Filip Kilibarda; 31.12.2018

Это настолько неприятно с mocks Python, что в итоге я создал собственную реализацию патча (при необходимости дополните ее другими функциями).

import contextlib

class Patcher:
    UNCHANGED_RET = object()

    def __init__(self):
        self.call_count = 0
        self.return_value = Patcher.UNCHANGED_RET


@contextlib.contextmanager
def patch(klass, method_name):
    patcher = Patcher()
    orig_method = getattr(klass, method_name)

    def new_method(myself, *args, **kwargs):
        patcher.call_count += 1
        orig_return_value = orig_method(myself, *args, **kwargs)

        if patcher.return_value != Patcher.UNCHANGED_RET:
            return patcher.return_value

        return orig_return_value

    setattr(klass, method_name, new_method)
    yield patcher
    setattr(klass, method_name, orig_method)

Используйте следующим образом:

class MyClass:
    def f(self):
        return 42


x = MyClass()
with patch(MyClass, 'f') as f_patcher:
    y = MyClass()  # inside or outside -- does not matter
    assert x.f() == 42
    assert f_patcher.call_count == 1
    f_patcher.return_value = 7
    assert y.f() == 7
    assert f_patcher.call_count == 2
person Eugene D. Gubenkov    schedule 28.04.2021