60

I'm using the Python mock module for tests. I would like to replace an active object with a mock, and automatically have all calls made to the mock object forwarded to the original object. I think this is called a "Spy" in standard testing terminology. At the moment I'm doing inside a test:

# Insert a mock replacement
orig_active_attr = server.active_attr
server.active_attr = mock.Mock()

# Set up side effects to 'proxy' to the original object
server.active_attr.meth1.side_effect = orig_active_attr.meth1
server.active_attr.meth2.side_effect = orig_active_attr.meth2

# Call the method being tested
server.method_being_tested()

# Assert stuff on the mock.
server.active_attr.meth2.assert_called_once()

It would be nice if all method calls on the mock could be forwarded to the live object automatically without the boilerplate.

Rod
  • 52,748
  • 3
  • 38
  • 55
NeilenMarais
  • 2,949
  • 1
  • 25
  • 23
  • > It would be nice if all method calls on the mock could be forwarded to the live object automatically without the boilerplate. ---- no it wouldn't. a mock would not be a mock if it's _actually_ performing the behavior you want to mock. – wesm Oct 01 '16 at 16:00
  • 4
    Semantics I guess, what I'm looking for here is called a Spy, but the library that does it is called mock. Or at least that is how I understand it :) – NeilenMarais Oct 03 '16 at 04:39

6 Answers6

57

I seem to have stumbled across the solution:

import mock

class A(object):
    def meth(self, a):
        return a
a = A()
ma = mock.Mock(wraps=a)

Seems to work okay for functions, methods and properties, but not for class or instance attributes.

See the documentation.

Brian McCutchon
  • 8,354
  • 3
  • 33
  • 45
NeilenMarais
  • 2,949
  • 1
  • 25
  • 23
33

You can use patch.object(wraps=obj_instance) as suggested in Spying on instance methods with Python's mock module.

For example:

from mock import patch

class Foo(object):
    def bar(self, x, y):
        return x + y + 1

def test_bar():
    foo = Foo()
    with patch.object(foo, 'bar', wraps=foo.bar) as wrapped_foo:
        foo.bar(1, 2)
        wrapped_foo.assert_called_with(1, 2)
Wilfred Hughes
  • 29,846
  • 15
  • 139
  • 192
3

Here's how to mock only datetime.date.today(), forwarding the rest of datetime calls to the datetime module:

from unittest import mock, TestCase

import foo_module

class FooTest(TestCase):
  
    @mock.patch(f'{foo_module.__name__}.datetime', wraps=foo_module.datetime)
    def test_something(self, mock_datetime):
        # mock only datetime.date.today()
        mock_datetime.date.today.return_value = datetime.date(2019, 3, 15)
        # other calls to datetime functions will be forwarded to original datetime

foo_module imports datetime and uses many other datetime functions besides date.today.

mrts
  • 16,697
  • 8
  • 89
  • 72
  • I think this doesn't answer this Question about _spying_ (as opposed to what I think is just mocking in this answer). – cellepo Aug 13 '21 at 00:16
  • 1
    This answers the question/problem statement in the description: _It would be nice if all method calls on the mock could be forwarded to the live object automatically without the boilerplate._ – mrts Aug 16 '21 at 11:58
3

If you use pytest, the pytest-mock package has a convenient spy object.

class Foo(object):
    def bar(self, v):
        return v * 2

def test_spy_method(mocker):
    foo = Foo()
    spy = mocker.spy(foo, 'bar')
    assert foo.bar(21) == 42

    spy.assert_called_once_with(21)
    assert spy.spy_return == 42
Christian Long
  • 10,385
  • 6
  • 60
  • 58
2

You can use a simple function to iterate through all the method and configure your mock

def spy_mock(instance):
    members = inspect.getmembers(instance, inspect.ismethod)
    attrs = {'%s.side_effect' % k:v for k,v in members}
    return mock.Mock(**attrs)

Usage would be

import inspect
from unittest import mock

class ActiveAttr:

    def meth2(self):
        print("Meth2 called")

class Server:

    def __init__(self):
        self.active_attr = ActiveAttr()

    def method_being_tested(self):
        self.active_attr.meth2()


def spy_mock(instance):
    members = inspect.getmembers(instance, inspect.ismethod)
    attrs = {'%s.side_effect' % k:v for k,v in members}
    return mock.Mock(**attrs)

server = Server()
server.active_attr = spy_mock(server.active_attr)

server.method_being_tested()

server.active_attr.meth2.assert_called_once()
Rod
  • 52,748
  • 3
  • 38
  • 55
  • 2
    It isn't clear how to use these modified methods that you're spying on. Could you show an example? – byxor Jan 23 '17 at 22:32
  • I think this doesn't answer this Question about _spying_ (as opposed to what I think is just mocking in this answer). – cellepo Aug 13 '21 at 00:17
  • Been a long time since I answered this and @NeilenMarais answer is better. I still went ahead and updated the answer to show how it would use. It is indeed a spy that fulfills that answers the question. – Rod Aug 18 '21 at 20:04
2

Extending upon the pattern from Wes McKinney (via Wilfred Hughes Answer), here is how to spy on a method/member of an object, where object is imported into module under test.

Note this solution is Python 2.x compliant!

module under test:

import object

def function_in_module_under_test():
  object.method_from_imported_object()

testing spied assertion on method_from_imported_object:

from unittest import mock

import module_under_test    

def test_method(self):
  with mock.patch.object(
    module_under_test.object,
    "method_from_imported_object", 
    module_under_test.object.method_from_imported_object,
  ) as spy_method_from_imported_object:

    # Demonstrate that subsequent spy asserts here, can be trusted
    spy_method_from_imported_object.assert_not_called()
    
    # now Test
    module_under_test.function_in_module_under_test()
    spy_method_from_imported_object.assert_called_once()
Clint Eastwood
  • 4,995
  • 3
  • 31
  • 27
cellepo
  • 4,001
  • 2
  • 38
  • 57
  • 1
    awesome! I'm stuck with Python 2 so can't really use `wraps=bla` :-| – Clint Eastwood Mar 03 '22 at 19:38
  • To test internal methods, I mocked the class first, then called `method_mocks` to find the absolute path. Then I used `patch.object` to mock the object directly. I had to use it with 2 args, b/c the third arg (new) points to the actual object I want to call, but it doesn't work with .assert* b/c it is not mock object. Not sure if this was changed in later versions? – gagarwa Feb 08 '23 at 23:41