Skip to content

Commit 08add92

Browse files
committed
Now possible to spy methods in class, static and class methods
Also improved docs, made more compliant to reStructuredText expectations.
1 parent 7f00103 commit 08add92

File tree

3 files changed

+115
-20
lines changed

3 files changed

+115
-20
lines changed

README.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ Spy
7979
The spy acts exactly like the original method in all cases, except it allows use of `mock`
8080
features with it, like retrieving call count.
8181

82+
From version 0.7 onward it also works for class and static methods. Originally it was only safe to
83+
use with instance methods.
84+
8285
.. code-block:: python
8386
8487
def test_spy(mocker):

pytest_mock.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import sys
22

3+
import inspect
34
import pytest
45

56

@@ -36,23 +37,40 @@ def spy(self, obj, method_name):
3637
Creates a spy of method. It will run method normally, but it is now possible to use `mock`
3738
call features with it, like call count.
3839
39-
:param object obj:
40-
An object.
40+
:param object obj: An object.
41+
:param unicode method_name: A method in object.
42+
:rtype: mock.MagicMock
43+
:return: Spy object.
44+
"""
45+
method = getattr(obj, method_name)
4146

42-
:param unicode method_name:
43-
A method in object.
47+
if not inspect.ismethod(method): # staticmethod
48+
can_autospec = False
49+
elif method.__self__ is obj: # classmethod
50+
can_autospec = False
51+
else:
52+
can_autospec = True
4453

45-
:return: mock.MagicMock
46-
Spy object.
47-
"""
48-
return self.patch.object(obj, method_name, side_effect=getattr(obj, method_name))
54+
if can_autospec:
55+
kwargs = dict(autospec=True)
56+
else:
57+
# Can't use autospec because of https://bugs.python.org/issue23078
58+
kwargs = dict(
59+
new_callable=mock_module.MagicMock,
60+
spec=True,
61+
)
62+
63+
def func(*args, **kwargs):
64+
return method(*args, **kwargs)
65+
66+
return self.patch.object(obj, method_name, side_effect=func, **kwargs)
4967

5068
def stub(self):
5169
"""
5270
Creates a stub method. It accepts any arguments. Ideal to register to callbacks in tests.
5371
54-
:return: mock.MagicMock
55-
Stub object.
72+
:rtype: mock.MagicMock
73+
:return: Stub object.
5674
"""
5775
return mock_module.MagicMock(spec=lambda *args, **kwargs: None)
5876

test_pytest_mock.py

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import namedtuple
12
import os
23

34
import pytest
@@ -138,24 +139,97 @@ def test_mocker_has_mock_class_as_attribute_for_instantiation():
138139
assert isinstance(mocker.Mock(), mock_module.Mock)
139140

140141

141-
def test_mocker_spy(mocker):
142+
def test_mocker_stub(mocker):
143+
def foo(on_something):
144+
on_something('foo', 'bar')
145+
146+
stub = mocker.stub()
147+
148+
foo(stub)
149+
stub.assert_called_once_with('foo', 'bar')
150+
151+
152+
def test_mocker_spy(mocker_spy_cases):
153+
foo, expect_obj = mocker_spy_cases
154+
155+
assert foo.bar(arg=10) == 20
156+
if expect_obj:
157+
foo.bar.assert_called_once_with(foo, arg=10)
158+
else:
159+
foo.bar.assert_called_once_with(arg=10)
160+
161+
162+
def create_instance_method_spy(mocker):
142163
class Foo(object):
143164

144165
def bar(self, arg):
145166
return arg * 2
146167

147168
foo = Foo()
148-
spy = mocker.spy(foo, 'bar')
169+
mocker.spy(foo, 'bar')
170+
return foo, False
149171

150-
assert foo.bar(arg=10) == 20
151-
spy.assert_called_once_with(arg=10)
152172

173+
def create_instance_method_by_class_spy(mocker):
174+
class Foo(object):
153175

154-
def test_mocker_stub(mocker):
155-
def foo(on_something):
156-
on_something('foo', 'bar')
176+
def bar(self, arg):
177+
return arg * 2
157178

158-
stub = mocker.stub()
179+
mocker.spy(Foo, 'bar')
180+
return Foo(), True
159181

160-
foo(stub)
161-
stub.assert_called_once_with('foo', 'bar')
182+
183+
def create_class_method_spy(mocker):
184+
class Foo(object):
185+
186+
@classmethod
187+
def bar(cls, arg):
188+
return arg * 2
189+
190+
mocker.spy(Foo, 'bar')
191+
return Foo, False
192+
193+
194+
def create_class_method_with_metaclass_spy(mocker):
195+
class MetaFoo(type): pass
196+
197+
class Foo(object):
198+
199+
__metaclass__ = MetaFoo
200+
201+
@classmethod
202+
def bar(cls, arg):
203+
return arg * 2
204+
205+
mocker.spy(Foo, 'bar')
206+
return Foo, False
207+
208+
209+
def create_static_method_spy(mocker):
210+
class Foo(object):
211+
212+
@staticmethod
213+
def bar(arg):
214+
return arg * 2
215+
216+
mocker.spy(Foo, 'bar')
217+
return Foo, False
218+
219+
220+
mock_spy_case = namedtuple('mock_spy_case', ['do', 'name'])
221+
222+
@pytest.fixture(
223+
params=[
224+
mock_spy_case(do=create_instance_method_spy, name='instance_method'),
225+
mock_spy_case(do=create_instance_method_by_class_spy, name='instance_method_by_class'),
226+
mock_spy_case(do=create_class_method_spy, name='classmethod'),
227+
mock_spy_case(do=create_class_method_with_metaclass_spy, name='classmethod_with_metaclass'),
228+
mock_spy_case(do=create_static_method_spy, name='staticmethod'),
229+
],
230+
ids=lambda i: i.name,
231+
)
232+
def mocker_spy_cases(mocker, request):
233+
case = request.param
234+
235+
return case.do(mocker)

0 commit comments

Comments
 (0)