Skip to content

Commit

Permalink
Ensure that type_validation parameter on mock_callable/mock_async_cal…
Browse files Browse the repository at this point in the history
…lable methods has precedence over StrictMock's type_validation parameter

This allows to have a StrickMock instance to always ignore type_validation but also allow a StrickMock to always enforce type validation and allowing very specific calls to ignore validation.
This can be handy when you want to write a test that checks conditions that should be not possible from a typing prospective but that you still want to have as extra defensive strategy.
  • Loading branch information
macisamuele committed Mar 20, 2021
1 parent 0b9bf1d commit fc2d2c1
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 19 deletions.
6 changes: 2 additions & 4 deletions pytest-testslide/tests/test_pytest_testslide.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ async def test_mock_async_callable_assertion_works(testslide):
def test_mock_async_callable_failing_assertion_works(testslide):
testslide.mock_async_callable(sample_module.ParentTarget, "async_static_method").for_call("a", "b").to_call_original().and_assert_called_once()
# mock_constructor integration test
def test_mock_constructor_patching_works(testslide):
testslide.mock_constructor(sample_module, "ParentTarget").to_raise(RuntimeError("Mocked!"))
Expand Down Expand Up @@ -115,7 +114,6 @@ def test_patch_attribute_unpatching_works(testslide):
# not happen
assert sample_module.SomeClass.attribute == "value"
def test_aggregated_exceptions(testslide):
mocked_cls = StrictMock(sample_module.CallOrderTarget)
testslide.mock_callable(mocked_cls, 'f1')\
Expand All @@ -131,7 +129,7 @@ def test_aggregated_exceptions(testslide):
assert "passed, 4 errors" in result.stdout.str()
assert "failed" not in result.stdout.str()
expected_failure = re.compile(
""".*_______ ERROR at teardown of test_mock_callable_failing_assertion_works ________
r""".*_______ ERROR at teardown of test_mock_callable_failing_assertion_works ________
1 failures.
<class \'AssertionError\'>: calls did not match assertion.
\'time\', \'sleep\':
Expand Down
49 changes: 49 additions & 0 deletions tests/strict_mock_testslide.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
UnsupportedMagic,
)

from . import sample_module


def extra_arg_with_wraps(f):
@functools.wraps(f)
Expand Down Expand Up @@ -1196,3 +1198,50 @@ def it_trims_prefix(self):
str(self.strict_mock),
)
)

@context.sub_context
def check_return_type_validation(context):
@context.shared_context
def run_context(context, target):
@context.example
def default_validation_at_mock_callable_level(self):
self.mock_callable(target, "instance_method").to_return_value(1)

if isinstance(target, StrictMock) and not target._type_validation:
target.instance_method(arg1="", arg2="")
else:
with self.assertRaises(TypeCheckError):
target.instance_method(arg1="", arg2="")

@context.example
def enforce_validation_at_mock_callable_level(self):
self.mock_callable(
target, "instance_method", type_validation=True
).to_return_value(1)

with self.assertRaises(TypeCheckError):
target.instance_method(arg1="", arg2="")

@context.example
def ignore_validation_at_mock_callable_level(self):
self.mock_callable(
target, "instance_method", type_validation=False
).to_return_value(1)
target.instance_method(arg1="", arg2="")

@context.sub_context
def using_concrete_instance(context):
context.merge_context("run context", target=sample_module.Target())

@context.sub_context
def using_strict_mock(context):
context.merge_context(
"run context", target=StrictMock(sample_module.ParentTarget)
)

@context.sub_context
def using_strict_mock_with_disabled_type_validation(context):
context.merge_context(
"run context",
target=StrictMock(sample_module.ParentTarget, type_validation=False),
)
7 changes: 7 additions & 0 deletions testslide/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,16 @@ def with_sig_and_type_validation(*args: Any, **kwargs: Any) -> Any:
with_sig_and_type_validation.__qualname__ = "TestSldeValidation({})".format(
with_sig_and_type_validation.__qualname__
)
setattr( # noqa: B010
with_sig_and_type_validation, "__is_testslide_type_validation_wrapping", True
)
return with_sig_and_type_validation


def _is_wrapped_for_signature_and_type_validation(value: Callable) -> bool:
return getattr(value, "__is_testslide_type_validation_wrapping", False)


