# HG changeset patch # User Atul Varma # Date 1271183473 25200 # Node ID 7a04d148ead6db2d618a23f56da284c5c68ec0c2 # Parent 548a4fe96851159fda87c1fc58d4692878b695d4 Added tests diff -r 548a4fe96851 -r 7a04d148ead6 .hgignore --- a/.hgignore Tue Apr 13 11:05:00 2010 -0700 +++ b/.hgignore Tue Apr 13 11:31:13 2010 -0700 @@ -1,1 +1,2 @@ syntax: glob +*.pyc diff -r 548a4fe96851 -r 7a04d148ead6 minimock.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/minimock.py Tue Apr 13 11:31:13 2010 -0700 @@ -0,0 +1,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 + + >>> isfile_id = id(os.path.isfile) + >>> mock("os.path.isfile", returns=True) + >>> 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 + + >>> 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 '' % (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) diff -r 548a4fe96851 -r 7a04d148ead6 test_bugzilla.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test_bugzilla.py Tue Apr 13 11:31:13 2010 -0700 @@ -0,0 +1,63 @@ +import doctest +import unittest + +from minimock import Mock +import bugzilla + +CFG_WITH_LOGIN = {'api_server': 'http://foo/latest', + 'username': 'bar', + 'password': 'baz'} + +class Tests(unittest.TestCase): + pass + +def test_upload(self): + """ + >>> jsonreq = Mock('jsonreq') + >>> jsonreq.mock_returns = { + ... "status": 201, + ... "body": {"ref": "http://foo/latest/attachment/1"}, + ... "reason": "Created", + ... "content_type": "application/json" + ... } + >>> bzapi = bugzilla.BugzillaApi(config=CFG_WITH_LOGIN, + ... jsonreq=jsonreq) + >>> bzapi.post_attachment(bug_id=536619, + ... contents="testing!", + ... filename="contents.txt", + ... description="test upload") + Called jsonreq( + body={'is_obsolete': False, 'flags': [], 'description': 'test upload', 'content_type': 'text/plain', 'encoding': 'base64', 'file_name': 'contents.txt', 'is_patch': False, 'data': 'dGVzdGluZyE=', 'is_private': False, 'size': 8}, + method='POST', + query_args={'username': 'bar', 'password': 'baz'}, + url='http://foo/latest/bug/536619/attachment') + {'ref': 'http://foo/latest/attachment/1'} + """ + + pass + +def get_tests_in_module(module): + tests = [] + + loader = unittest.TestLoader() + suite = loader.loadTestsFromModule(module) + for test in suite: + tests.append(test) + + finder = doctest.DocTestFinder() + doctests = finder.find(module) + for test in doctests: + if len(test.examples) > 0: + tests.append(doctest.DocTestCase(test)) + + return tests + +def run_tests(verbosity=2): + module = __import__(__name__) + tests = get_tests_in_module(module) + suite = unittest.TestSuite(tests) + runner = unittest.TextTestRunner(verbosity=verbosity) + runner.run(suite) + +if __name__ == '__main__': + run_tests()