Skip to content

Commit

Permalink
Merge fd10653 into 013b319
Browse files Browse the repository at this point in the history
  • Loading branch information
deathowl committed Sep 23, 2021
2 parents 013b319 + fd10653 commit 8c02962
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 7 deletions.
34 changes: 34 additions & 0 deletions docs/patching/mock_callable/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ Note how you get two failed assertions, instead of just one:

It is now pretty clear what is broken, and why it is broken.



Defining a Target
-----------------

Expand Down Expand Up @@ -136,6 +138,38 @@ Note how it is **safe by default**: once ``for_call`` is used, other calls will

Also check :doc:`../argument_matchers/index`: they allow more relaxed argument matching like "any string matching this regexp" or "any positive number".


For usecases where certain arguments could take many values, and setting up all the for_calls could become tedious you can use ``for_partial_call``
This causes Testslide to ignore all validations of args and kwargs passed to the mock, except those that are defined in the ``for_partial_call``

Tests will still fail, if none of the necessary args or kwargs are passed, so this is a sane golden pathway, between writing safe and easy to use mocks.
Example:

.. code-block:: none
def test_for_partial_call_accepts_all_other_args_and_kwargs(self):
self.mock_callable(sample_module, "test_function",).for_partial_call(
"firstarg", kwarg1="a"
).to_return_value(["blah"])
sample_module.test_function("firstarg", "xx", kwarg1="a", kwarg2="x")
def test_for_partial_call_fails_if_no_required_args_are_present(self):
with self.assertRaises(mock_callable.UnexpectedCallArguments):
self.mock_callable(sample_module, "test_function",).for_partial_call(
"firstarg", kwarg1="a"
).to_return_value(["blah"])
sample_module.test_function(
"differentarg", "alsodifferent", kwarg1="a", kwarg2="x"
)
def test_for_partial_call_fails_if_no_required_kwargs_are_present(self):
with self.assertRaises(mock_callable.UnexpectedCallArguments):
self.mock_callable(sample_module, "test_function",).for_partial_call(
"firstarg", kwarg1="x"
).to_return_value(["blah"])
sample_module.test_function("firstarg", "secondarg", kwarg1="a", kwarg2="x")
Composition
^^^^^^^^^^^

Expand Down
44 changes: 44 additions & 0 deletions tests/accept_any_arg_unittest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from testslide import TestCase, mock_callable

from . import sample_module


class TestAcceptAnyArg(TestCase):
def test_for_partial_call_accepts_all_other_args(self):
self.mock_callable(sample_module, "test_function").for_partial_call(
"a"
).to_return_value(["blah"])
sample_module.test_function("a", "b")

def test_for_partial_call_accepts_all_other_kwargs(self):
self.mock_callable(sample_module, "test_function").for_partial_call(
"firstarg", "secondarg", kwarg1="a"
).to_return_value(["blah"])
sample_module.test_function("firstarg", "secondarg", kwarg1="a", kwarg2="x")

def test_for_partial_call_accepts_all_other_args_and_kwargs(self):
self.mock_callable(sample_module, "test_function",).for_partial_call(
"firstarg", kwarg1="a"
).to_return_value(["blah"])
sample_module.test_function("firstarg", "xx", kwarg1="a", kwarg2="x")

def test_for_partial_call_fails_if_no_required_args_are_present(self):
with self.assertRaises(mock_callable.UnexpectedCallArguments):
self.mock_callable(sample_module, "test_function",).for_partial_call(
"firstarg", kwarg1="a"
).to_return_value(["blah"])
sample_module.test_function(
"differentarg", "alsodifferent", kwarg1="a", kwarg2="x"
)

def test_for_partial_call_fails_if_no_required_kwargs_are_present(self):
with self.assertRaises(mock_callable.UnexpectedCallArguments):
self.mock_callable(sample_module, "test_function",).for_partial_call(
"firstarg", kwarg1="x"
).to_return_value(["blah"])
sample_module.test_function("firstarg", "secondarg", kwarg1="a", kwarg2="x")
49 changes: 42 additions & 7 deletions testslide/mock_callable.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ def __init__(
self._call_count: int = 0
self._max_calls: Optional[int] = None
self._has_order_assertion = False
self._ignore_other_args = False
self._ignore_other_kwargs = False

def register_call(self, *args: Any, **kwargs: Any) -> None:
global _received_ordered_calls
Expand Down Expand Up @@ -258,15 +260,33 @@ def inc_call_count(self) -> None:
)
)

def add_accepted_args(self, *args: Any, **kwargs: Any) -> None:
# TODO validate if args match callable signature
def add_accepted_args(
self,
ignore_other_args: bool = False,
ignore_other_kwargs: bool = False,
*args: Any,
**kwargs: Any,
) -> None:
self.accepted_args = (args, kwargs)
self._ignore_other_args = ignore_other_args
self._ignore_other_kwargs = ignore_other_kwargs

def can_accept_args(self, *args: Any, **kwargs: Any) -> bool:
if self.accepted_args:
if self.accepted_args == (args, kwargs):
return True
return False

if self._ignore_other_args:
args_match = all(elem in args for elem in self.accepted_args[0])
else:
args_match = args == self.accepted_args[0]
if self._ignore_other_kwargs:
kwargs_match = all(
elem in kwargs.keys()
and kwargs[elem] == self.accepted_args[1][elem]
for elem in self.accepted_args[1].keys()
)
else:
kwargs_match = kwargs == self.accepted_args[1]
return args_match and kwargs_match
else:
return True

Expand Down Expand Up @@ -799,6 +819,8 @@ def __init__(
self.type_validation = type_validation
self.caller_frame_info = caller_frame_info
self._allow_coro = False
self._ignore_other_args = False
self._ignore_other_kwargs = False
if isinstance(target, str):
self._target = testslide._importer(target)
else:
Expand Down Expand Up @@ -839,7 +861,9 @@ def _add_runner(self, runner: _BaseRunner) -> None:
if self._next_runner_accepted_args:
args, kwargs = self._next_runner_accepted_args
self._next_runner_accepted_args = None
runner.add_accepted_args(*args, **kwargs)
runner.add_accepted_args(
self._ignore_other_args, self._ignore_other_kwargs, *args, **kwargs
)
self._runner = runner
self._callable_mock.runners.insert(0, runner) # type: ignore

Expand Down Expand Up @@ -872,8 +896,19 @@ def for_call(
Filter for only calls like this.
"""
if self._runner:
self._runner.add_accepted_args(*args, **kwargs)
self._runner.add_accepted_args(False, False, *args, **kwargs)
else:
self._next_runner_accepted_args = (args, kwargs)
return self

def for_partial_call(
self, *args: Any, **kwargs: Any
) -> Union["_MockCallableDSL", "_MockAsyncCallableDSL", "_MockConstructorDSL"]:
if self._runner:
self._runner.add_accepted_args(True, True, *args, **kwargs)
else:
self._ignore_other_args = True
self._ignore_other_kwargs = True
self._next_runner_accepted_args = (args, kwargs)
return self

Expand Down

0 comments on commit 8c02962

Please sign in to comment.