def f(a: int, b: str, c:str|None =None) -> None:
passMocking functions
Checking arguments against original signature
For a given argument list, we want to know whether the original function can be called with that set of parameters. This can be done in two steps: 1. Extract the signature 2. Try to bind the argument list against the signature. This throws an exception if the arguments can not be matched against the signature
Binding free methods
Successful bind:
inspect.signature(f).bind(1, "1", "2")<BoundArguments (a=1, b='1', c='2')>
Unsuccessful bind:
try:
inspect.signature(f).bind(1)
except Exception as e:
print(e)missing a required argument: 'b'
Binding class methods
When binding class methods, the special case of the self parameter has to be considered.
class A:
def f(self, a:int):
return a+1signature = inspect.signature(A.f)
try:
signature.bind(1)
except Exception as e:
print(e)missing a required argument: 'a'
We will mark a function as ‘class’-function if it has a parameter called ‘self’.
is_class_method
is_class_method (func:pymoq.core.AnyCallable)
Returns true if the given function has a parameter called ‘self’
assert is_class_method(A.f)
assert not is_class_method(f)remove_self_parameter
remove_self_parameter (args:tuple[typing.Any])
Removes the first parameter of the argument list.
add_self_parameter
add_self_parameter (args:tuple[typing.Any])
Adds None to the front of the given tuple of arguments
assert add_self_parameter((1,2))==(None,1,2)
assert remove_self_parameter((None, 1,2))==(1,2)
assert remove_self_parameter(add_self_parameter((1,2))) == (1,2)Function Mock
FunctionMock
FunctionMock (func:pymoq.core.AnyCallable)
Mocks a function object based on its signature
mock = FunctionMock(f)assert mock._argument_names == ['a', 'b', 'c']Successful binds:
assert mock.arguments_valid(1, "1", "2")
assert mock.arguments_valid(1, "1")
assert mock.arguments_valid(1, "1", c="2")
assert mock.arguments_valid(a=1, b="1", c="2")Note that the types are not checked with this:
assert mock.arguments_valid(1, 1)Unsuccessful binds might be:
def show_exception(func: AnyCallable):
try:
func()
except Exception as e:
print(e)
return
assert False, "Expected Exception to be thrown"show_exception(lambda: mock.arguments_valid(1))# too few argumentsmissing a required argument: 'b'
show_exception(lambda:mock.arguments_valid(1,2,3,4)) # too many argumentstoo many positional arguments
show_exception(lambda: mock.arguments_valid(1,2,d=3)) # unknown argument namegot an unexpected keyword argument 'd'
Special case class method. Note that when calling arguments_valid directly, we need to pass in a dummy self argument.
mock = FunctionMock(A.f)
assert mock.arguments_valid(None, 1)
show_exception(lambda: mock.arguments_valid(None))
show_exception(lambda: mock.arguments_valid(None, 1,2))missing a required argument: 'a'
too many positional arguments
Setup
We want to be able to create call-setups on the function mock. A setup consists of a signature validation and a return value generator. When the mock is called with a list of arguments, we check this list against the signature validator. If the call matches, we call the return value generator to generate the return value.
Setup
Setup (signature_validator:pymoq.signature_validators.SignatureValidator)
This class bundles a signature validator with a call-result-action
Setup.returns
Setup.returns (return_value_generator:Any)
Set the ReturnValueGenerator to be called when this setup is successfully called
FunctionMock.setup
FunctionMock.setup (*args, **kwargs)
A setup can now be defined by passing a function that takes in the call parameters and outputs the appropriate value:
mock = FunctionMock(f)
mock.setup(ArgumentFunctionValidator(lambda a: isinstance(a, int), name='a', position=0)).returns(lambda first, **kwargs: first+1)
assert mock._setups[0].get_return_value(4) == 5assert mock._setups[0].get_return_value(4)==5
assert mock._setups[0].get_return_value(4, a=1)==5assert mock._setups[0].is_valid(1)
assert not mock._setups[0].is_valid("1")or by passing a constant, which is then returned regardless of the call values:
mock = FunctionMock(f)
mock.setup(ArgumentFunctionValidator(lambda a: isinstance(a, int), name='a', position=0)).returns(5)
assert mock._setups[0].get_return_value() == 5self argument on class methods
class A:
def f(self, a: int) -> None:
passmock = FunctionMock(A.f)
mock.setup(lambda a: isinstance(a, int)).returns(lambda self, a: 5)
assert mock._setups[0].is_valid(None, 1)mock._setups[0]._signature_validator.argument_validators[ArgumentFunctionValidator(argument_name:self, position=0): callable(),
ArgumentFunctionValidator(argument_name:a, position=1): callable()]
Call validation
When called, a function mock should perform the following steps: 1. check if the argument list binds against the original functions signature 2. check if the signature validator matches for the stored Setups in order they were added 3. The first setup with a matching signature validator should be used for generating the return value
Edge cases: - If no Setup matches, return None
Dealing with default-values
The number of arguments passed to a function call might differ from call to call if default values are used:
def f(a:int, b:str="str"):
...
mock = FunctionMock(f)
mock.setup(ArgumentFunctionValidator(lambda a: True, name='a', position=0), ArgumentFunctionValidator(lambda b: True, name='b', position=1)).returns(lambda *args,**kwargs: print(args, kwargs))mock._setups[0].get_return_value(1), mock._setups[0].get_return_value(1, b="b")(1,) {}
(1,) {'b': 'b'}
(None, None)
Next, we’ll match an argument list against the default values.
FunctionMock.fill_up_arg_list
FunctionMock.fill_up_arg_list (args:list[typing.Any], kwargs:dict[str,typing.Any], verbose:bool=False)
If a default-value is already present via name, the default value should not be used:
def f(a: int, b: str, c:str|None ='default str') -> None:
pass
mock = FunctionMock(f)
assert mock.fill_up_arg_list([1, 1.1], {'c': 'custom str'}) == {'c': 'custom str'}If a default-value is already overriden by a positional argument, it should not be used:
assert mock.fill_up_arg_list([1, 1.1, 'custom str'], {}) == {}If neither is the case, the default-value should be used:
assert mock.fill_up_arg_list([1, 1.1], {}) == {'c': 'default str'}Call method
Calls on a mock should be recorded. This makes it possible to unit-test that specific argument combinations were called a specific amount of time.
def f(a: int, b: str, c:str|None =None) -> None:
pass
mock = FunctionMock(f)Without any setup a call either fails or returns None:
test_fail(lambda: mock())
assert mock(1,"1") is NoneNow we’ll add two setups. The first one is the more generic one that only checks if a is an int and b is a string. The second one checks if a==2 and b is a string.
# this will be prettier, I promise!
mock = FunctionMock(f)
# Generic type checker
mock.setup(
ArgumentFunctionValidator(lambda a: isinstance(a, int), name='a', position=0),
ArgumentFunctionValidator(lambda b: isinstance(b, str), name='b', position=1),
ArgumentFunctionValidator(lambda c: c is None, name='c', position=2)).returns(lambda a,b,c: 5)
# Value checker
mock.setup(
ArgumentFunctionValidator(lambda a: a==2, name='a', position=0),
ArgumentFunctionValidator(lambda b: isinstance(b, str), name='b', position=1),
ArgumentFunctionValidator(lambda c: c is None, name='c', position=2)).returns(lambda a,b,c: 6)
assert mock(2, 'anyString')==6
assert mock(1, 'anyString')==5
assert mock(1, 1) is None
assert mock._calls == [
((2, 'anyString'), {'c': None}),
((1, 'anyString'), {'c': None}),
((1, 1), {'c': None})
]Lastly, the generic return value generator could be used to return a value based on the input value:
mock = FunctionMock(f)
# Generic type checker
mock.setup(
ArgumentFunctionValidator(lambda a: isinstance(a, int), name='a', position=0),
ArgumentFunctionValidator(lambda b: isinstance(b, str), name='b', position=1),
ArgumentFunctionValidator(lambda c: c is None, name='c', position=2)).returns(lambda a,b,c: a+1)
assert mock(1,'anyString')==2
assert mock(2,'anyString')==3On class methods
class A:
def f(self, a: int, b: str, c:str|None =None) -> None:
passmock = FunctionMock(A.f)test_fail(lambda: mock())
assert mock(1, "1", "c") is None# this will be prettier, I promise!
mock = FunctionMock(A.f)
# Generic type checker
mock.setup(
ArgumentFunctionValidator(lambda a: isinstance(a, int), name='a', position=1),
ArgumentFunctionValidator(lambda b: isinstance(b, str), name='b', position=2),
ArgumentFunctionValidator(lambda c: c is None, name='c', position=3)).returns(lambda self,a,b,c: 5)
# Value checker
mock.setup(
ArgumentFunctionValidator(lambda a: a==2, name='a', position=1),
ArgumentFunctionValidator(lambda b: isinstance(b, str), name='b', position=2),
ArgumentFunctionValidator(lambda c: c is None, name='c', position=3)).returns(lambda self,a,b,c: 6)
assert mock(2, 'anyString')==6
assert mock(1, 'anyString')==5
assert mock(1, 1) is NoneNamed arguments in setup
class A:
def f(self, a: int, b: str, c:str|None =None) -> None:
pass# this will be prettier, I promise!
mock = FunctionMock(A.f)
# Generic type checker
mock.setup(
ArgumentFunctionValidator(lambda a: isinstance(a, int), name='a', position=1),
ArgumentFunctionValidator(lambda b: isinstance(b, str), name='b', position=2),
c=ArgumentFunctionValidator(lambda c: c is None, name='c', position=3)).returns(lambda self,a,b,c: 5)
# Value checker
mock.setup(
ArgumentFunctionValidator(lambda a: a==2, name='a', position=1),
ArgumentFunctionValidator(lambda b: isinstance(b, str), name='b', position=2),
c=ArgumentFunctionValidator(lambda c: c is None, name='c', position=3)).returns(lambda self,a,b,c: 6)
assert mock(2, 'anyString')==6
assert mock(1, 'anyString')==5
assert mock(1, 1) is None