Reputation: 19181
When I invoke mock.patch I expect it to replace the type I am replacing with the type I provided using the new keyword argument.
It does not replace the type but it does return the correct object when patch.start()
is invoked.
The FakesPatcher
is a hack that forces the old object to create the new object. It works for Python 3.x and PyPy. However, it doesn't work for Python 2.x.(See edit below).
I want the FakesPatcher
to go away anyway and use mock.patch
instead.
What's am I doing wrong here and how can I fix it?
def substitute(obj, qualified_name, spec):
testdouble = mock.patch(qualified_name, spec=spec, spec_set=True, new=obj)
testdouble.attribute_name = qualified_name # Forces patch to use the extra patcher
class FakesPatcher(object):
"""Ugly hack."""
new = 1
def _new(*args, **kwargs):
return obj.__new__(obj)
def __enter__(self):
self._old_new = spec.__new__
spec.__new__ = self._new
return obj
def __exit__(self, exc_type, exc_val, exc_tb):
spec.__new__ = self._old_new
testdouble.additional_patchers.append(FakesPatcher())
return testdouble
def fake(obj):
"""
:rtype : mock._patch
:param obj:
"""
try:
configuration = obj.Configuration()
except AttributeError:
raise TypeError('A fake testdouble must have a Configuration class.')
try:
spec = configuration.spec
except AttributeError:
raise TestDoubleConfigurationError('The type to be faked was not specified.')
qualified_name = get_qualified_name(spec)
attrs = dict(obj.__dict__)
attrs.pop('Configuration')
methods = get_missing_methods(spec, obj)
for method in methods:
def make_default_implementation(attr):
def default_implementation(*args, **kwargs):
raise NotImplementedError('%s was not implemented when the object was faked.' % attr)
return default_implementation
attrs.update({method: make_default_implementation(method)})
properties = get_missing_properties(spec, obj)
for prop in properties:
def make_default_implementation(attr):
def default_implementation(*args, **kwargs):
raise NotImplementedError('%s was not implemented when the object was faked.' % attr)
return property(fget=lambda *args, **kwargs: default_implementation(*args, **kwargs),
fset=lambda *args, **kwargs: default_implementation(*args, **kwargs),
fdel=lambda *args, **kwargs: default_implementation(*args, **kwargs))
attrs.update({prop: make_default_implementation(prop)})
fake_qualified_name = get_qualified_name(obj)
obj = type(obj.__name__, obj.__bases__, attrs)
return substitute(obj, qualified_name, spec)
In case you want to play with the code and test it you can find it here.
EDIT:
I solved the Python 2.x errors by replacing the lambda with an instance method.
Upvotes: 2
Views: 365
Reputation: 55752
In your tests, if you want to use mock.patch
in a with
statement, the mock library requires you that you use the return value of the patch as the mock object. Your test now become
@it.should('replace the original methods with the fake methods')
def test_should_replace_the_original_methods_with_the_fake_methods(case):
class FakeObject(object):
class Configuration(object):
spec = RealObject
def was_faked(self):
return True
with fake(FakeObject) as realObject:
fake_obj = realObject()
case.assertTrue(fake_obj.was_faked())
You can then use the following substitute or even get rid of it.
def substitute(obj, qualified_name, spec):
return mock.patch(qualified_name, new=obj, spec=spec)
Patching works by patching types at the calling site. The following excerpt from the documentation is important.
target should be a string in the form ‘package.module.ClassName’. The target is imported and the specified object replaced with the new object, so the target must be importable from the environment you are calling patch from. The target is imported when the decorated function is executed, not at decoration time.
If you want to patch the actual type, without using the return value with the with
statement, you must not resolve the name of the class to a qualified name but local name.
The following changes
@it.should('replace the original methods with the fake methods')
def test_should_replace_the_original_methods_with_the_fake_methods(case):
...
with fake(FakeObject, '%s.%s' % (__name__,'RealObject')):
fake_obj = RealObject()
case.assertTrue(fake_obj.was_faked())
testdoubles__init__.py
def fake(obj, qualified_name=None):
"""
:rtype : mock._patch
:param obj:
"""
try:
configuration = obj.Configuration()
except AttributeError:
raise TypeError('A fake testdouble must have a Configuration class.')
try:
spec = configuration.spec
except AttributeError:
raise TestDoubleConfigurationError('The type to be faked was not specified.')
qualified_name = qualified_name or get_qualified_name(spec)
...
Now the problem is that you can't reliably find out where RealObject is coming from, at least I couldn't really find a way. You could assume that it is from the module where the calling function reside and do:
qualified_name = "%s.%s" % (obj.__module__, spec.__name__)
Upvotes: 1