Skip to content

Commit

Permalink
make new functionality available under for_partial_call to align with…
Browse files Browse the repository at this point in the history
… for_call as recommended by @fornellas.

Also added more tests, and moved docs around
  • Loading branch information
deathowl committed Sep 23, 2021
1 parent 1d47717 commit fd10653
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 50 deletions.
47 changes: 32 additions & 15 deletions docs/patching/mock_callable/index.rst
Expand Up @@ -83,21 +83,6 @@ Note how you get two failed assertions, instead of just one:

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

For usecases where certain arguments could take many values, and setting up all the for_calls could become tedious you can use ``ignore_other_args`` and ``ignore_other_kwargs``
This causes Testslide to ignore all validations of args and kwargs passed to the mock, except those that are pinned in the defined for caller_str
Example:
.. code-block:: none
def test_ignore_other_args(self):
self.mock_callable(sample_module, "test_function", ignore_other_args=True
).for_call("a").to_return_value(["blah"])
sample_module.test_function("a", "b")

def test_ignore_other_kwargs(self):
self.mock_callable(ample_module, "test_function", ignore_other_kwargs=True
).for_call("firstarg", "secondarg", kwarg1="a").to_return_value(["blah"])
sample_module.test_function("firstarg", "secondarg", kwarg1="a", kwarg2="x")



Defining a Target
Expand Down Expand Up @@ -153,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
50 changes: 34 additions & 16 deletions tests/accept_any_arg_unittest.py
@@ -1,26 +1,44 @@
from testslide import TestCase
# 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_ignore_other_args(self):
self.mock_callable(
sample_module, "test_function", ignore_other_args=True
).for_call("a").to_return_value(["blah"])
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_ignore_other_kwargs(self):
self.mock_callable(
sample_module, "test_function", ignore_other_kwargs=True
).for_call("firstarg", "secondarg", kwarg1="a").to_return_value(["blah"])
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_ignore_other_args_and_kwargs(self):
self.mock_callable(
sample_module,
"test_function",
ignore_other_args=True,
ignore_other_kwargs=True,
).for_call("firstarg", kwarg1="a").to_return_value(["blah"])
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")
33 changes: 14 additions & 19 deletions testslide/mock_callable.py
Expand Up @@ -42,8 +42,6 @@ def mock_callable(
# * True: type validation will be enabled (regardless of target type)
# * False: type validation will be disabled
type_validation: Optional[bool] = None,
ignore_other_args: bool = False,
ignore_other_kwargs: bool = False,
) -> "_MockCallableDSL":
caller_frame = inspect.currentframe().f_back # type: ignore
# loading the context ends up reading files from disk and that might block
Expand All @@ -55,8 +53,6 @@ def mock_callable(
caller_frame_info,
allow_private=allow_private,
type_validation=type_validation,
ignore_other_args=ignore_other_args,
ignore_other_kwargs=ignore_other_kwargs,
)


Expand All @@ -66,8 +62,6 @@ def mock_async_callable(
callable_returns_coroutine: bool = False,
allow_private: bool = False,
type_validation: bool = True,
ignore_other_args: bool = False,
ignore_other_kwargs: bool = False,
) -> "_MockAsyncCallableDSL":
caller_frame = inspect.currentframe().f_back # type: ignore
# loading the context ends up reading files from disk and that might block
Expand All @@ -80,8 +74,6 @@ def mock_async_callable(
callable_returns_coroutine,
allow_private,
type_validation,
ignore_other_args=ignore_other_args,
ignore_other_kwargs=ignore_other_kwargs,
)


Expand Down Expand Up @@ -807,8 +799,6 @@ def __init__(
original_callable: Optional[Callable] = None,
allow_private: bool = False,
type_validation: Optional[bool] = None,
ignore_other_args: bool = False,
ignore_other_kwargs: bool = False,
) -> None:
if not _is_setup():
raise RuntimeError(
Expand All @@ -829,8 +819,8 @@ def __init__(
self.type_validation = type_validation
self.caller_frame_info = caller_frame_info
self._allow_coro = False
self._ignore_other_args = ignore_other_args
self._ignore_other_kwargs = ignore_other_kwargs
self._ignore_other_args = False
self._ignore_other_kwargs = False
if isinstance(target, str):
self._target = testslide._importer(target)
else:
Expand Down Expand Up @@ -906,13 +896,22 @@ def for_call(
Filter for only calls like this.
"""
if self._runner:
self._runner.add_accepted_args(
self._ignore_other_args, self._ignore_other_kwargs, *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

##
## Behavior
##
Expand Down Expand Up @@ -1173,8 +1172,6 @@ def __init__(
callable_returns_coroutine: bool,
allow_private: bool = False,
type_validation: bool = True,
ignore_other_args: bool = False,
ignore_other_kwargs: bool = False,
) -> None:
self._callable_returns_coroutine = callable_returns_coroutine
super().__init__(
Expand All @@ -1183,8 +1180,6 @@ def __init__(
caller_frame_info,
allow_private=allow_private,
type_validation=type_validation,
ignore_other_args=ignore_other_args,
ignore_other_kwargs=ignore_other_kwargs,
)
self._allow_coro = True

Expand Down

0 comments on commit fd10653

Please sign in to comment.