Skip to content

Make spy a context manager #118

Open

Description

@neumond

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

Activity

  1. nicoddemus commented on Jun 14, 2018

    @nicoddemus
    Member

    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.

  2. alexAubin commented on Sep 23, 2019

    @alexAubin

    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.

  3. Mark90 commented on Nov 13, 2019

    @Mark90

    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).

  4. Mark90 commented on Nov 13, 2019

    @Mark90

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

    Awesome work by the way :)

  5. changed the title [-]Spy improvements: capture return value, make spy a context manager[/-] [+]Make spy a context manager[/+] on Nov 18, 2019
  6. nicoddemus commented on Nov 18, 2019

    @nicoddemus
    Member

    @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. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions