Pregunta

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.

¿Fue útil?

Solución

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__)
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top