Mocking objects

Mocking an object with all its (public) attributes and methods

Public names

Since the main purpose of pymoq is to mock interfaces (aka protocols), we need a way to extract the public members of an interface. By convention, public members should not start with an underscore.

Methods

All methods whose name doesn’t start with an underscore should be included. The only public method in the following class is get.

class IWeb(Protocol):    
    def get(self, suffix:str) -> str:
        ...
    
    def _internal_stuff(self) -> None:
        ...

All names are exposed through the dir method…

', '.join(dir(IWeb))
'__abstractmethods__, __annotations__, __class__, __class_getitem__, __delattr__, __dict__, __dir__, __doc__, __eq__, __format__, __ge__, __getattribute__, __gt__, __hash__, __init__, __init_subclass__, __le__, __lt__, __module__, __ne__, __new__, __parameters__, __reduce__, __reduce_ex__, __repr__, __setattr__, __sizeof__, __slots__, __str__, __subclasshook__, __weakref__, _abc_impl, _internal_stuff, _is_protocol, _is_runtime_protocol, get'

… which can be filtered for names that don’t start with an underscore:


source

get_public_names

 get_public_names (protocol:type)

Returns all names that are considered public from the given class

assert get_public_names(IWeb)==['get']

Attributes

Attributes defined in protocol classes are not directly stored in the dir list. Instead, they are accessible in __annotations__. Note that protocol variables have to be defined at class level, not inside the __init__ method.

class IStore(Protocol):
    store_id: int
    name: str
    _internal_key: int
    
    def get(self, name:str) -> int:
        ...
IStore.__annotations__
{'store_id': int, 'name': str, '_internal_key': int}

source

get_public_attributes

 get_public_attributes (protocol:<class'_ProtocolMeta'>)

Return a list of all attributes of the given protocol that are considered public.

get_public_attributes(IStore)
['store_id', 'name']
assert 'store_id' in get_public_attributes(IStore)
assert 'name' in get_public_attributes(IStore)
assert not '_internal_key' in get_public_attributes(IStore)

Construction from Protocol

Dynamic attribute access

Dynamic attribute access is possible by overriding the special method __getattr__. This method is called when a name is not found in the current instance of the class.

class Outer:
    def __init__(self):
        self.valid = 2
        self.values = {'inner': 1}
        
    def __getattr__(self, name: str):
        print(f'Calling __getattr__("{name}")')
        if name in self.values:
            return self.values[name]
        raise AttributeError(f'Name {name} not found in values dictionary')
        
o = Outer()

Attribute is present in the class instance, so its accessed directly:

print(o.valid)
2

Attribute is not present in the class instance, so __getattr__ is called:

print(o.inner)
Calling __getattr__("inner")
1
try:
    o.invalid
except Exception as e:
    print(type(e), e)
Calling __getattr__("invalid")
<class 'AttributeError'> Name invalid not found in values dictionary

The Mock object

The Mock object is the central class that the user of pymoq will interact with. It should be initialized with a protocol, setup function-mocks for all protocol-methods and handle the call redirection to the correct mock.


source

Mock

 Mock (protocol:<class'_ProtocolMeta'>)

Initialize self. See help(type(self)) for accurate signature.

mock = Mock(IWeb)
assert str(mock) == 'Mock[IWeb]'

assert list(mock._function_mocks.keys()) == ['get']

mock._function_mocks
{'get': <pymoq.mocking.functions.FunctionMock>}

When a function is called on a Mock, it should check whether that function is part of the underlyings protocol public interface. If not, throw an AttributeError. If yes, return the appropriate function mock.

mock = Mock(IWeb)

test_fail(lambda: mock.not_a_name)
assert isinstance(mock.get, FunctionMock)

With this we can now build a working prototype of a mocked protocol.

mock = Mock(IWeb)

mock.get.setup(
    ArgumentFunctionValidator(lambda a: isinstance(a, str), name='suffix', position=1)).returns(lambda self,suffix: f'suffix: {suffix}')
assert mock.get('anyString') == 'suffix: anyString'
assert mock.get(1) is None
test_fail(lambda: mock.get())

Build library