pytest-mock icon indicating copy to clipboard operation
pytest-mock copied to clipboard

Make spy a context manager

Open neumond opened this issue 7 years ago • 5 comments

Capture return value

NOTE: already implemented in 1.11

class SomeClass:
    def method(self, a):
        return a * 2

    def facade(self):
        self.method(4)
        return 'Done'


def test_some_class(mocker):
    spy = mocker.spy(SomeClass, 'method')
    o = SomeClass()
    o.facade()
    assert spy.called
    assert spy.call_count == 1

    print(spy.mock_calls[0])
    # nicely outputs method arguments
    # call(<m.SomeClass object at 0x7febfed74518>, 4)

    print(spy.return_value)
    # just tells us we have mock object here
    # <MagicMock name='method()' id='140651569552856'>
    
    # it would be much better to have there
    # assert spy.return_value == 8
    # or, rather
    # assert spy.mock_calls[0].return_value == 8

Spy as context manager

That's probably kind of misusing mocker object, but in my case there's a quite long test and I want to restrict fragment of code where particular spy object is applied.

def test_context_mgr(mocker):
    o = SomeClass()
    with mocker.spy(SomeClass, 'method') as spy:  # AttributeError: __enter__ here
        o.facade()
        assert spy.call_count == 1
    o.facade()

Currently I can only find arguments

neumond avatar Jun 14 '18 17:06 neumond

Hi @neumond,

Thanks for taking the time to write your suggestions! Also appreciate the easy to follow examples.

Unfortunately none of the suggestions can be implemented without breaking the current interface, so we might need a new method name that returns an object that:

  • can records the return value of mocked functions;
  • can be used as a context manager.

Do you have any suggestions?

cc @fogo, which contributed mocker.spy initially.

nicoddemus avatar Jun 14 '18 20:06 nicoddemus

Hello there,

just found this randomly while looking for some sort of "spy as context manager" feature. Though what naively imagined would be more like :

def test_context_mgr(mocker):
    o = SomeClass()
    with mocker.spy(SomeClass, 'method', call_count=1):
        o.facade()
    o.facade()

(not necessarily using spy() as name if it's troublesome)

The motivation would be to have something similar to pytest.raises which only works for exceptions as far as I understand.

alexAubin avatar Sep 23 '19 11:09 alexAubin

I run into a similar need of capturing return values from the object spied upon. My solution was a wrapper around unittest.mock.patch.object which adds a side-effect to stores both function call args as well as function result (return value or exception raised). But then I also recreated the same yield_fixture to guarantee the patched object would be restored, so basically I've duplicated quite a bit of pytest-mock. And I hate duplication. :)

If we can agree on what the solution should roughly look like,I can try writing up a PR. What about mocker.watch() with same signature as spy()? It would basically work like patch.object, then after starting the patch apply a side_effect wrapper that registers call args/returnvalue/exceptions (I'm storing them as namedtuples, but can change that).

Mark90 avatar Nov 13 '19 09:11 Mark90

Didn't notice that you already implemented this with 1.11. Please disregard my comment.

Awesome work by the way :)

Mark90 avatar Nov 13 '19 10:11 Mark90

@Mark90 indeed the issue was a bit confusing, thanks for bringing this to attention.

I've updated the title and mentioned that the return value has been implemented in 1.11. 👍

nicoddemus avatar Nov 18 '19 20:11 nicoddemus