diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 284e57e6efd3082cff63f9ae13b653d5b2b18d2a..33016387b6177c6402b6e2c2c233a8295908b5a6 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.2.2 +current_version = 1.4.0 commit = True message = Bump version to {new_version} tag = True @@ -16,4 +16,3 @@ optional_value = release values = dev release - diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000000000000000000000000000000000000..4ca59efe4fc02992fff3dbd24aff7f0ead21d93f --- /dev/null +++ b/.codespellrc @@ -0,0 +1,3 @@ +[codespell] +skip=./docs/_build,./.mypy_cache,*.sublime-workspace,.git +ignore-words-list=atleast diff --git a/.github/workflows/test-lint-go.yml b/.github/workflows/test-lint-go.yml new file mode 100644 index 0000000000000000000000000000000000000000..1751a6ddad9cec572c09966193e3e892a7182ae2 --- /dev/null +++ b/.github/workflows/test-lint-go.yml @@ -0,0 +1,105 @@ +name: Run tests and lint, maybe deploy + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - '2.7' + - '3.5' + - '3.6' + - '3.7' + - '3.8' + - '3.9' + - '3.10' + - '3.11-dev' + name: Run tests on Python ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v2 + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest numpy + - name: Run pytest + run: pytest + + lint: + runs-on: ubuntu-latest + name: Lint with flake8 + steps: + - uses: actions/checkout@v2 + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + - name: Run flake8 + run: flake8 . + + spellcheck: + runs-on: ubuntu-latest + name: Spellcheck with codespell + steps: + - uses: actions/checkout@v2 + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install codespell + - name: Run codespell + run: codespell + + type-check: + runs-on: ubuntu-latest + name: Check with mypy + steps: + - uses: actions/checkout@v2 + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install mypy + - name: Run mypy + run: mypy mockito + + deploy: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + needs: [test, lint, type-check] + runs-on: ubuntu-latest + name: Deploy to pypi + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + verbose: true diff --git a/.gitignore b/.gitignore index 3f5268d8bf591abb66ff29aa5e1c232acac368ea..99a6b81057ee64a4b2fbe2749ac9f5574779e872 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.pyc -build +build/ +dist/ docs/_build .eggs/ .pytest_cache -__pycache__ \ No newline at end of file +__pycache__ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 75ae86ddaf020f74786c2da1730ffc43a5bb0899..0000000000000000000000000000000000000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: python -python: -- '2.7' -- '3.4' -- '3.5' -- '3.6' -- '3.7' -- '3.8' -install: - - if [[ $TRAVIS_PYTHON_VERSION == 3.8 ]]; then pip install flake8; fi - - pip install . -script: - - if [[ $TRAVIS_PYTHON_VERSION == 3.8 ]]; then flake8 .; fi - - py.test -deploy: - provider: pypi - user: herrkaste - password: - secure: ou5t21x3ysjuRA4oj0oEJIiwffkrsKMoyBL0AhBc+Qq7bxFIEMCdTgfkh1lrWrhGA0xNIAwDHOL9gJrpYaqeLUx6F0mCQc2zRfNzYNf/t4x0+23WsIGQ1HxWGCW9ixLmtXU+zFGK6pUoLZjPdCT0HjZsAjgKOudTv4M1+BlUhFnmAvdmBKjl3jfNY4h5JWbVrhPg6HzMgfNI+vQ7JIFjHZ4a0i2BqEbTMt/2UZGal+Mau0sEO3/y4Ud0LcTRhtA6VA0J7nEcv85q+/JhqmbXTs9h6Bz1KC3V4nMPaaIpGqhrX20eLI6fxULlB/yuBq5jNXSvDMeH9lRyv5AlDUy9NAh++JciXSYYp3p984V/LEwRKM3VyB+ZUUe+KeLN7rk6d/Q2elFW9IHpw9cSsmbl1zrG4GjP+eCpCOw0lrLO6MAijSCGXEzWN+5ViwMDrGCS/6CjRRUBRxcXBebeo6ZB6Wkw+JWdFLW3s/OMzDeVtOEkuP6qdR7VMNn2uYOkPbiDZO4d5UGS09gGMWYasqxP/QJth2yuF95uQmqOhLuGSzI02YS6+L1/Xh2fEmsD8LFF3ATfA0MZ/phHjjvD/ZUmnVgGczW9p1zEohJ9EDQsV4P2fHzNP6nblcx7iBTzKsEsqcjTpOn7UYhFAsyiga17dhcfa5IU2nSb0JzzIeWdM0Q= - on: - tags: true - python: 3.8 - branch: master - distributions: sdist bdist_wheel - repo: kaste/mockito-python diff --git a/CHANGES.txt b/CHANGES.txt index b563f914c600a62c77dc8454c31a69c51f7f8267..c2304777711d5c6d5bb37e26da1063d7af486650 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,59 @@ MOCKITO CHANGE LOG ================== +Release 1.4.0 (August 25, 2022) +------------------------------- + +- @avandierast implemented `thenCallOriginalImplementation`. See #60 + +:: + + # Let `os.path.exists` use the real filesystem (often needed when + # the testing framework needs itself a working `os.path.exists` + # implementation) *but* fake a `.flake8` file. + when(os.path).exists(...).thenCallOriginalImplementation() + when(os.path).exists('.flake8').thenReturn(True) + + + +Release 1.3.5 (August 18, 2022) +------------------------------- + +- Restore compatibility with Python 2.7 + + +Release 1.3.3 (June 23, 2022) +----------------------------- + +- Hotfix: Correctly unstub methods extracted to the module level, for example ``random.randint()`` et.al. from the standard library. See #53 + + +Release 1.3.2 (June 23, 2022) +----------------------------- + +- Let `mock(spec=SomeClass)` work just as `mock(SomeClass)` + + +Release 1.3.1 (June 14, 2022) +----------------------------- + +- Reimplement `captor` to capture only during execution phase of a test. + + +Release 1.3.0 (December 3, 2021) +-------------------------------- + +- Teach `captor` to remember all used values (@shashankrnr32). E.g. + +:: + + arg = captor() + mock.do_something(123) + mock.do_something(456) + verify(mock).do_something(arg) + assert arg.all_values == [123, 456] + + Release 1.2.2 (September 9, 2020) --------------------------------- @@ -21,8 +74,10 @@ Release 1.2.0 (November 25, 2019) - @felixonmars fixed a small compatibility issue with python 3.8 - Mocking properties has become a bit easier. (#26) E.g. +:: + prop = mock() - when(m).__get__(...).thenReturn(23) + when(prop).__get__(...).thenReturn(23) m = mock({'name': prop}) diff --git a/docs/nutshell.rst b/docs/nutshell.rst index 2da28e415bdb58461832bc1bd430a3ce1267853a..0d1051d56f9f1dd876f4a829fe323f5f366d4a40 100644 --- a/docs/nutshell.rst +++ b/docs/nutshell.rst @@ -41,15 +41,14 @@ No difference whatsoever when you mock modules >>> when(os.path).exist('./somewhat').thenReturn(True) Traceback (most recent call last): <...> - mockito.invocation.InvocationError: You tried to stub a method 'exist' the objec - t (<module 'ntpath' from ...>) doesn't have. + mockito.invocation.InvocationError: You tried to stub a method 'exist' + the object (<module 'ntpath' from ...>) doesn't have. If that's too strict, you can change it :: - >>> when(os.path, strict=False).exist('another_place').thenReturn('well, nice he - re') + >>> when(os.path, strict=False).exist('another_place').thenReturn('well, nice here') <mockito.invocation.AnswerSelector object at 0x00D429B0> >>> os.path.exist('another_place') 'well, nice here' diff --git a/docs/walk-through.rst b/docs/walk-through.rst index e1bbc7bbaa1f20e8c304979b46bf1fd25146a0ea..c8e7c101c92ceb47ae0a4435013fca01c9e603d0 100644 --- a/docs/walk-through.rst +++ b/docs/walk-through.rst @@ -19,6 +19,7 @@ There are of course reasons when you don't want to overspecify specific tests. Y # now, obviously, you get the same answer, regardless of the arguments os.path.exists('FooBar') # => True + os.path.exists('BarFoo') # => True You can combine both stubs. E.g. nothing exists, except one file:: @@ -31,6 +32,21 @@ And because it's a similar pattern, we can introduce :func:`spy2` here. Spies ca spy2(os.path.exists) when(os.path).exists('.flake8').thenReturn(False) +Another way to write the same thing is:: + + when(os.path).exists(...).thenCallOriginalImplementation() + when(os.path).exists('.flake8').thenReturn(False) + +And actually `spy2` uses `thenCallOriginalImplementation` under the hood. Why spying at all: either you want the implementation *almost* intact as above or you +need the implementation to stay intact but want to `verify` its usage. E.g.:: + + spy2(os.path.exists) + # now do some stuff + do_stuff() + # then verify the we asked for the cache exactly once + verify(os.path, times=1).exists("cache/.foo") + + When patching, you **MUST** **not** forget to :func:`unstub` of course! You can do this explicitly :: @@ -41,6 +57,7 @@ When patching, you **MUST** **not** forget to :func:`unstub` of course! You can Usually you do this unconditionally in your `teardown` function. If you're using `pytest`, you could define a fixture instead :: + # conftest.py import pytest @@ -193,7 +210,7 @@ To start with an empty stub use :func:`mock`:: verify(obj).say('Hi') # by default all invoked methods take any arguments and return None - # you can configure your expected method calls with the ususal `when` + # you can configure your expected method calls with the usual `when` when(obj).say('Hi').thenReturn('Ho') # There is also a shortcut to set some attributes diff --git a/mockito/__init__.py b/mockito/__init__.py index 8de2c1c4e738a2950fc2337cbf37536dcaa6bb64..53bd3799d5332be0ec4e3f1b300788703b731fdc 100644 --- a/mockito/__init__.py +++ b/mockito/__init__.py @@ -44,7 +44,7 @@ from .matchers import * # noqa: F401 F403 from .matchers import any, contains, times from .verification import never -__version__ = '1.2.2' +__version__ = '1.4.0' __all__ = [ 'mock', diff --git a/mockito/invocation.py b/mockito/invocation.py index 10954c5c45dea4be1104f34d057ab1060031c127..6266b20ee43253c5a9285c521053577fc9c4c789 100644 --- a/mockito/invocation.py +++ b/mockito/invocation.py @@ -18,19 +18,26 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -from . import matchers +import functools +import inspect import operator -from . import signature +from collections import deque + +from . import matchers, signature from . import verification as verificationModule from .utils import contains_strict -from collections import deque -import functools +MYPY = False +if MYPY: + from typing import Any, Callable, Deque, Dict, Tuple class InvocationError(AttributeError): pass +class AnswerError(AttributeError): + pass + __tracebackhide__ = operator.methodcaller( "errisinstance", @@ -44,8 +51,8 @@ class Invocation(object): self.method_name = method_name self.strict = mock.strict - self.params = () - self.named_params = {} + self.params = () # type: Tuple[Any, ...] + self.named_params = {} # type: Dict[str, Any] def _remember_params(self, params, named_params): self.params = params @@ -81,17 +88,22 @@ class RememberedInvocation(Invocation): signature.match_signature(sig, args, kwargs) def __call__(self, *params, **named_params): + if self.mock.eat_self(self.method_name): + params_without_first_arg = params[1:] + else: + params_without_first_arg = params if self.strict: self.ensure_mocked_object_has_method(self.method_name) self.ensure_signature_matches( - self.method_name, params, named_params) + self.method_name, params_without_first_arg, named_params) - self._remember_params(params, named_params) + self._remember_params(params_without_first_arg, named_params) self.mock.remember(self) for matching_invocation in self.mock.stubbed_invocations: if matching_invocation.matches(self): matching_invocation.should_answer(self) + matching_invocation.capture_arguments(self) return matching_invocation.answer_first( *params, **named_params) @@ -156,6 +168,26 @@ class MatchingInvocation(Invocation): return False return True + def capture_arguments(self, invocation): + for x, p1 in enumerate(self.params): + if isinstance(p1, matchers.Capturing): + try: + p2 = invocation.params[x] + except IndexError: + continue + + p1.capture_value(p2) + + for key, p1 in self.named_params.items(): + if isinstance(p1, matchers.Capturing): + try: + p2 = invocation.named_params[key] + except KeyError: + continue + + p1.capture_value(p2) + + def _remember_params(self, params, named_params): if ( contains_strict(params, Ellipsis) @@ -237,6 +269,7 @@ class VerifiableInvocation(MatchingInvocation): matched_invocations = [] for invocation in self.mock.invocations: if self.matches(invocation): + self.capture_arguments(invocation) matched_invocations.append(invocation) self.verification.verify(self, len(matched_invocations)) @@ -383,23 +416,60 @@ def raise_(exception, *a, **kw): raise exception +def discard_self(function): + def function_without_self(*args, **kwargs): + args = args[1:] + return function(*args, **kwargs) + + return function_without_self + + class AnswerSelector(object): def __init__(self, invocation): self.invocation = invocation + self.discard_first_arg = \ + invocation.mock.eat_self(invocation.method_name) def thenReturn(self, *return_values): for return_value in return_values: - self.__then(functools.partial(return_, return_value)) + answer = functools.partial(return_, return_value) + self.__then(answer) return self def thenRaise(self, *exceptions): for exception in exceptions: - self.__then(functools.partial(raise_, exception)) + answer = functools.partial(raise_, exception) + self.__then(answer) return self def thenAnswer(self, *callables): for callable in callables: - self.__then(callable) + answer = callable + if self.discard_first_arg: + answer = discard_self(answer) + self.__then(answer) + return self + + def thenCallOriginalImplementation(self): + answer = self.invocation.mock.get_original_method( + self.invocation.method_name + ) + if not answer: + raise AnswerError( + "'%s' has no original implementation for '%s'." % + (self.invocation.mock.mocked_obj, self.invocation.method_name) + ) + if ( + # A classmethod is not callable + # and a staticmethod is not callable in old version of python, + # so we get the underlying function. + isinstance(answer, classmethod) or isinstance(answer, staticmethod) + # If the method is bound, we unbind it. + or inspect.ismethod(answer) + ): + answer = answer.__func__ + + self.__then(answer) return self def __then(self, answer): @@ -415,7 +485,7 @@ class AnswerSelector(object): class CompositeAnswer(object): def __init__(self): #: Container for answers, which are just ordinary callables - self.answers = deque() + self.answers = deque() # type: Deque[Callable] #: Counter for the maximum answers we ever had self.answer_count = 0 diff --git a/mockito/matchers.py b/mockito/matchers.py index 9a445841f24b4d8d33791e91a71884799054e245..fb7c599816562de4303a5252035c36dd22ed2e0a 100644 --- a/mockito/matchers.py +++ b/mockito/matchers.py @@ -60,7 +60,7 @@ The one usage you should not care about is a loose signature when using """ import re - +builtin_any = any __all__ = [ 'and_', 'or_', 'not_', @@ -77,6 +77,7 @@ __all__ = [ 'kwargs', 'KWARGS' ] + class _ArgsSentinel(object): def __repr__(self): return '*args' @@ -106,10 +107,21 @@ KWARGS = kwargs = {KWARGS_SENTINEL: '_'} # """ + +class MatcherError(RuntimeError): + '''Indicates generic runtime error raised by mockito-python matchers + ''' + pass + + class Matcher: def matches(self, arg): pass +class Capturing: + def capture_value(self, value): + pass + class Any(Matcher): def __init__(self, wanted_type=None): @@ -183,9 +195,7 @@ class Or(Matcher): for matcher in matchers] def matches(self, arg): - return __builtins__['any']( - [matcher.matches(arg) for matcher in self.matchers] - ) + return builtin_any([matcher.matches(arg) for matcher in self.matchers]) def __repr__(self): return "<Or: %s>" % self.matchers @@ -243,21 +253,29 @@ class Matches(Matcher): return "<Matches: %s>" % self.regex.pattern -class ArgumentCaptor(Matcher): +class ArgumentCaptor(Matcher, Capturing): def __init__(self, matcher=None): self.matcher = matcher or Any() - self.value = None + self.all_values = [] def matches(self, arg): result = self.matcher.matches(arg) if not result: return - self.value = arg return True + @property + def value(self): + if not self.all_values: + raise MatcherError("No argument value was captured!") + return self.all_values[-1] + + def capture_value(self, value): + self.all_values.append(value) + def __repr__(self): - return "<ArgumentCaptor: matcher=%s value=%s>" % ( - repr(self.matcher), self.value, + return "<ArgumentCaptor: matcher=%s values=%s>" % ( + repr(self.matcher), self.all_values, ) @@ -370,14 +388,22 @@ def matches(regex, flags=0): def captor(matcher=None): - """Returns argument captor that captures value for further assertions + """Returns argument captor that captures values for further assertions Example:: - arg_captor = captor(any(int)) - when(mock).do_something(arg_captor) + arg = captor() mock.do_something(123) - assert arg_captor.value == 123 + mock.do_something(456) + verify(mock).do_something(arg) + assert arg.value == 456 + assert arg.all_values == [123, 456] + + You can restrict what the captor captures using the other matchers + shown herein:: + + arg = captor(any(str)) + arg = captor(contains("foo")) """ return ArgumentCaptor(matcher) diff --git a/mockito/mocking.py b/mockito/mocking.py index 0955e1979bf1c9e47024b7a73fc3fe2abcb574c1..fed48464a203106a7751439661662eb47c496ec1 100644 --- a/mockito/mocking.py +++ b/mockito/mocking.py @@ -33,6 +33,13 @@ __tracebackhide__ = operator.methodcaller( invocation.InvocationError ) +MYPY = False +if MYPY: + from typing import Deque, List, Union + RealInvocation = Union[ + invocation.RememberedInvocation, + invocation.RememberedProxyInvocation + ] class _Dummy(object): @@ -40,7 +47,7 @@ class _Dummy(object): # must be configured before use, but we want `mock`s to be callable by # default. def __call__(self, *args, **kwargs): - return self.__getattr__('__call__')(*args, **kwargs) + return self.__getattr__('__call__')(*args, **kwargs) # type: ignore[attr-defined] # noqa: E501 def remembered_invocation_builder(mock, method_name, *args, **kwargs): @@ -54,24 +61,29 @@ class Mock(object): self.strict = strict self.spec = spec - self.invocations = deque() - self.stubbed_invocations = deque() + self.invocations = [] # type: List[RealInvocation] + self.stubbed_invocations = deque() \ + # type: Deque[invocation.StubbedInvocation] - self.original_methods = {} + self._original_methods = {} + self._methods_to_unstub = {} self._signatures_store = {} def remember(self, invocation): - self.invocations.appendleft(invocation) + self.invocations.append(invocation) def finish_stubbing(self, stubbed_invocation): self.stubbed_invocations.appendleft(stubbed_invocation) def clear_invocations(self): - self.invocations = deque() + self.invocations = [] + + def get_original_method(self, method_name): + return self._original_methods.get(method_name, None) # STUBBING - def get_original_method(self, method_name): + def _get_original_method_before_stub(self, method_name): """ Looks up the original method on the `spec` object and returns it together with an indication of whether the method is found @@ -97,21 +109,13 @@ class Mock(object): def replace_method(self, method_name, original_method): def new_mocked_method(*args, **kwargs): - # we throw away the first argument, if it's either self or cls - if ( - inspect.ismethod(new_mocked_method) - or inspect.isclass(self.mocked_obj) - and not isinstance(new_mocked_method, staticmethod) - ): - args = args[1:] - return remembered_invocation_builder( self, method_name, *args, **kwargs) new_mocked_method.__name__ = method_name if original_method: new_mocked_method.__doc__ = original_method.__doc__ - new_mocked_method.__wrapped__ = original_method + new_mocked_method.__wrapped__ = original_method # type: ignore[attr-defined] # noqa: E501 try: new_mocked_method.__module__ = original_method.__module__ except AttributeError: @@ -123,30 +127,33 @@ class Mock(object): ) if isinstance(original_method, staticmethod): - new_mocked_method = staticmethod(new_mocked_method) + new_mocked_method = staticmethod(new_mocked_method) # type: ignore[assignment] # noqa: E501 elif isinstance(original_method, classmethod): - new_mocked_method = classmethod(new_mocked_method) + new_mocked_method = classmethod(new_mocked_method) # type: ignore[assignment] # noqa: E501 elif ( inspect.isclass(self.mocked_obj) and inspect.isclass(original_method) # TBC: Inner classes ): - new_mocked_method = staticmethod(new_mocked_method) + new_mocked_method = staticmethod(new_mocked_method) # type: ignore[assignment] # noqa: E501 self.set_method(method_name, new_mocked_method) def stub(self, method_name): try: - self.original_methods[method_name] + self._methods_to_unstub[method_name] except KeyError: - original_method, was_in_spec = self.get_original_method( - method_name) + ( + original_method, + was_in_spec + ) = self._get_original_method_before_stub(method_name) if was_in_spec: # This indicates the original method was found directly on # the spec object and should therefore be restored by unstub - self.original_methods[method_name] = original_method + self._methods_to_unstub[method_name] = original_method else: - self.original_methods[method_name] = None + self._methods_to_unstub[method_name] = None + self._original_methods[method_name] = original_method self.replace_method(method_name, original_method) def forget_stubbed_invocation(self, invocation): @@ -162,27 +169,25 @@ class Mock(object): inv.method_name == invocation.method_name for inv in self.stubbed_invocations ): - original_method = self.original_methods.pop(invocation.method_name) + original_method = self._methods_to_unstub.pop( + invocation.method_name + ) self.restore_method(invocation.method_name, original_method) def restore_method(self, method_name, original_method): # If original_method is None, we *added* it to mocked_obj, so we # must delete it here. - # If we mocked an instance, our mocked function will actually hide - # the one on its class, so we delete as well. - if ( - not original_method - or not inspect.isclass(self.mocked_obj) - and inspect.ismethod(original_method) - ): - delattr(self.mocked_obj, method_name) - else: + if original_method: self.set_method(method_name, original_method) + else: + delattr(self.mocked_obj, method_name) def unstub(self): - while self.original_methods: - method_name, original_method = self.original_methods.popitem() + while self._methods_to_unstub: + method_name, original_method = self._methods_to_unstub.popitem() self.restore_method(method_name, original_method) + self.stubbed_invocations = deque() + self.invocations = [] # SPECCING @@ -203,6 +208,25 @@ class Mock(object): self._signatures_store[method_name] = sig return sig + def eat_self(self, method_name): + """Returns if the method will have a prepended self/class arg on call + """ + try: + original_method = self._original_methods[method_name] + except KeyError: + return False + else: + # If original_method is None, we *added* it to mocked_obj + # and thus, it will eat self iff mocked_obj is a class. + return ( + inspect.ismethod(original_method) + or ( + inspect.isclass(self.mocked_obj) + and not isinstance(original_method, staticmethod) + and not inspect.isclass(original_method) + ) + ) + class _OMITTED(object): def __repr__(self): @@ -257,10 +281,9 @@ def mock(config_or_spec=None, spec=None, strict=OMITTED): """ if type(config_or_spec) is dict: - config = config_or_spec + config, spec = config_or_spec, spec else: - config = {} - spec = config_or_spec + config, spec = {}, spec or config_or_spec if strict is OMITTED: strict = False if spec is None else True diff --git a/mockito/mockito.py b/mockito/mockito.py index 53854febaad69c4dd9dfa01ee2d13822665cb76a..72b30a7165295a8fedab7e7ff610067101318e9c 100644 --- a/mockito/mockito.py +++ b/mockito/mockito.py @@ -164,7 +164,7 @@ def when(obj, strict=True): when(<obj>).<method_name>(<args>).thenReturn(<value>) Compared to simple *patching*, stubbing in mockito requires you to specify - conrete `args` for which the stub will answer with a concrete `<value>`. + concrete `args` for which the stub will answer with a concrete `<value>`. All invocations that do not match this specific call signature will be rejected. They usually throw at call time. @@ -238,7 +238,8 @@ def when2(fn, *args, **kwargs): more documentation. Returns `AnswerSelector` interface which exposes `thenReturn`, - `thenRaise`, and `thenAnswer` as usual. Always `strict`. + `thenRaise`, `thenAnswer`, and `thenCallOriginalImplementation` as usual. + Always `strict`. Usage:: diff --git a/mockito/signature.py b/mockito/signature.py index fabef5f14e3c0aadbba44c1f297aafc58566f56a..aaa76deb9a739b10aad8848c936d3c93fe52755a 100644 --- a/mockito/signature.py +++ b/mockito/signature.py @@ -10,7 +10,7 @@ import types try: from inspect import signature, Parameter except ImportError: - from funcsigs import signature, Parameter + from funcsigs import signature, Parameter # type: ignore[import, no-redef] PY3 = sys.version_info >= (3,) diff --git a/mockito/spying.py b/mockito/spying.py index 2fa5b444a4cbf39af53d3c17d30e468585f5c593..fec365b360c4f5786de856f479afc239a39faeac 100644 --- a/mockito/spying.py +++ b/mockito/spying.py @@ -25,7 +25,6 @@ import inspect from .mockito import when2 from .invocation import RememberedProxyInvocation from .mocking import Mock, _Dummy, mock_registry -from .utils import get_obj __all__ = ['spy'] @@ -96,10 +95,4 @@ def spy2(fn): # type: (...) -> None """ - if isinstance(fn, str): - answer = get_obj(fn) - else: - answer = fn - - when2(fn, Ellipsis).thenAnswer(answer) - + when2(fn, Ellipsis).thenCallOriginalImplementation() diff --git a/mockito/utils.py b/mockito/utils.py index 52e8be4e5019339d3c46777982cf81bf7dbb361f..15d56cda0b4d90c6ddf62d95b5c77802b67b7190 100644 --- a/mockito/utils.py +++ b/mockito/utils.py @@ -77,7 +77,7 @@ def find_invoking_frame_and_try_parse(): if frame_info[3] in ('patch', 'spy2'): continue - source = ''.join(frame_info[4]) + source = ''.join(frame_info[4] or []) m = FIND_ID.match(source) if m: # id should be something like `os.path.exists` etc. diff --git a/mockito/verification.py b/mockito/verification.py index e7f766e572abc4832069f4cce596606cf817808e..0f1726494059339e42180435f7d5bef7b4e6071a 100644 --- a/mockito/verification.py +++ b/mockito/verification.py @@ -22,6 +22,7 @@ import operator __all__ = ['never', 'VerificationError'] + class VerificationError(AssertionError): '''Indicates error during verification of invocations. @@ -104,7 +105,7 @@ Instead got: % ( invocation, "\n ".join( - str(invoc) for invoc in reversed(invocations) + str(invoc) for invoc in invocations ) ) ) @@ -130,13 +131,13 @@ class InOrder(object): def __init__(self, original_verification): ''' - @param original_verification: Original verifiaction to degrade to if + @param original_verification: Original verification to degrade to if order of invocation was ok. ''' self.original_verification = original_verification def verify(self, wanted_invocation, count): - for invocation in reversed(wanted_invocation.mock.invocations): + for invocation in wanted_invocation.mock.invocations: if not invocation.verified_inorder: if not wanted_invocation.matches(invocation): raise VerificationError( diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..c7e52221e9cbb632d8ae8a94b11f19b0f6ac8cfa --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +asyncio_mode=auto +xfail_strict=true diff --git a/setup.py b/setup.py index db955cf5df129c56903a67c8cfebcbe443dd7264..f785bcec48fa16876e85d0ff1a95610fa257da57 100755 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ setup(name='mockito', description='Spying framework', long_description=open('README.rst').read(), install_requires=install_requires, + python_requires='>=2.7', classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', @@ -32,9 +33,10 @@ setup(name='mockito', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ]) diff --git a/tests/call_original_implem_test.py b/tests/call_original_implem_test.py new file mode 100644 index 0000000000000000000000000000000000000000..aca48f1eeb27d36b7f8307b7b993f79ba2a69c10 --- /dev/null +++ b/tests/call_original_implem_test.py @@ -0,0 +1,89 @@ + +import sys + +import pytest +from mockito import mock, when +from mockito.invocation import AnswerError + +from . import module +from .test_base import TestBase + + +class Dog: + def __init__(self, huge=False): + self.huge = huge + + def bark(self): + if self.huge: + return "woof" + else: + return "waf ! waf ! waf ! waf ! waf ! waf !" + + @classmethod + def class_bark(cls): + return cls.__name__ + " woof" + + @staticmethod + def static_bark(arg): + return str(arg) + " woof" + + +class CallOriginalImplementationTest(TestBase): + + def testClassMethod(self): + when(Dog).class_bark().thenCallOriginalImplementation() + + self.assertEqual("Dog woof", Dog.class_bark()) + + def testStaticMethod(self): + when(Dog).static_bark("wif").thenCallOriginalImplementation() + self.assertEqual("wif woof", Dog.static_bark("wif")) + + def testStaticMethodOnInstance(self): + dog = Dog() + when(Dog).static_bark("wif").thenCallOriginalImplementation() + self.assertEqual("wif woof", dog.static_bark("wif")) + + def testMethod(self): + when(Dog).bark().thenCallOriginalImplementation() + + assert Dog(huge=True).bark() == "woof" + + def testMethodOnInstance(self): + dog = Dog(huge=True) + when(dog).bark().thenCallOriginalImplementation() + + assert dog.bark() == "woof" + + def testFunction(self): + when(module).one_arg(Ellipsis).thenCallOriginalImplementation() + assert module.one_arg("woof") == "woof" + + def testChain(self): + when(module).one_arg(Ellipsis) \ + .thenReturn("wif") \ + .thenCallOriginalImplementation() \ + .thenReturn("waf") + assert module.one_arg("woof") == "wif" + assert module.one_arg("woof") == "woof" + assert module.one_arg("woof") == "waf" + + def testDumbMockHasNoOriginalImplementations(self): + dog = mock() + answer_selector = when(dog).bark() + with pytest.raises(AnswerError) as exc: + answer_selector.thenCallOriginalImplementation() + + if sys.version_info >= (3, 0): + class_str_value = "mockito.mocking.mock.<locals>.Dummy" + else: + class_str_value = "mockito.mocking.Dummy" + assert str(exc.value) == ( + "'<class '%s'>' " + "has no original implementation for 'bark'." + ) % class_str_value + + def testSpeccedMockHasOriginalImplementations(self): + dog = mock({"huge": True}, spec=Dog) + when(dog).bark().thenCallOriginalImplementation() + assert dog.bark() == "woof" diff --git a/tests/ellipsis_test.py b/tests/ellipsis_test.py index e4e586020721304a85502e9a4f70a25669c876eb..c39cd95b55e53a546ea9eaa84515a231eadba0f5 100644 --- a/tests/ellipsis_test.py +++ b/tests/ellipsis_test.py @@ -1,9 +1,8 @@ -import pytest - from collections import namedtuple -from mockito import when, args, kwargs, invocation, mock +import pytest +from mockito import args, invocation, kwargs, mock, when class Dog(object): diff --git a/tests/instancemethods_test.py b/tests/instancemethods_test.py index 860d31d43e12746bc8bc3591ce4400bafb61de55..297ca46df29a907333349e895b92d539c0330305 100644 --- a/tests/instancemethods_test.py +++ b/tests/instancemethods_test.py @@ -73,6 +73,14 @@ class InstanceMethodsTest(TestBase): unstub() assert rex.waggle() == 'Wuff!' + def testPartialUnstubShowsTheMockedClass(self): + when(Dog).waggle().thenReturn('Nope!') + + rex = Dog() + when(rex).waggle().thenReturn('Sure!') + unstub(rex) + + assert rex.waggle() == 'Nope!' def testStubAnInstanceMethod(self): when(Dog).waggle().thenReturn('Boing!') diff --git a/tests/matchers_test.py b/tests/matchers_test.py index 0ffcbc3b520f00b7a4229cdffaff34e5b658db05..0b33e9e4b094d4618b2f35b7b0cc30342a5c934b 100644 --- a/tests/matchers_test.py +++ b/tests/matchers_test.py @@ -17,9 +17,9 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. - +from mockito.matchers import MatcherError from .test_base import TestBase -from mockito import mock, verify +from mockito import mock, verify, when from mockito.matchers import and_, or_, not_, eq, neq, lt, lte, gt, gte, \ any_, arg_that, contains, matches, captor, ANY, ARGS, KWARGS import re @@ -208,33 +208,91 @@ class MatchesMatcherTest(TestBase): class ArgumentCaptorTest(TestBase): - def testShouldSatisfyIfInnerMatcherIsSatisfied(self): - c = captor(contains("foo")) - self.assertTrue(c.matches("foobar")) - - def testShouldNotSatisfyIfInnerMatcherIsNotSatisfied(self): - c = captor(contains("foo")) - self.assertFalse(c.matches("barbam")) - - def testShouldReturnNoneValueByDefault(self): - c = captor(contains("foo")) - self.assertEqual(None, c.value) - - def testShouldReturnNoneValueIfDidntMatch(self): - c = captor(contains("foo")) - c.matches("bar") - self.assertEqual(None, c.value) - - def testShouldReturnLastMatchedValue(self): - c = captor(contains("foo")) - c.matches("foobar") - c.matches("foobam") - c.matches("bambaz") - self.assertEqual("foobam", c.value) - - def testShouldDefaultMatcherToAny(self): + def test_matches_anything_by_default(self): + assert captor().matches(12) + assert captor().matches("anything") + assert captor().matches(int) + + def test_matches_is_constrained_by_inner_matcher(self): + assert captor(any_(int)).matches(12) + assert not captor(any_(int)).matches("12") + + def test_all_values_initially_is_empty(self): + c = captor() + assert c.all_values == [] + + def test_captures_all_values(self): + m = mock() + c = captor() + + when(m).do(c) + m.do("any") + m.do("thing") + + assert c.all_values == ["any", "thing"] + + def test_captures_only_matching_values(self): + m = mock() + c = captor(any_(int)) + + when(m).do(c) + m.do("any") + m.do("thing") + m.do(21) + + assert c.all_values == [21] + + def test_captures_all_values_while_verifying(self): + m = mock() + c = captor() + + m.do("any") + m.do("thing") + verify(m, times=2).do(c) + + assert c.all_values == ["any", "thing"] + + def test_remember_last_value(self): + m = mock() c = captor() - c.matches("foo") - c.matches(123) - self.assertEqual(123, c.value) + when(m).do(c) + m.do("any") + m.do("thing") + + assert c.value == "thing" + + def test_remember_last_value_while_verifying(self): + m = mock() + c = captor() + + m.do("any") + m.do("thing") + verify(m, times=2).do(c) + + assert c.value == "thing" + + def test_accessing_value_throws_if_nothing_captured_yet(self): + c = captor() + with self.assertRaises(MatcherError): + _ = c.value + + def test_expose_issue_49_using_when(self): + m = mock() + c = captor() + + when(m).do(c, 10) + when(m).do(c, 11) + m.do("anything", 10) + + assert c.all_values == ["anything"] + + def test_expose_issue_49_using_verify(self): + m = mock() + c = captor() + + m.do("anything", 10) + verify(m).do(c, 10) + verify(m, times=0).do(c, 11) + + assert c.all_values == ["anything"] diff --git a/tests/modulefunctions_test.py b/tests/modulefunctions_test.py index 6d8728f320cb18c64e2198129201706264c5ee11..da8379c4e1249766a864cd4adecea7e33145779c 100644 --- a/tests/modulefunctions_test.py +++ b/tests/modulefunctions_test.py @@ -20,11 +20,12 @@ import os -from .test_base import TestBase -from mockito import when, unstub, verify, any +from mockito import any, unstub, verify, when from mockito.invocation import InvocationError from mockito.verification import VerificationError +from .test_base import TestBase + class ModuleFunctionsTest(TestBase): def tearDown(self): @@ -98,3 +99,15 @@ class ModuleFunctionsTest(TestBase): from . import module when(module).Foo().thenReturn('mocked') assert module.Foo() == 'mocked' + + def testUnstubFunctionOnModuleWhichIsActuallyAMethod_issue_53(self): + import random + when(random).randint(Ellipsis).thenReturn("mocked") + assert random.randint(1, 10) == "mocked" + unstub(random) + assert random.randint(1, 10) != "mocked" + + def testAddFakeMethodInNotStrictMode(self): + when(os.path, strict=False).new_exists("test").thenReturn(True) + + self.assertEqual(True, os.path.new_exists("test")) diff --git a/tests/signatures_test.py b/tests/signatures_test.py index d7c93c656bbd9705449b3db9967e92647479bf69..9fe0cc25f006a4f8c8807a63e41208d2742cbab5 100644 --- a/tests/signatures_test.py +++ b/tests/signatures_test.py @@ -8,7 +8,7 @@ from collections import namedtuple class CallSignature(namedtuple('CallSignature', 'args kwargs')): def raises(self, reason): - return pytest.mark.xfail(self, raises=reason, strict=True) + return pytest.mark.xfail(str(self), raises=reason, strict=True) def sig(*a, **kw): return CallSignature(a, kw) @@ -450,7 +450,7 @@ class TestSignatures: try: import builtins except ImportError: - import __builtin__ as builtins + import __builtin__ as builtins # type: ignore[import, no-redef] # noqa: E501 try: when(builtins).open('foo') diff --git a/tests/speccing_test.py b/tests/speccing_test.py index 0ef5e701e97e684a9a6f223f23bbf7e9356b8f68..a3c974c543e16a4fbe0d073c621ccc8f7e476ae2 100644 --- a/tests/speccing_test.py +++ b/tests/speccing_test.py @@ -102,6 +102,21 @@ class TestSpeccing: assert isinstance(action, Action) + def testShouldPassIsInstanceChecks_2(self): + action = mock(spec=Action) + + assert isinstance(action, Action) + + def testShouldPassIsInstanceChecks_3(self): + action = mock({}, Action) + + assert isinstance(action, Action) + + def testShouldPassIsInstanceChecks_4(self): + action = mock({}, spec=Action) + + assert isinstance(action, Action) + def testHasANiceName(self): action = mock(Action) diff --git a/tests/spying_test.py b/tests/spying_test.py index f2fa87dc447ae2cf4e53a18aa25841d5d1070383..e3254438d5bd8a7e560f67dd39088b52dcdc8c11 100644 --- a/tests/spying_test.py +++ b/tests/spying_test.py @@ -154,3 +154,7 @@ class TestSpy2: assert dummy.return_args('box') == 'foo' assert dummy.return_args('fox') == 'fix' + def testSpyOnClass(self): + spy2(Dummy.foo) + assert Dummy().foo() == 'foo' + diff --git a/tests/staticmethods_test.py b/tests/staticmethods_test.py index ee9a0f55db5d98e2dbe77680dece76d2513610b2..64426de8d97784429ce50903c851917584a2d115 100644 --- a/tests/staticmethods_test.py +++ b/tests/staticmethods_test.py @@ -18,10 +18,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -from .test_base import TestBase -from mockito import when, verify, unstub, any +from mockito import any, unstub, verify, when from mockito.verification import VerificationError +from .test_base import TestBase + + class Dog: @staticmethod def bark(): @@ -76,6 +78,14 @@ class StaticMethodsTest(TestBase): self.assertEqual("miau", Dog.barkHardly(1, 2)) + def testStubsWithArgsOnInstance(self): + dog = Dog() + self.assertEqual("woof woof", dog.barkHardly(1, 2)) + + when(Dog).barkHardly(1, 2).thenReturn("miau") + + self.assertEqual("miau", dog.barkHardly(1, 2)) + def testStubsButDoesNotMachArguments(self): self.assertEqual("woof woof", Dog.barkHardly(1, "anything")) diff --git a/tests/stubbing_test.py b/tests/stubbing_test.py index 6c520d33e9693cbb2d149d0c0e71da3adec9c495..2d0e2ba4c3be88d12084025463f22d379e6d9efc 100644 --- a/tests/stubbing_test.py +++ b/tests/stubbing_test.py @@ -19,9 +19,9 @@ # THE SOFTWARE. import pytest +from mockito import any, mock, times, verify, when from .test_base import TestBase -from mockito import mock, when, verify, times, any class TestEmptyMocks: @@ -375,7 +375,7 @@ class StubbingTest(TestBase): self.assertEqual(m.with_key_words(testing="Very Funky"), "Very Funky Stuff") - def testSubsWithThenAnswerAndMixedArgs(self): + def testStubsWithThenAnswerAndMixedArgs(self): repo = mock() def method_one(value, active_only=False): diff --git a/tests/unstub_test.py b/tests/unstub_test.py index 5743424b702cac6abdd85a207f5b0681c4370b09..e9cccee1e0da266b73b5939634811acbb8d89e5e 100644 --- a/tests/unstub_test.py +++ b/tests/unstub_test.py @@ -1,6 +1,6 @@ import pytest -from mockito import when, unstub, verify, ArgumentError +from mockito import mock, when, unstub, verify, ArgumentError class Dog(object): @@ -31,6 +31,13 @@ class TestUntub: assert mox.waggle() == 'Unsure' + def testUnconfigureMock(self): + m = mock() + when(m).foo().thenReturn(42) + assert m.foo() == 42 + unstub(m) + assert m.foo() is None + class TestAutomaticUnstubbing: def testWith1(self): @@ -128,3 +135,4 @@ class TestAutomaticUnstubbing: assert rex.waggle() == 'Sure' verify(Dog).waggle() + diff --git a/tests/when_interface_test.py b/tests/when_interface_test.py index e5cef5341317d7c166d23ea2b678e0eb325ea30d..cbaafc1294fe5ce86b4e9da5aeb36f2e322acc0c 100644 --- a/tests/when_interface_test.py +++ b/tests/when_interface_test.py @@ -59,7 +59,7 @@ class TestPassAroundStrictness: assert rex.waggle() == 'Sure' assert rex.weggle() == 'Sure' - # For documentation; the inital strict value of the mock will be used + # For documentation; the initial strict value of the mock will be used # here. So the above when(..., strict=False) just assures we can # actually *add* an attribute to the mocked object with pytest.raises(InvocationError):