changeset 7:7a04d148ead6

Added tests
author Atul Varma <avarma@mozilla.com>
date Tue, 13 Apr 2010 11:31:13 -0700
parents 548a4fe96851
children 993fc3b27263
files .hgignore minimock.py test_bugzilla.py
diffstat 3 files changed, 366 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- 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
--- /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
+        <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)
--- /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()