-
Notifications
You must be signed in to change notification settings - Fork 1
/
minimock.py
302 lines (255 loc) · 9.98 KB
/
minimock.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# (c) 2006 Ian Bicking, Mike Beachy, and contributors
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
r"""
minimock is a simple library for doing Mock objects with doctest.
When using doctest, mock objects can be very simple.
Here's an example of something we might test, a simple email sender::
>>> import smtplib
>>> def send_email(from_addr, to_addr, subject, body):
... conn = smtplib.SMTP('localhost')
... msg = 'To: %s\nFrom: %s\nSubject: %s\n\n%s' % (
... to_addr, from_addr, subject, body)
... conn.sendmail(from_addr, [to_addr], msg)
... conn.quit()
Now we want to make a mock ``smtplib.SMTP`` object. We'll have to
inject our mock into the ``smtplib`` module::
>>> smtplib.SMTP = Mock('smtplib.SMTP')
>>> smtplib.SMTP.mock_returns = Mock('smtp_connection')
Now we do the test::
>>> send_email('ianb@colorstudy.com', 'joe@example.com',
... 'Hi there!', 'How is it going?')
Called smtplib.SMTP('localhost')
Called smtp_connection.sendmail(
'ianb@colorstudy.com',
['joe@example.com'],
'To: joe@example.com\nFrom: ianb@colorstudy.com\nSubject: Hi there!\n\nHow is it going?')
Called smtp_connection.quit()
Voila! We've tested implicitly that no unexpected methods were called
on the object. We've also tested the arguments that the mock object
got. We've provided fake return calls (for the ``smtplib.SMTP()``
constructor). These are all the core parts of a mock library. The
implementation is simple because most of the work is done by doctest.
"""
__all__ = ["mock", "restore", "Mock"]
import sys
import inspect
# A list of mocked objects. Each item is a tuple of (original object,
# namespace dict, object name, and a list of object attributes).
#
mocked = []
def lookup_by_name(name, nsdicts):
"""
Look up an object by name from a sequence of namespace dictionaries.
Returns a tuple of (nsdict, object, attributes); nsdict is the
dictionary the name was found in, object is the base object the name is
bound to, and the attributes list is the chain of attributes of the
object that complete the name.
>>> import os
>>> nsdict, name, attributes = lookup_by_name("os.path.isdir",
... (locals(),))
>>> name, attributes
('os', ['path', 'isdir'])
>>> nsdict, name, attributes = lookup_by_name("os.monkey", (locals(),))
Traceback (most recent call last):
...
NameError: name 'os.monkey' is not defined
"""
for nsdict in nsdicts:
attrs = name.split(".")
names = []
while attrs:
names.append(attrs.pop(0))
obj_name = ".".join(names)
if obj_name in nsdict:
attr_copy = attrs[:]
tmp = nsdict[obj_name]
try:
while attr_copy:
tmp = getattr(tmp, attr_copy.pop(0))
except AttributeError:
pass
else:
return nsdict, obj_name, attrs
raise NameError("name '%s' is not defined" % name)
def mock(name, nsdicts=None, mock_obj=None, **kw):
"""
Mock the named object, placing a Mock instance in the correct namespace
dictionary. If no iterable of namespace dicts is provided, use
introspection to get the locals and globals of the caller of this
function.
All additional keyword args are passed on to the Mock object
initializer.
An example of how os.path.isfile is replaced:
>>> import os
>>> os.path.isfile
<function isfile at ...>
>>> isfile_id = id(os.path.isfile)
>>> mock("os.path.isfile", returns=True)
>>> os.path.isfile
<Mock ... os.path.isfile>
>>> os.path.isfile("/foo/bar/baz")
Called os.path.isfile('/foo/bar/baz')
True
>>> mock_id = id(os.path.isfile)
>>> mock_id != isfile_id
True
A second mock object will replace the first, but the original object
will be the one replaced with the replace() function.
>>> mock("os.path.isfile", returns=False)
>>> mock_id != id(os.path.isfile)
True
>>> restore()
>>> os.path.isfile
<function isfile at ...>
>>> isfile_id == id(os.path.isfile)
True
"""
if nsdicts is None:
stack = inspect.stack()
try:
# stack[1][0] is the frame object of the caller to this function
globals_ = stack[1][0].f_globals
locals_ = stack[1][0].f_locals
nsdicts = (locals_, globals_)
finally:
del(stack)
if mock_obj is None:
mock_obj = Mock(name, **kw)
nsdict, obj_name, attrs = lookup_by_name(name, nsdicts)
# Get the original object and replace it with the mock object.
tmp = nsdict[obj_name]
if not attrs:
original = tmp
nsdict[obj_name] = mock_obj
else:
for attr in attrs[:-1]:
tmp = getattr(tmp, attr)
original = getattr(tmp, attrs[-1])
setattr(tmp, attrs[-1], mock_obj)
mocked.append((original, nsdict, obj_name, attrs))
def restore():
"""
Restore all mocked objects.
"""
global mocked
# Restore the objects in the reverse order of their mocking to assure
# the original state is retrieved.
while mocked:
original, nsdict, name, attrs = mocked.pop()
if not attrs:
nsdict[name] = original
else:
tmp = nsdict[name]
for attr in attrs[:-1]:
tmp = getattr(tmp, attr)
setattr(tmp, attrs[-1], original)
return
class Printer(object):
"""Prints all calls to the file it's instantiated with.
Can take any object that implements `write'.
"""
def __init__(self, file):
self.file = file
def call(self, func_name, *args, **kw):
parts = [repr(a) for a in args]
parts.extend(
'%s=%r' % (items) for items in sorted(kw.items()))
msg = 'Called %s(%s)' % (func_name, ', '.join(parts))
if len(msg) > 80:
msg = 'Called %s(\n %s)' % (
func_name, ',\n '.join(parts))
print >> self.file, msg
def set(self, obj_name, attr, value):
print >> self.file, 'Set %s.%s = %r' % (obj_name, attr, value)
class Mock(object):
def __init__(self, name, returns=None, returns_iter=None,
returns_func=None, raises=None, show_attrs=False,
tracker=None, **kw):
object.__setattr__(self, 'mock_name', name)
object.__setattr__(self, 'mock_returns', returns)
if returns_iter is not None:
returns_iter = iter(returns_iter)
object.__setattr__(self, 'mock_returns_iter', returns_iter)
object.__setattr__(self, 'mock_returns_func', returns_func)
object.__setattr__(self, 'mock_raises', raises)
object.__setattr__(self, 'mock_attrs', kw)
object.__setattr__(self, 'mock_show_attrs', show_attrs)
if tracker is None:
tracker = Printer(sys.stdout)
object.__setattr__(self, 'mock_tracker', tracker)
def __repr__(self):
return '<Mock %s %s>' % (hex(id(self)), self.mock_name)
def __call__(self, *args, **kw):
self.mock_tracker.call(self.mock_name, *args, **kw)
return self._mock_return(*args, **kw)
def _mock_return(self, *args, **kw):
if self.mock_raises is not None:
raise self.mock_raises
elif self.mock_returns is not None:
return self.mock_returns
elif self.mock_returns_iter is not None:
try:
return self.mock_returns_iter.next()
except StopIteration:
raise Exception("No more mock return values are present.")
elif self.mock_returns_func is not None:
return self.mock_returns_func(*args, **kw)
else:
return None
def __getattr__(self, attr):
if attr not in self.mock_attrs:
if self.mock_name:
new_name = self.mock_name + '.' + attr
else:
new_name = attr
self.mock_attrs[attr] = Mock(new_name,
show_attrs=self.mock_show_attrs,
tracker=self.mock_tracker)
return self.mock_attrs[attr]
def __setattr__(self, attr, value):
if attr in ["mock_raises", "mock_returns", "mock_returns_func", "mock_returns_iter", "mock_returns_func", "show_attrs"]:
object.__setattr__(self, attr, value)
else:
if self.mock_show_attrs:
self.mock_tracker.set(self.name, attr, value)
self.mock_attrs[attr] = value
__test__ = {
"mock" :
r"""
An additional test for mocking a function accessed directly (i.e.
not via object attributes).
>>> import os
>>> rename = os.rename
>>> orig_id = id(rename)
>>> mock("rename")
>>> mock_id = id(rename)
>>> mock("rename")
>>> mock_id != id(rename)
True
>>> restore()
>>> orig_id == id(rename) == id(os.rename)
True
The example from the module docstring, done with the mock/restore
functions.
>>> import smtplib
>>> def send_email(from_addr, to_addr, subject, body):
... conn = smtplib.SMTP('localhost')
... msg = 'To: %s\nFrom: %s\nSubject: %s\n\n%s' % (
... to_addr, from_addr, subject, body)
... conn.sendmail(from_addr, [to_addr], msg)
... conn.quit()
>>> mock("smtplib.SMTP", returns=Mock('smtp_connection'))
>>> send_email('ianb@colorstudy.com', 'joe@example.com',
... 'Hi there!', 'How is it going?')
Called smtplib.SMTP('localhost')
Called smtp_connection.sendmail(
'ianb@colorstudy.com',
['joe@example.com'],
'To: joe@example.com\nFrom: ianb@colorstudy.com\nSubject: Hi there!\n\nHow is it going?')
Called smtp_connection.quit()
>>> restore()
""",
}
if __name__ == '__main__':
import doctest
doctest.testmod(optionflags=doctest.ELLIPSIS)