Source code for fakeable

# -*- coding: utf-8 -*-

# Copyright 2013 Denver Coneybeare
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Provides a mechanism by which classes can produce "fake" instances during unit
tests and real instances when not under test.
"""

from __future__ import print_function
from __future__ import unicode_literals

__all__ = [
    "Fakeable",
    "set_fake_class",
    "set_fake_object",
    "unset",
    "clear",
    "add_created_callback",
    "remove_created_callback",
    "FakeableCleanupMixin",
]

__version__ = "1.0.4-dev"


[docs]class Fakeable(type): """ A metaclass to be used by types that wish to be fakeable. In order to make a class fakeable, all that needs to be done is set *fakeable.Fakeable* as its metaclass. In Python 2, this done by setting the special attribute ``__metaclass__ = fakeable.Fakeable`` in the class definition. In Python 3, this is done by specifying ``metaclass=fakeable.Fakeable`` in the base class list of the class definition. Python 2 example:: class HttpDownloader(object): __metaclass__ = fakeable.Fakeable ... Python 3 example:: class HttpDownloader(metaclass=fakeable.Fakeable): ... If you have the third-party ``six`` module installed, then you can do this in a way that works in both Python 2 and Python 3:: class HttpDownloader(six.with_metaclass(fakeable.Fakeable)): ... The name used in the *fakeable* module functions to refer to this class is simply the name of the class. This value is stored in the ``__FAKE_NAME__`` attribute of the class. If a class explicitly defines a ``__FAKE_NAME__`` attribute then that value will be used instead of the default. """ def __new__(mcs, name, bases, dict_): # use the class name as the "name" of the fake, but allow the class # to override this name by setting __FAKE_NAME__ try: __FAKE_NAME__ = dict_["__FAKE_NAME__"] except KeyError: dict_["__FAKE_NAME__"] = name else: # ensure that the __FAKE_NAME__ attribute of the class is hashable hash(__FAKE_NAME__) # create the type object with the possibly-slightly-modified dict type_ = type.__new__(mcs, name, bases, dict_) return type_ def __call__(cls, *args, **kwargs): fake_name = cls.__FAKE_NAME__ # try looking up the fake object by the class first try: instance = FAKE_FACTORY.get(cls, *args, **kwargs) except FAKE_FACTORY.FakeNotFound: pass else: FAKE_FACTORY.notify_fakeable_created(fake_name, instance, cls) return instance # try looking up the fake object by name try: instance = FAKE_FACTORY.get(fake_name, *args, **kwargs) except FAKE_FACTORY.FakeNotFound: pass else: FAKE_FACTORY.notify_fakeable_created(fake_name, instance, cls) return instance # no fake instance was registered; create a real instance instance = type.__call__(cls, *args, **kwargs) FAKE_FACTORY.notify_fakeable_created(fake_name, instance, cls) return instance
class FakeFactory(object): """ A database of fake objects. """ def __init__(self): self.fake_factories = {} self.fakeable_created_callbacks = [] def set_fake_class(self, name, value): """ See module-level set_fake_class() function for full documentation """ entry = FakeClassEntry(self, name, value) self.fake_factories[name] = entry return entry def set_fake_object(self, name, value): """ See module-level set_fake_object() function for full documentation """ entry = FakeObjectEntry(self, name, value) self.fake_factories[name] = entry return entry def unset(self, name): """ See module-level unset() function for full documentation """ try: del self.fake_factories[name] except KeyError: return False else: return True def clear(self): """ See module-level clear() function for full documentation """ self.fake_factories.clear() self.fakeable_created_callbacks = [] def add_created_callback(self, callback): """ See module-level add_created_callback() function for full documentation """ self.fakeable_created_callbacks.append(callback) def remove_created_callback(self, callback): """ See module-level remove_created_callback() function for full documentation """ try: self.fakeable_created_callbacks.remove(callback) except ValueError: return False else: return True def notify_fakeable_created(self, name, obj, obj_type): """ Notifies all callbacks about an instance of a fakeable class being created. This method is not normally invoked directly, but rather is invoked by the :class:`~fakeable.Fakeable` metaclass when it is requested to create a new object. The arguments are exactly those to specify to the callbacks registered via :meth:`add_created_callback` so see the documentation for that method for details. """ for callback in self.fakeable_created_callbacks: callback(name, obj, obj_type) def get(self, name, *args, **kwargs): """ Gets or creates the fake object for a class. This method is the one used by the :class:`fakeable.Fakeable` metaclass to create the fake instances. It should not normally be invoked directly. Arguments: *name* (string or :class:`fakeable.Fakeable` instance) the name of the class, or the class itself, whose fake object to get or create; if a string, this will be the name of the class or, if the class defines __FAKE_NAME__, the value of that class' __FAKE_NAME__ attribute. Returns the fake object for the class with the given name. Raises self.FakeNotFound if no fake was registered with the given name. """ try: entry = self.fake_factories[name] except KeyError: raise self.FakeNotFound() else: instance = entry.get(*args, **kwargs) return instance class FakeNotFound(Exception): """ Exception raised by get() if a fake is not found to be registered with the name that it is given. """ pass class FakeEntry(object): """ An entry in the fake factory. This is an abstract base class, and must be subclassed and the get() method overridden to be meaningful. """ def __init__(self, fake_factory, name): self.fake_factory = fake_factory self.name = name def unregister(self): """ Invokes self.fake_factory.unset(self.name). """ self.fake_factory.unset(self.name) def get(self, *args, **kwargs): """ Must be implemented by subclasses to get or create the fake object. The given arguments are those that were specified to the class constructor. """ raise NotImplementedError("must be implemented by a subclass") def __enter__(self): pass def __exit__(self, exc_type, exc_val, exc_tb): self.unregister() class FakeObjectEntry(FakeEntry): """ An entry in the fake factory where a predefined object is returned when instances of the class are created. """ def __init__(self, fake_factory, name, value): super(FakeObjectEntry, self).__init__(fake_factory, name) self.value = value def get(self, *args, **kwargs): return self.value class FakeClassEntry(FakeEntry): """ An entry in the fake factory where instances of a different class are to be created and returned when instances of the class are created. """ def __init__(self, fake_factory, name, value): super(FakeClassEntry, self).__init__(fake_factory, name) self.value = value def get(self, *args, **kwargs): instance = self.value(*args, **kwargs) return instance # the global FakeFactory instance FAKE_FACTORY = FakeFactory()
[docs]def set_fake_class(name, value): """ Configures the class with the given name to create fake objects instead of real objects when created. When an instance of the class of the given name is created a new instance of the given type will be created instead. Arguments: *name* (string or :class:`fakeable.Fakeable`) the name of the class, or the class itself, that will have fake instances created instead of real instances; if a string, this will be the name of the class or, if the class defines __FAKE_NAME__, the value of that class' __FAKE_NAME__ attribute. *value* (class) the class whose instances will be created in place of the real class; whatever arguments were given to the __init__() method of the real class will be passed on to the __init__() method of the created instance of this class. Returns a context manager that can be used as the target of a "with" statement; when the context of the "with" statement is exited the fake class will be automatically unregistered by a call to self.unset(name). """ FAKE_FACTORY.set_fake_class(name, value)
[docs]def set_fake_object(name, value): """ Configures the class with the given name to always use a fake object instead of real objects when created. When an instance of the class of the given name is created the given value will be returned instead. Arguments: *name* (string or :class:`fakeable.Fakeable`) the name of the class, or the class itself, that will have fake instances created instead of real instances; if a string, this will be the name of the class or, if the class defines __FAKE_NAME__, the value of that class' __FAKE_NAME__ attribute. *value* (object) the object to be returned in place of a new instance of the real class; whatever arguments were given to the __init__() method of the real class will be discarded. Returns a context manager that can be used as the target of a "with" statement; when the context of the "with" statement is exited the fake object will be automatically unregistered by a call to self.unset(name). """ FAKE_FACTORY.set_fake_object(name, value)
[docs]def unset(name): """ Unregisters a fake that was registered by a previous invocation of set_fake_object() or set_fake_class(). Arguments: *name* (string or :class:`fakeable.Fakeable`) the name of the class, or the class itself, whose fake is to be unregistered; if a string, this will be the name of the class or, if the class defines __FAKE_NAME__, the value of that class' __FAKE_NAME__ attribute. Returns True if a fake was indeed registered with the given name and was successfully unregistered. Returns False if a fake was *not* registered with the given name and therefore this function did nothing. """ FAKE_FACTORY.unset(name)
[docs]def add_created_callback(callback): """ Registers a callback to be invoked each time an instance of a :class:`~fakeable.Fakeable` class is created. The given callback will invoked each time that a class with the :class:`~fakeable.Fakeable` metaclass is created, whether it returns a fake object or a new instance of the real class. This can be useful during unit testing to examine the objects created by a third-party after the fact. No checking for duplicate callback registrations is performed; therefore, if a given callback is registered twice then it will be invoked twice each time that an instance of a :class:`~fakeable.Fakeable` class is created. Callbacks are invoked synchronously, and in the order in which they are added. No exception handling is performed around the callbacks; therefore, if a callback raises an exception it will trickle up the call stack until it is either caught or falls of the end, aborting the program. This will also prevent the other callbacks from receiving the notification. Callbacks may be unregistered by :func:`~fakeable.remove_created_callback` (to unregister a specific callback) or :func:`~fakeable.clear` (to unregister *all* callbacks). Arguments: *callback* (function) a function that will be invoked each time an instance of a :class:`fakeable.Fakeable` class is created; this function must accept the arguments documented below. The arguments of the callback function are: *name* (string) the name of the fakeable type, which is normally a string, and will be equal to the ``__FAKE_NAME__`` attribute of the :class:`~fakeable.Fakeable` class. *obj* (an object) the object that was returned by the request for a new instance of the :class:`~fakeable.Fakeable` class; this will be a new instance of the :class:`~fakeable.Fakeable` class if no fake object is registered or the fake object if one is. *obj_type* (class object) the class object of the :class:`~fakeable.Fakeable` class. """ FAKE_FACTORY.add_created_callback(callback)
[docs]def remove_created_callback(callback): """ Unregisters a callback that was registered by a previous invocation of :func:`~fakeable.add_created_callback`. If the given callback is registered more than once then only one of its registrations will be removed. Therefore, to fully unregister a callback there must be one call to this function for each call to :func:`~fakeable.add_created_callback`. Arguments: *callback* (function) the callback to remove; this must be the exact object that was specified to add_fake_created_callback() for the "callback" argument Returns True if the given callback was found in the list of registered callbacks and was removed; returns False if the given callback was *not* found in the list of registered callbacks and therefore this method did nothing. """ return FAKE_FACTORY.remove_created_callback(callback)
[docs]def clear(): """ Unregisters all fake objects that have been previously registered and all callbacks that have been registered via add_created_callback(). """ FAKE_FACTORY.clear()
[docs]class FakeableCleanupMixin(object): """ A convenience class that can be inherited by unit test classes so that fakes are automatically cleaned up before and after each test runs. This helps prevent fake objects inadvertently leaking into other test cases, which can cause difficult-to-diagnose behaviour when it happens. This class is intended to be subclassed by other classes that also inherit from ``unittest.TestCase``. It defines :meth:`~fakeable.FakeableCleanupMixin.setUp` and :meth:`~fakeable.FakeableCleanupMixin.tearDown`, both of which simply invoke :func:`fakeable.clear` and the method of the same name in the superclass. *Example*: using this "mixin" class in a ``unittest.TestCase``:: class TestSomething(fakeable.FakeableCleanupMixin, unittest.TestCase): def test(self): fakeable.set_fake_class("Something", FakeSomething) self.assertTrue(do_something()) Before the method ``test()`` is executed the ``setUp()`` method will invoke :func:`fakeable.clear` to make sure there are no leftover fakes that were registered elsewhere. Also, after the ``test()`` method completes, ``tearDown()`` will invoke :func:`fakeable.clear` to make sure that none of the registered fakes are left behind to screw things up downstream. Note, however, that ``fakeable.FakeableCleanupMixin`` *must* occur before ``unittest.TestCase`` in the base class list. Otherwise, the ``setUp()`` and ``tearDown()`` methods of ``FakeableCleanupMixin`` will not be invoked. *Bad Example (of specifying base classes of a test case)*:: # BAD!! -- because unittest.TestCase is specified *before* # fakeable.FakeableCleanupMixin in the base class list, # the setUp() and tearDown() methods of FakeableCleanupMixin will not # be invoked, defeating the purpose of adding it as a base class class TestSomething(unittest.TestCase, fakeable.FakeableCleanupMixin): ... *Good Example (of specifying base classes of a test case)*:: # GOOD -- because unittest.TestCase is specified *after* # fakeable.FakeableCleanupMixin in the base class list, # the setUp() and tearDown() methods of FakeableCleanupMixin *will* # be invoked, properly unregistering all registered fakes class TestSomething(fakeable.FakeableCleanupMixin, unittest.TestCase): ... If the ``FakeableCleanupMixin`` subclass also wants to override ``setUp()`` and/or ``tearDown()``, be sure to use the ``super()`` built-in to call the method of the same name in the superclass (as opposed to naming the superclass and calling it directly). Using ``super()`` ensures that the method-resolution order ("mro") will be used and each superclass' method will be invoked in the correct order. *Bad Example (of overriding the setUp() method)*:: class TestSomething(fakeable.FakeableCleanupMixin, unittest.TestCase): def setUp(self): # BAD!! -- does not respect the MRO and some superclass setUp() # methods may not be invoked fakeable.FakeableCleanupMixin.setUp(self) ... *Good Example (of overriding the setUp() method)*:: class TestSomething(fakeable.FakeableCleanupMixin, unittest.TestCase): def setUp(self): # GOOD -- respects the MRO and ensures that the setUp() method # of each superclass is invoked and in the correct order super(TestSomething, self).setUp() ... """
[docs] def setUp(self): """ Invokes the ``setUp`` method of the superclass, and then :func:`fakeable.clear`. This ensures that fakes that have been previously set do not leak into tests. """ super(FakeableCleanupMixin, self).setUp() clear()
[docs] def tearDown(self): """ Invokes :func:`fakeable.clear` and then the ``tearDown()`` method of the superclass. This ensures that fakes that have been set in tests do not leak into other tests. It also precludes the need to explicitly unregister fake objects. """ try: clear() finally: super(FakeableCleanupMixin, self).tearDown()

Project Versions

This Page