def _validate_return_type(
template: Union[Mock, Callable], value: Any, caller_frame_info: Traceback
) -> None:
Expand Down
49 changes: 36 additions & 13 deletions testslide/mock_callable.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,15 @@


def mock_callable(
target: Any, method: str, allow_private: bool = False, type_validation: bool = True
target: Any,
method: str,
allow_private: bool = False,
# type_validation accepted values:
# * None: type validation will be enabled except if target is a StrictMock
# with disabled type validation
# * True: type validation will be enabled (regardless of target type)
# * False: type validation will be disabled
type_validation: Optional[bool] = None,
) -> "_MockCallableDSL":
caller_frame = inspect.currentframe().f_back # type: ignore
# loading the context ends up reading files from disk and that might block
Expand Down Expand Up @@ -543,30 +551,42 @@ def __init__(
method: str,
caller_frame_info: Traceback,
is_async: bool = False,
type_validation: bool = True,
# type_validation accepted values:
# * None: type validation will be enabled except if target is a StrictMock
# with disabled type validation
# * True: type validation will be enabled (regardless of target type)
# * False: type validation will be disabled
type_validation: Optional[bool] = None,
) -> None:
self.target = target
self.method = method
self.runners: List[_BaseRunner] = []
self.is_async = is_async
self.type_validation = type_validation
self.type_validation = type_validation or type_validation is None
self.caller_frame_info = caller_frame_info

if type_validation is None and isinstance(target, StrictMock):
# If type validation is enabled on the specific call
# but the StrictMock has type validation disabled then
# type validation should be disabled
self.type_validation = target._type_validation

def _get_runner(self, *args: Any, **kwargs: Any) -> Any:
for runner in self.runners:
if runner.can_accept_args(*args, **kwargs):
return runner
return None

def _validate_return_type(self, runner: _BaseRunner, value: Any) -> None:
if (
self.type_validation
and runner.TYPE_VALIDATION
and runner.original_callable is not None
):
_validate_return_type(
runner.original_callable, value, self.caller_frame_info
)
if self.type_validation and runner.TYPE_VALIDATION:
if runner.original_callable is not None:
_validate_return_type(
runner.original_callable, value, self.caller_frame_info
)
elif isinstance(runner.target, StrictMock):
_validate_return_type(
getattr(runner.target, runner.method), value, self.caller_frame_info
)

def __call__(self, *args: Any, **kwargs: Any) -> Optional[Any]:
runner = self._get_runner(*args, **kwargs)
Expand Down Expand Up @@ -730,7 +750,10 @@ def _patch(
original_callable = getattr(self._target, self._method)

new_value = _wrap_signature_and_type_validation(
new_value, self._target, self._method, self.type_validation
new_value,
self._target,
self._method,
self.type_validation or self.type_validation is None,
)

restore = self._method in self._target.__dict__
Expand Down Expand Up @@ -761,7 +784,7 @@ def __init__(
callable_mock: Union[Callable[[Type[object]], Any], _CallableMock, None] = None,
original_callable: Optional[Callable] = None,
allow_private: bool = False,
type_validation: bool = True,
type_validation: Optional[bool] = None,
) -> None:
if not _is_setup():
raise RuntimeError(
Expand Down
18 changes: 16 additions & 2 deletions testslide/strict_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,14 @@ async def awaitable_return_validation_wrapper(
raise NonAwaitableReturn(self, name)

return_value = await result_awaitable
if not isinstance(
if not testslide.lib._is_wrapped_for_signature_and_type_validation(
# The original value was already wrapped for type
# validation. Skipping additional validation to
# allow, for example, mock_callable to disable
# validation for a very specific mock call rather
# for the whole StrictMock instance
value
) and not isinstance(
# If the return value is a _BaseRunner then type
# validation, if needed, has already been performed
return_value,
Expand All @@ -686,7 +693,14 @@ def return_validation_wrapper(*args, **kwargs):
return_value = signature_validation_wrapper(
*args, **kwargs
)
if not isinstance(
if not testslide.lib._is_wrapped_for_signature_and_type_validation(
# The original value was already wrapped for type
# validation. Skipping additional validation to
# allow, for example, mock_callable to disable
# validation for a very specific mock call rather
# for the whole StrictMock instance
value
) and not isinstance(
# If the return value is a _BaseRunner then type
# validation, if needed, has already been performed
return_value,
Expand Down

0 comments on commit fc2d2c1

Please sign in to comment.