Home > Back-end >  How to spec a Python mock which defaults to AttributeError() without explicit assignment?
How to spec a Python mock which defaults to AttributeError() without explicit assignment?

Time:12-05

I have a question related to Python unittest.mock.Mock and spec_set functionalities.

My goal is to create a Mock with the following functionalities:

  1. It has a spec of an arbitrary class I decide at creation time.
  2. I must be able to assign on the mock only attributes or methods according to the spec of point 1
  3. The Mock must raise AttributeError in the following situations:
    • I try to assign an attribute that is not in the spec
    • I call or retrieve a property that is either missing in the spec_set, or present in the spec_set but assigned according to the above point.

Some examples of the behavior I would like:

class MyClass:
   property: int = 5
   def func() -> int:
       pass

# MySpecialMock is the Mock with the functionalities I am dreaming about :D
mock = MyMySpecialMock(spec_set=MyClass)

mock.not_existing # Raise AttributeError
mock.func() # Raise AttributeError
mock.func = lambda: "it works"
mock.func() # Returns "it works"

I have tried multiple solutions without any luck, or without being explicitly verbose. The following are some examples:

  1. Using Mock(spec_set=...), but it does not raise errors in case I call a specced attribute which I did not explicitly set
  2. Using Mock(spec_set=...) and explicitly override every attribute with a function with an Exception side effect, but it is quite verbose since I must repeat all the attributes...

My goal is to find a way to automatize 2, but I have no clean way to do so. Did you ever encounter such a problem, and solve it?

For the curious ones, the goal is being able to enhance the separation of unit testings; I want to be sure that my mocks are called only on the methods I explicitly set, to avoid weird and unexpected side effects.

Thank you in advance!

CodePudding user response:

spec_set defines a mock object which is the same as the class, but then doesn't allow any changes to be made to it, since it defines special __getattr__ and __setattr__. This means that the first test (calling a non-existent attr) will fail as expected, but then so will trying to set an attr:

from unitest import mock

class X:
    pass

m = mock.Mock(spec_set=X)
m.func()
# __getattr__: AttributeError: Mock object has no attribute 'func'
m.func = lambda: "it works"
# __setattr__: AttributeError: Mock object has no attribute 'func'

Instead, you can use create_autospec() which copies an existing function, and adds the mock functions to it, but without affecting __setattr__:

n = mock.create_autospec(X)
n.func()
# __getattr__: AttributeError: Mock object has no attribute 'func'
n.func = lambda: "it works"
n.func()
# 'it works'

CodePudding user response:

I think I found a satisfying answer to my problem, by using the dir method.

To create the Mock with the requirements I listed above, it should be enough to do the following:

def create_mock(spec: Any) -> Mock:
    mock = Mock(spec_set=spec)

    attributes_to_override = dir(spec)
    for attr in filter(lambda name: not name.startswith("__"), attributes_to_override):
        setattr(mock, attr, Mock(side_effect=AttributeError(f"{attr} not implemented")))

    return mock
  • Related