from typing import Protocol
class IWeb(Protocol):
"Interface for accessing internet resources"
def get(self, url:str, page:int, verbose:bool=False) -> str:
"Fetches the ressource at `url` and returns it in string representation"General structure
class RessourceFetcher:
base_url: str = "https://some_base.com/"
def __init__(self, web: IWeb):
self._web = web
def check_ressource(self, ressource_name: str, page:int, verbose:bool=False) -> bool:
url = self.base_url + ressource_name
ressource = self._web.get(url, page, verbose)
return ressource is not NoneThe general structure of pymoqs workflows is:
Setup
mock = Mock(IWeb)
mock.get\
.setup(ArbitrarySignatureValidator)\
.returns(ArbitraryReturnValueGenerator)In general, a SignatureValidator is a list of ArgumentValidator s.
An object following the ArgumentValidator protocol has, among other properties, a is_valid method that accepts a single argument and returns a bool, indicating if the given argument matches the expected conditions. Conditions might be a type check, a direct value check or something else entirely (e.g. “is it a string that starts with ‘py’?).
To make construcing a suitable list of ArgumentValidator s more convenient, there are a bunch of shortcuts for passing values to the setup method. They coded in argument_validator_from_argument. As of 2023-03-18, the shortcuts are:
- If an object is passed that satisfies the
ArgumentValidatorProtocol, it is used without any alteration - If a type is passed, an
ArgumentFunctionValidatoris constructed that matches against that type - If a Callable is passed, an
ArgumentFunctionValidatoris constructed that passes the argument through to the callable - In any other case, an
ArgumentFunctionValidatoris constructed that matches the argument against the passed value
In the example:
mock = Mock(IWeb)
mock.get\
.setup('https://some_base.com/ressource', int, False);- The first argument constructs a
ArgumentValidatorthat returns true iff the stringhttps://some_base.com/ressourceis passed (last case) - The second argument constructs a
ArgumentValidatorthat returns true iff the passed argument is of typeint(second case) - The third argument constructs a
ArgumentValidatorthat returns true iff the passed argument hast the valueFalse(last case)
The first argument could also be an arbitrary function evaluation like:
mock.get\
.setup(lambda arg: isinstance(arg, str) and arg.startswith('https'), int, False);This now matches against any arg that is of type string and starts with the substring https.
Special validators
The special validator AnyInt does a type check on int and can further be used to conveniently define int-specific validation rules, like greater_than or less_than_or_equal. See AnyInt for a list of availbe special validation rules.
Additionally AnyArg is available as a way to define that there is no restriction at all on the argument. This argument validator accepts any argument input.
Return Action
If the a call on a mock satisfies one of the setups, the corresponding return action is invoked:
mock.get\
.setup(ArbitrarySignatureValidator)\
.returns(ArbitraryReturnValueGenerator)The ArbitraryReturnValueGenerator is an object that follows the ReturnValueGenerator protocol. Essentially thats any callable. pymoq passes the arguments that were used in the specific call to the ReturnValueGenerator, enabling the user to return values depending on the concrete arguments used in each call.
To make constructing a ReturnValueGenerator more convenient, one can pass a non-callable object. pymoq constructs a ReturnValueGenerator from this that takes in any number of arguments and always returns that one value.
E.g.
mock.get\
.setup('https://some_base.com/ressource', int, False)\
.returns(True)
assert mock.get('https://some_base.com/ressource', 0, False)
assert mock.get('https://some_base.com/ressource', 1, False)will always return True (if the signature matches the validator in the setup function).
In contrast,
mock.get\
.setup('https://some_base.com/ressource', int, False)\
.returns(lambda self, url, page, verbose: page+1)
assert mock.get('https://some_base.com/ressource', 0, False) == 1
assert mock.get('https://some_base.com/ressource', 5, False) == 6will return page + 1, making the return value dependent on the caller value.
Return sequence
It’s possible to setup a sequence of return values. For each invocation that matches the signature validator, the next value of the sequence is returned. If the sequence is empty, None is returned.
mock = Mock(IWeb)
mock.get.setup('resource', int, bool).returns_sequence([1,2,3])
assert mock.get('resource', 1, True)==1
assert mock.get('resource', 2, False)==2
assert mock.get('resource', 3, True)==3
print(mock.get('ressource', 1, True))None
Return exceptions
A return action could also be the throwing of an exception:
class WebException(Exception):
"""Exception that describes web-access errors"""
mock = Mock(IWeb)
fetcher = RessourceFetcher(mock)
# setup failing web call
mock.get.setup('https://some_base.com/unavailable_ressource', int, bool).throws(WebException())
# act and assert exception
with pytest.raises(WebException):
fetcher.check_ressource('unavailable_ressource', 1, True)
# does not raise exception if call signature does not match
fetcher.check_ressource('available_ressource', 1, True);Verification
After the test method was run, one might want to verify how often certain invocations were done on the mock. The general structure for verification looks like:
mock.get.verify(ArbitrarySignatureValidator).ArbitraryQuantor()The SignatureValidator is the same as for the setup method, so look above for explanation. The Quantor defines how often a call was expected whose arguments match the SignatureValidator.
Available Quantors:
mock = Mock(IWeb)
fetcher = RessourceFetcher(mock)
# setup
mock.get.setup(str, int, bool).returns(True)
# act
fetcher.check_ressource('ressource', 1)
fetcher.check_ressource('ressource', 2)
fetcher.check_ressource('ressource', 1, verbose=True)
# assert
mock.get.verify(str, int, bool).times(3)
mock.get.verify(str, int, bool).more_than(1)
mock.get.verify(str, int, bool).more_than_or_equal_to(3)
mock.get.verify(str, int, bool).less_than(4)
mock.get.verify(str, int, bool).less_than_or_equal_to(3)
mock.get.verify(str, str).never()
mock.get.verify(str, AnyInt('page', 2).less_than(2), bool).times(2);It’s possible to inspect all calls that were done on the mock:
mock.get.verify(str, AnyInt('page', 2).less_than(2), bool).all_calls[((None, 'https://some_base.com/ressource', 1, False), {}),
((None, 'https://some_base.com/ressource', 2, False), {}),
((None, 'https://some_base.com/ressource', 1, True), {})]
Or only the calls that match the current signature validator:
mock.get.verify(str, AnyInt('page', 2).less_than(2), bool).verified_calls[((None, 'https://some_base.com/ressource', 1, False), {}),
((None, 'https://some_base.com/ressource', 1, True), {})]