diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1d55b1d..b1a9040 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.6.0-dev +current_version = 2.0.0-dev0 commit = True message = Bump version to {new_version} tag = True diff --git a/.github/workflows/test-lint-go.yml b/.github/workflows/test-lint-go.yml index 15ae07e..adf6dff 100644 --- a/.github/workflows/test-lint-go.yml +++ b/.github/workflows/test-lint-go.yml @@ -38,7 +38,7 @@ jobs: - name: Setup python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.13' - name: Install dependencies run: | python -m pip install --upgrade pip @@ -54,7 +54,7 @@ jobs: - name: Setup python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.13' - name: Install dependencies run: | python -m pip install --upgrade pip @@ -70,7 +70,7 @@ jobs: - name: Setup python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.13' - name: Install dependencies run: | python -m pip install --upgrade pip @@ -88,7 +88,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.13' - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/README.rst b/README.rst index 9305830..a1c3002 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ Mockito is a spying framework originally based on `the Java library with the same name `_. (Actually *we* invented the strict stubbing mode -back in 2009.) +back in 2009.) .. image:: https://github.com/kaste/mockito-python/actions/workflows/test-lint-go.yml/badge.svg :target: https://github.com/kaste/mockito-python/actions/workflows/test-lint-go.yml @@ -45,6 +45,35 @@ Read the docs http://mockito-python.readthedocs.io/en/latest/ +Breaking changes in v2 +====================== + +Two functions have been renamed: + +- `verifyNoMoreInteractions` is deprecated. Use `ensureNoUnverifiedInteractions` instead. + +Although `verifyNoMoreInteractions` is the name used in mockito for Java, it is a misnomer over there +too (imo). Its docs say "Checks if any of given mocks has any unverified interaction.", and we +make that clear now in the name of the function, so you don't need the docs to tell you what it does. + +- `verifyNoUnwantedInteractions` is deprecated. Use `verifyExpectedInteractions` instead. + +The new name should make it clear that it corresponds to the usage of `expect` (as alternative to `when`). + +Context managers now check the usage and any expectations (if you used `expect`) on exit. You can +disable this check by setting the environment variable `MOCKITO_CONTEXT_MANAGERS_CHECK_USAGE` to `"0"`. +Note that this does not disable the check for any explicit expectations you might have set with `expect`. + +This roughly corresponds to the `verifyStubbedInvocationsAreUsed` contra the `verifyExpectedInteractions` +functions. + + +New in v2 +========= + +- `between` now supports open ranges, e.g. `between=(0, )` to check that at least 0 interactions + occurred. + Development =========== diff --git a/docs/the-functions.rst b/docs/the-functions.rst index e5cd7df..3378635 100644 --- a/docs/the-functions.rst +++ b/docs/the-functions.rst @@ -4,7 +4,7 @@ The functions ============= -Stable entrypoints are: :func:`when`, :func:`mock`, :func:`unstub`, :func:`verify`, :func:`spy`. Experimental or new function introduces with v1.0.x are: :func:`when2`, :func:`expect`, :func:`verifyNoUnwantedInteractions`, :func:`verifyStubbedInvocationsAreUsed`, :func:`patch` +Stable entrypoints are: :func:`when`, :func:`mock`, :func:`unstub`, :func:`verify`, :func:`spy`. New function introduced in v1 are: :func:`when2`, :func:`expect`, :func:`verifyExpectedInteractions`, :func:`verifyStubbedInvocationsAreUsed`, :func:`patch` .. autofunction:: when .. autofunction:: when2 @@ -19,9 +19,14 @@ Stable entrypoints are: :func:`when`, :func:`mock`, :func:`unstub`, :func:`verif This looks like a plethora of verification functions, and especially since you often don't need to `verify` at all. .. autofunction:: verify -.. autofunction:: verifyNoMoreInteractions .. autofunction:: verifyZeroInteractions -.. autofunction:: verifyNoUnwantedInteractions -.. autofunction:: verifyStubbedInvocationsAreUsed +.. autofunction:: verifyExpectedInteractions + +Note that `verifyExpectedInteractions` was named `verifyNoUnwantedInteractions` in v1. +The usage of `verifyNoUnwantedInteractions` is deprecated. +.. autofunction:: verifyStubbedInvocationsAreUsed +.. autofunction:: ensureNoUnverifiedInteractions +Note that `ensureNoUnverifiedInteractions` was named `verifyNoMoreInteractions` in v1. +The usage of `verifyNoMoreInteractions` is deprecated. diff --git a/mockito/__init__.py b/mockito/__init__.py index 2ba57ae..93bb2e8 100644 --- a/mockito/__init__.py +++ b/mockito/__init__.py @@ -28,11 +28,13 @@ expect, unstub, forget_invocations, + ensureNoUnverifiedInteractions, verify, - verifyNoMoreInteractions, verifyZeroInteractions, - verifyNoUnwantedInteractions, + verifyExpectedInteractions, verifyStubbedInvocationsAreUsed, + verifyNoUnwantedInteractions, # deprecated + verifyNoMoreInteractions, # deprecated ArgumentError, ) from . import inorder @@ -44,7 +46,7 @@ from .matchers import any, contains, times from .verification import never -__version__ = '1.6.0-dev' +__version__ = '2.0.0-dev' __all__ = [ 'mock', @@ -54,18 +56,22 @@ 'when2', 'patch', 'expect', + 'ensureNoUnverifiedInteractions', 'verify', - 'verifyNoMoreInteractions', 'verifyZeroInteractions', - 'verifyNoUnwantedInteractions', + 'verifyExpectedInteractions', 'verifyStubbedInvocationsAreUsed', 'inorder', 'unstub', 'forget_invocations', 'VerificationError', 'ArgumentError', + 'any', # compatibility 'contains', # compatibility 'never', # compatibility + 'times', # deprecated + 'verifyNoUnwantedInteractions', # deprecated + 'verifyNoMoreInteractions', # deprecated ] diff --git a/mockito/invocation.py b/mockito/invocation.py index 2c3d7fa..2114f49 100644 --- a/mockito/invocation.py +++ b/mockito/invocation.py @@ -18,6 +18,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import annotations +from abc import ABC +import os import inspect import operator from collections import deque @@ -26,7 +29,12 @@ from . import verification as verificationModule from .utils import contains_strict -from typing import Any, Callable, Deque, Dict, Tuple +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Callable, NoReturn, Self, TypeVar, TYPE_CHECKING + from .mocking import Mock + T = TypeVar('T') class InvocationError(AttributeError): @@ -43,15 +51,15 @@ class AnswerError(AttributeError): class Invocation(object): - def __init__(self, mock, method_name): + def __init__(self, mock: Mock, method_name: str) -> None: self.mock = mock self.method_name = method_name self.strict = mock.strict - self.params: Tuple[Any, ...] = () - self.named_params: Dict[str, Any] = {} + self.params: tuple[Any, ...] = () + self.named_params: dict[str, Any] = {} - def _remember_params(self, params, named_params): + def _remember_params(self, params: tuple, named_params: dict) -> None: self.params = params self.named_params = named_params @@ -65,26 +73,30 @@ def __repr__(self): return "%s(%s)" % (self.method_name, params) -class RememberedInvocation(Invocation): - def __init__(self, mock, method_name): - super(RememberedInvocation, self).__init__(mock, method_name) +class RealInvocation(Invocation, ABC): + def __init__(self, mock: Mock, method_name: str) -> None: + super(RealInvocation, self).__init__(mock, method_name) self.verified = False self.verified_inorder = False - def ensure_mocked_object_has_method(self, method_name): + +class RememberedInvocation(RealInvocation): + def ensure_mocked_object_has_method(self, method_name: str) -> None: if not self.mock.has_method(method_name): raise InvocationError( "You tried to call a method '%s' the object (%s) doesn't " "have." % (method_name, self.mock.mocked_obj)) - def ensure_signature_matches(self, method_name, args, kwargs): + def ensure_signature_matches( + self, method_name: str, args: tuple, kwargs: dict + ) -> None: sig = self.mock.get_signature(method_name) if not sig: return signature.match_signature(sig, args, kwargs) - def __call__(self, *params, **named_params): + def __call__(self, *params: Any, **named_params: Any) -> Any | None: if self.mock.eat_self(self.method_name): params_without_first_arg = params[1:] else: @@ -132,17 +144,12 @@ def __call__(self, *params, **named_params): return None -class RememberedProxyInvocation(Invocation): - '''Remeber params and proxy to method of original object. +class RememberedProxyInvocation(RealInvocation): + """Remember params and proxy to method of original object. Calls method on original object and returns it's return value. - ''' - def __init__(self, mock, method_name): - super(RememberedProxyInvocation, self).__init__(mock, method_name) - self.verified = False - self.verified_inorder = False - - def __call__(self, *params, **named_params): + """ + def __call__(self, *params: Any, **named_params: Any) -> Any: self._remember_params(params, named_params) self.mock.remember(self) obj = self.mock.spec @@ -155,7 +162,17 @@ def __call__(self, *params, **named_params): return method(*params, **named_params) -class MatchingInvocation(Invocation): + +class MatchingInvocation(Invocation, ABC): + """ + Abstract base class for `RememberedInvocation` and `VerifiableInvocation`. + + Mainly implements `matches` which is used to compare calling signatures + where placeholders and matchers (like `any()` or `Ellipsis`) are + interpreted. Here, `self` can contain such special placeholders which then + consume multiple arguments of the (other) `invocation`. + + """ @staticmethod def compare(p1, p2): if isinstance(p1, matchers.Matcher): @@ -165,7 +182,17 @@ def compare(p1, p2): return False return True - def capture_arguments(self, invocation): + def capture_arguments(self, invocation: RealInvocation) -> None: + """Capture arguments of `invocation` into "capturing" matchers of self. + + This is used in conjunction with "capturing" matchers like + `ArgumentCaptor`, e.g. `captor`. + + Imagine a `when(obj).method(captor).thenReturn()` configuration. Now, + when `obj.method("foo")` is called, "foo" will be passed to + `captor.capture_value`. + + """ for x, p1 in enumerate(self.params): if isinstance(p1, matchers.Capturing): try: @@ -185,7 +212,7 @@ def capture_arguments(self, invocation): p1.capture_value(p2) - def _remember_params(self, params, named_params): + def _remember_params(self, params: tuple, named_params: dict) -> None: if ( contains_strict(params, Ellipsis) and (params[-1] is not Ellipsis or named_params) @@ -212,7 +239,7 @@ def wrap(p): # Note: matches(a, b) does not imply matches(b, a) because # the left side might contain wildcards (like Ellipsis) or matchers. # In its current form the right side is a concrete call signature. - def matches(self, invocation): # noqa: C901 (too complex) + def matches(self, invocation: Invocation) -> bool: # noqa: C901, E501 (too complex) if self.method_name != invocation.method_name: return False @@ -257,11 +284,34 @@ def matches(self, invocation): # noqa: C901 (too complex) class VerifiableInvocation(MatchingInvocation): - def __init__(self, mock, method_name, verification): + """ + Denotes the function or method signature after `verify` is called. + + I.e. verify(obj).method(arg1, ...) + ^^^^^^^^^^^^^^^^^ VerifiableInvocation denotes this part + + The constructor takes the mock object, which is the registered `Mock` for + the `obj` in the previous examples, the method name (in the example: + `method`), and the verification mode (i.e. `verificationModule.Times(1)`). + + In the immediately following `__call__` call, the arguments (`args1, ...`) + are captured and verified. For `verify` `__call__` ends the verification + process, there is no third fluent interface. + + Both calls, `__init__` plus `__call__`, encapsulate a method or function + call. But the `__call__` is essentially virtual and can contain + placeholders and matchers. + """ + def __init__( + self, + mock: Mock, + method_name: str, + verification: verificationModule.VerificationMode + ) -> None: super(VerifiableInvocation, self).__init__(mock, method_name) self.verification = verification - def __call__(self, *params, **named_params): + def __call__(self, *params: Any, **named_params: Any) -> None: self._remember_params(params, named_params) matched_invocations = [] for invocation in self.mock.invocations: @@ -284,7 +334,9 @@ def __call__(self, *params, **named_params): stub.allow_zero_invocations = True -def verification_has_lower_bound_of_zero(verification): +def verification_has_lower_bound_of_zero( + verification: verificationModule.VerificationMode | None +) -> bool: if ( isinstance(verification, verificationModule.Times) and verification.wanted_count == 0 @@ -301,7 +353,47 @@ def verification_has_lower_bound_of_zero(verification): class StubbedInvocation(MatchingInvocation): - def __init__(self, mock, method_name, verification=None, strict=None): + """ + Denotes the function or method signature after `when` or `expect` is + called, -- the second part of the fluent interface. + + I.e. when(obj).method(arg1, ...).thenReturn(value1) + expect(obj).method(arg1, ...).thenReturn(value1) + ^^^^^^^^^^^^^^^^^ StubbedInvocation denotes this part + + The constructor takes the mock object, which is the registered `Mock` for + the `obj` in the previous examples, and the method name (in the example: + `method`). + + The `verification` argument is only given when `expect` is being used. + `strict` is used to overrule the `strict` flag of the `mock` object. + + In the immediately following `__call__` call, the arguments (`args1, ...`) + are captured. The third part of the fluent interface (`AnswerSelector`) + is returned. + + Both calls, `__init__` plus `__call__`, encapsulate a method or function + call. But the `__call__` is essentially virtual and can contain + placeholders and matchers. + + The actual stubbing occurs directly in the `__call__` method. The stubbing + is delegated to the `mock` object. In essence, it will likely patch or add + a replacement callable to `obj`, i.e. + `setattr(obj, method_name, new_method)`. + + Note about the nomenclature: In strict OOP languages, we only had + "methods", but in Python `obj` could be a class, instance, or module -- + generally speaking: a "callable". (I.e. classes are also just callables; + there is no "new" keyword in Python.) + + """ + def __init__( + self, + mock: Mock, + method_name: str, + verification: verificationModule.VerificationMode | None = None, + strict: bool | None = None + ) -> None: super(StubbedInvocation, self).__init__(mock, method_name) #: Holds the verification set up via `expect`. @@ -319,27 +411,26 @@ def __init__(self, mock, method_name, verification=None, strict=None): self.used = 0 #: Set if `verifyStubbedInvocationsAreUsed` should pass, regardless - #: of any factual invocation. E.g. set by `verify(..., times=0)` - if verification_has_lower_bound_of_zero(verification): - self.allow_zero_invocations = True - else: - self.allow_zero_invocations = False + #: of any factual invocation. E.g. set by `expect(..., times=0)` + self.allow_zero_invocations: bool = \ + verification_has_lower_bound_of_zero(verification) - - def ensure_mocked_object_has_method(self, method_name): + def ensure_mocked_object_has_method(self, method_name: str) -> None: if not self.mock.has_method(method_name): raise InvocationError( "You tried to stub a method '%s' the object (%s) doesn't " "have." % (method_name, self.mock.mocked_obj)) - def ensure_signature_matches(self, method_name, args, kwargs): + def ensure_signature_matches( + self, method_name: str, args: tuple, kwargs: dict + ) -> None: sig = self.mock.get_signature(method_name) if not sig: return signature.match_signature_allowing_placeholders(sig, args, kwargs) - def __call__(self, *params, **named_params): + def __call__(self, *params: Any, **named_params: Any) -> AnswerSelector: if self.strict: self.ensure_mocked_object_has_method(self.method_name) self.ensure_signature_matches( @@ -350,13 +441,13 @@ def __call__(self, *params, **named_params): self.mock.finish_stubbing(self) return AnswerSelector(self) - def forget_self(self): + def forget_self(self) -> None: self.mock.forget_stubbed_invocation(self) - def add_answer(self, answer): + def add_answer(self, answer: Callable) -> None: self.answers.add(answer) - def answer_first(self, *args, **kwargs): + def answer_first(self, *args: Any, **kwargs: Any) -> Any: self.used += 1 return self.answers.answer(*args, **kwargs) @@ -382,67 +473,65 @@ def should_answer(self, invocation: RememberedInvocation) -> None: elif isinstance(verification, verificationModule.Between): if actual_count > verification.wanted_to: raise InvocationError( - "\nWanted between: [%i, %i], actual times: %i" + "\nWanted between: [%s, %s], actual times: %s" % (verification.wanted_from, verification.wanted_to, actual_count)) # The way mockito's `verify` works is, that it checks off all 'real', # remembered invocations, if they get verified. This is a simple - # mechanism so that a later `verifyNoMoreInteractions` just has to - # ensure that all invocations have this flag set to ``True``. + # mechanism so that a later `ensureNoUnverifiedInteractions` just has + # to ensure that all invocations have this flag set to ``True``. # For verifications set up via `expect` we want all invocations # to get verified 'implicitly', on-the-go, so we set this flag here. invocation.verified = True - def verify(self): + def verify(self) -> None: if self.verification: self.verification.verify(self, self.used) - def check_used(self): + def check_used(self) -> None: if not self.allow_zero_invocations and self.used < len(self.answers): raise verificationModule.VerificationError( "\nUnused stub: %s" % self) -def return_(value): - def answer(*args, **kwargs): +def return_(value: T) -> Callable[..., T]: + def answer(*args, **kwargs) -> T: return value return answer -def raise_(exception): - def answer(*args, **kwargs): +def raise_(exception: Exception | type[Exception]) -> Callable[..., NoReturn]: + def answer(*args, **kwargs) -> NoReturn: raise exception return answer - -def discard_self(function): - def function_without_self(*args, **kwargs): +def discard_self(function: Callable[..., T]) -> Callable[..., T]: + def function_without_self(*args, **kwargs) -> T: args = args[1:] return function(*args, **kwargs) - return function_without_self class AnswerSelector(object): - def __init__(self, invocation): + def __init__(self, invocation: StubbedInvocation) -> None: self.invocation = invocation self.discard_first_arg = \ invocation.mock.eat_self(invocation.method_name) - def thenReturn(self, *return_values): + def thenReturn(self, *return_values: Any) -> Self: for return_value in return_values or (None,): answer = return_(return_value) self.__then(answer) return self - def thenRaise(self, *exceptions): + def thenRaise(self, *exceptions: Exception | type[Exception]) -> Self: for exception in exceptions or (Exception,): answer = raise_(exception) self.__then(answer) return self - def thenAnswer(self, *callables): + def thenAnswer(self, *callables: Callable) -> Self: if not callables: raise TypeError("No answer function provided") for callable in callables: @@ -452,7 +541,7 @@ def thenAnswer(self, *callables): self.__then(answer) return self - def thenCallOriginalImplementation(self): + def thenCallOriginalImplementation(self) -> Self: answer = self.invocation.mock.get_original_method( self.invocation.method_name ) @@ -474,33 +563,36 @@ def thenCallOriginalImplementation(self): self.__then(answer) return self - def __then(self, answer): + def __then(self, answer: Callable) -> None: self.invocation.add_answer(answer) - def __enter__(self): + def __enter__(self) -> None: pass - def __exit__(self, *exc_info): + def __exit__(self, *exc_info) -> None: + self.invocation.verify() + if os.environ.get("MOCKITO_CONTEXT_MANAGERS_CHECK_USAGE", "1") == "1": + self.invocation.check_used() self.invocation.forget_self() class CompositeAnswer(object): - def __init__(self): + def __init__(self) -> None: #: Container for answers, which are just ordinary callables - self.answers: Deque[Callable] = deque() + self.answers: deque[Callable] = deque() #: Counter for the maximum answers we ever had self.answer_count = 0 - def __len__(self): + def __len__(self) -> int: # The minimum is '1' bc we always have a default answer of 'None' return max(1, self.answer_count) - def add(self, answer): + def add(self, answer: Callable) -> None: self.answer_count += 1 self.answers.append(answer) - def answer(self, *args, **kwargs): + def answer(self, *args: Any, **kwargs: Any) -> Any: if len(self.answers) == 0: return None diff --git a/mockito/mock_registry.py b/mockito/mock_registry.py index 2b6ebf7..638442c 100644 --- a/mockito/mock_registry.py +++ b/mockito/mock_registry.py @@ -18,6 +18,11 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .mocking import Mock class MockRegistry: @@ -30,13 +35,13 @@ class MockRegistry: def __init__(self): self.mocks = IdentityMap() - def register(self, obj, mock): + def register(self, obj: object, mock: Mock) -> None: self.mocks[obj] = mock - def mock_for(self, obj): + def mock_for(self, obj: object) -> Mock | None: return self.mocks.get(obj, None) - def unstub(self, obj): + def unstub(self, obj: object) -> None: try: mock = self.mocks.pop(obj) except KeyError: @@ -44,12 +49,12 @@ def unstub(self, obj): else: mock.unstub() - def unstub_all(self): + def unstub_all(self) -> None: for mock in self.get_registered_mocks(): mock.unstub() self.mocks.clear() - def get_registered_mocks(self): + def get_registered_mocks(self) -> list[Mock]: return self.mocks.values() diff --git a/mockito/mocking.py b/mockito/mocking.py index 4076138..aed2741 100644 --- a/mockito/mocking.py +++ b/mockito/mocking.py @@ -18,6 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import annotations import inspect import operator from collections import deque @@ -25,6 +26,8 @@ from . import invocation, signature, utils from .mock_registry import mock_registry +from typing import Callable + __all__ = ['mock'] __tracebackhide__ = operator.methodcaller( @@ -32,12 +35,6 @@ invocation.InvocationError ) -from typing import Deque, List, Union -RealInvocation = Union[ - invocation.RememberedInvocation, - invocation.RememberedProxyInvocation -] - class _Dummy: # We spell out `__call__` here for convenience. All other magic methods @@ -47,39 +44,50 @@ def __call__(self, *args, **kwargs): return self.__getattr__('__call__')(*args, **kwargs) # type: ignore[attr-defined] # noqa: E501 -def remembered_invocation_builder(mock, method_name, *args, **kwargs): +def remembered_invocation_builder( + mock: Mock, method_name: str, *args, **kwargs +): invoc = invocation.RememberedInvocation(mock, method_name) return invoc(*args, **kwargs) class Mock(object): - def __init__(self, mocked_obj, strict=True, spec=None): + def __init__( + self, + mocked_obj: object, + strict: bool = True, + spec: object | None = None + ) -> None: self.mocked_obj = mocked_obj self.strict = strict self.spec = spec - self.invocations: List[RealInvocation] = [] - self.stubbed_invocations: Deque[invocation.StubbedInvocation] = deque() + self.invocations: list[invocation.RealInvocation] = [] + self.stubbed_invocations: deque[invocation.StubbedInvocation] = deque() - self._original_methods = {} - self._methods_to_unstub = {} - self._signatures_store = {} + self._original_methods: dict[str, Callable | None] = {} + self._methods_to_unstub: dict[str, Callable | None] = {} + self._signatures_store: dict[str, signature.Signature | None] = {} - def remember(self, invocation): + def remember(self, invocation: invocation.RealInvocation) -> None: self.invocations.append(invocation) - def finish_stubbing(self, stubbed_invocation): + def finish_stubbing( + self, stubbed_invocation: invocation.StubbedInvocation + ) -> None: self.stubbed_invocations.appendleft(stubbed_invocation) - def clear_invocations(self): + def clear_invocations(self) -> None: self.invocations = [] - def get_original_method(self, method_name): + def get_original_method(self, method_name: str) -> Callable | None: return self._original_methods.get(method_name, None) # STUBBING - def _get_original_method_before_stub(self, method_name): + def _get_original_method_before_stub( + self, method_name: str + ) -> tuple[Callable | None, bool]: """ Looks up the original method on the `spec` object and returns it together with an indication of whether the method is found @@ -99,10 +107,12 @@ def _get_original_method_before_stub(self, method_name): # the better error message for the user. return getattr(self.spec, method_name, None), False - def set_method(self, method_name, new_method): + def set_method(self, method_name: str, new_method: object) -> None: setattr(self.mocked_obj, method_name, new_method) - def replace_method(self, method_name, original_method): + def replace_method( + self, method_name: str, original_method: object | None + ) -> None: def new_mocked_method(*args, **kwargs): return remembered_invocation_builder( @@ -123,18 +133,18 @@ def new_mocked_method(*args, **kwargs): ) if isinstance(original_method, staticmethod): - new_mocked_method = staticmethod(new_mocked_method) # type: ignore[assignment] # noqa: E501 + new_mocked_method = staticmethod(new_mocked_method) elif isinstance(original_method, classmethod): 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) # type: ignore[assignment] # noqa: E501 + new_mocked_method = staticmethod(new_mocked_method) self.set_method(method_name, new_mocked_method) - def stub(self, method_name): + def stub(self, method_name: str) -> None: try: self._methods_to_unstub[method_name] except KeyError: @@ -152,7 +162,9 @@ def stub(self, method_name): self._original_methods[method_name] = original_method self.replace_method(method_name, original_method) - def forget_stubbed_invocation(self, invocation): + def forget_stubbed_invocation( + self, invocation: invocation.StubbedInvocation + ) -> None: assert invocation in self.stubbed_invocations if len(self.stubbed_invocations) == 1: @@ -170,7 +182,9 @@ def forget_stubbed_invocation(self, invocation): ) self.restore_method(invocation.method_name, original_method) - def restore_method(self, method_name, original_method): + def restore_method( + self, method_name: str, original_method: object | None + ) -> None: # If original_method is None, we *added* it to mocked_obj, so we # must delete it here. if original_method: @@ -178,7 +192,7 @@ def restore_method(self, method_name, original_method): else: delattr(self.mocked_obj, method_name) - def unstub(self): + def unstub(self) -> None: while self._methods_to_unstub: method_name, original_method = self._methods_to_unstub.popitem() self.restore_method(method_name, original_method) @@ -187,13 +201,13 @@ def unstub(self): # SPECCING - def has_method(self, method_name): + def has_method(self, method_name: str) -> bool: if self.spec is None: return True return hasattr(self.spec, method_name) - def get_signature(self, method_name): + def get_signature(self, method_name: str) -> signature.Signature | None: if self.spec is None: return None @@ -204,7 +218,7 @@ def get_signature(self, method_name): self._signatures_store[method_name] = sig return sig - def eat_self(self, method_name): + def eat_self(self, method_name: str) -> bool: """Returns if the method will have a prepended self/class arg on call """ try: diff --git a/mockito/mockito.py b/mockito/mockito.py index 7957d6a..759533a 100644 --- a/mockito/mockito.py +++ b/mockito/mockito.py @@ -18,12 +18,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import annotations import operator from . import invocation from . import verification -from .utils import get_obj, get_obj_attr_tuple +from .utils import deprecated, get_obj, get_obj_attr_tuple from .mocking import Mock from .mock_registry import mock_registry from .verification import VerificationError @@ -38,73 +39,76 @@ class ArgumentError(Exception): ) -def _multiple_arguments_in_use(*args): - return len([x for x in args if x]) > 1 - - -def _invalid_argument(value): - return (value is not None and value < 1) or value == 0 - - -def _invalid_between(between): +def _invalid_between(between) -> bool: if between is not None: try: - start, end = between + if len(between) == 1: + start, end = between[0], float('inf') + else: + start, end = between + if start < 0 or start > end: + return True except Exception: return True - - if start > end or start < 0: - return True return False + def _get_wanted_verification( - times=None, atleast=None, atmost=None, between=None): - if times is not None and times < 0: - raise ArgumentError("'times' argument has invalid value.\n" - "It should be at least 0. You wanted to set it to:" - " %i" % times) - if _multiple_arguments_in_use(atleast, atmost, between): + times=None, atleast=None, atmost=None, between=None +) -> verification.VerificationMode | None: + if (times, atleast, atmost, between).count(None) < 3: raise ArgumentError( - "You can set only one of the arguments: 'atleast', " + "You can set only one of the arguments: 'times', 'atleast', " "'atmost' or 'between'.") - if _invalid_argument(atleast): - raise ArgumentError("'atleast' argument has invalid value.\n" - "It should be at least 1. You wanted to set it " - "to: %i" % atleast) - if _invalid_argument(atmost): - raise ArgumentError("'atmost' argument has invalid value.\n" - "It should be at least 1. You wanted to set it " - "to: %i" % atmost) - if _invalid_between(between): - raise ArgumentError( - """'between' argument has invalid value. -It should consist of positive values with second number not greater -than first e.g. (1, 4) or (0, 3) or (2, 2). -You wanted to set it to: %s""" % (between,)) - if atleast: + if times is not None: + if times < 0: + raise ArgumentError( + "'times' argument has invalid value.\n" + f"It should be at least 0. You wanted to set it to: {times}" + ) + return verification.Times(times) + if atleast is not None: + if atleast < 1: + raise ArgumentError( + "'atleast' argument has invalid value.\n" + f"It should be at least 1. You wanted to set it to: {atleast}" + ) return verification.AtLeast(atleast) - elif atmost: + if atmost is not None: + if atmost < 1: + raise ArgumentError( + "'atmost' argument has invalid value.\n" + f"It should be at least 1. You wanted to set it to: {atmost}" + ) return verification.AtMost(atmost) - elif between: + if between is not None: + if _invalid_between(between): + raise ArgumentError( + "'between' argument has invalid value.\n" + "It should consist of positive values with second number " + "greater than first e.g. (1, 4) or (0, 3) or (2, 2), " + "or a single non-negative number for open-ended range " + f"e.g. (0,). You wanted to set it to: {between}" + ) return verification.Between(*between) - elif times is not None: - return verification.Times(times) + return None + -def _get_mock(obj, strict=True): +def _get_mock(obj: object, strict=True) -> Mock: theMock = mock_registry.mock_for(obj) if theMock is None: theMock = Mock(obj, strict=strict, spec=obj) mock_registry.register(obj, theMock) return theMock -def _get_mock_or_raise(obj): +def _get_mock_or_raise(obj: object) -> Mock: theMock = mock_registry.mock_for(obj) if theMock is None: raise ArgumentError("obj '%s' is not registered" % obj) return theMock -def verify(obj, times=1, atleast=None, atmost=None, between=None, +def verify(obj, times=None, atleast=None, atmost=None, between=None, inorder=False): """Central interface to verify interactions. @@ -131,8 +135,11 @@ def verify(obj, times=1, atleast=None, atmost=None, between=None, if isinstance(obj, str): obj = get_obj(obj) - verification_fn = _get_wanted_verification( - times=times, atleast=atleast, atmost=atmost, between=between) + verification_fn = ( + _get_wanted_verification( + times=times, atleast=atleast, atmost=atmost, between=between + ) or verification.Times(1) + ) if inorder: verification_fn = verification.InOrder(verification_fn) @@ -301,12 +308,12 @@ def expect(obj, strict=True, dog.bark('Wuff') # will throw at call time: too many invocations # maybe if you need to ensure that `dog.bark()` was called at all - verifyNoUnwantedInteractions() + verifyExpectedInteractions() .. note:: You must :func:`unstub` after stubbing, or use `with` statement. - See :func:`when`, :func:`when2`, :func:`verifyNoUnwantedInteractions` + See :func:`when`, :func:`when2`, :func:`verifyExpectedInteractions` """ @@ -360,8 +367,15 @@ def forget_invocations(*objs): theMock.clear_invocations() -def verifyNoMoreInteractions(*objs): - verifyNoUnwantedInteractions(*objs) +def ensureNoUnverifiedInteractions(*objs): + """Check if any given object has any unverified interaction. + + You can use this after `verify`-ing to ensure no other interactions + happened. + + Can lead to over-specified tests. + """ + verifyExpectedInteractions(*objs) for obj in objs: theMock = _get_mock_or_raise(obj) @@ -374,11 +388,14 @@ def verifyNoMoreInteractions(*objs): def verifyZeroInteractions(*objs): """Verify that no methods have been called on given objs. - Note that strict mocks usually throw early on unexpected, unstubbed - invocations. Partial mocks ('monkeypatched' objects or modules) do not - support this functionality at all, bc only for the stubbed invocations - the actual usage gets recorded. So this function is of limited use, - nowadays. + Rarely used because `verify(..., times=0)` is more explicit. Also: + strict mocks usually throw early on unexpected, unstubbed invocations. + For them, there may be no need to verify afterwards. + `expect(..., times=0)` may also appropriate. + + Partial mocks ('monkeypatched' objects or modules) only look at the + stubbed invocations as the actual usage gets recorded only for them. + However, you could use `spy` and inject it. """ for obj in objs: @@ -390,14 +407,14 @@ def verifyZeroInteractions(*objs): -def verifyNoUnwantedInteractions(*objs): +def verifyExpectedInteractions(*objs): """Verifies that expectations set via `expect` are met E.g.:: expect(os.path, times=1).exists(...).thenReturn(True) os.path('/foo') - verifyNoUnwantedInteractions(os.path) # ok, called once + verifyExpectedInteractions(os.path) # ok, called once If you leave out the argument *all* registered objects will be checked. @@ -418,6 +435,7 @@ def verifyNoUnwantedInteractions(*objs): for i in mock.stubbed_invocations: i.verify() + def verifyStubbedInvocationsAreUsed(*objs): """Ensure stubs are actually used. @@ -435,3 +453,29 @@ def verifyStubbedInvocationsAreUsed(*objs): for mock in theMocks: for i in mock.stubbed_invocations: i.check_used() + + +@deprecated( + "'verifyNoMoreInteractions' is deprecated. " + "Use 'ensureNoUnverifiedInteractions' instead." +) +def verifyNoMoreInteractions(*objs): + return ensureNoUnverifiedInteractions(*objs) + +verifyNoMoreInteractions.__doc__ = ( # noqa: E305 + ensureNoUnverifiedInteractions.__doc__ # type: ignore[operator] + + "\n\nDeprecated: Use 'ensureNoUnverifiedInteractions' instead." +) + + +@deprecated( + "'verifyNoUnwantedInteractions' is deprecated. " + "Use 'verifyExpectedInteractions' instead." +) +def verifyNoUnwantedInteractions(*args, **kwargs): + return verifyExpectedInteractions(*args, **kwargs) + +verifyNoUnwantedInteractions.__doc__ = ( # noqa: E305 + verifyExpectedInteractions.__doc__ # type: ignore[operator] + + "\n\nDeprecated: Use 'verifyExpectedInteractions' instead." +) diff --git a/mockito/signature.py b/mockito/signature.py index 93f8f4e..bc3f23f 100644 --- a/mockito/signature.py +++ b/mockito/signature.py @@ -1,4 +1,4 @@ - +from __future__ import annotations from . import matchers from .utils import contains_strict @@ -6,13 +6,12 @@ import inspect try: - from inspect import signature, Parameter + from inspect import signature, Parameter, Signature except ImportError: - from funcsigs import signature, Parameter # type: ignore[import, no-redef] - + from funcsigs import signature, Parameter, Signature # type: ignore[import-not-found, no-redef] # noqa: E501 -def get_signature(obj, method_name): +def get_signature(obj: object, method_name: str) -> Signature | None: method = getattr(obj, method_name) # Eat self for unbound methods bc signature doesn't do it @@ -29,12 +28,13 @@ def get_signature(obj, method_name): return None -def match_signature(sig, args, kwargs): +def match_signature(sig: Signature, args: tuple, kwargs: dict) -> None: sig.bind(*args, **kwargs) - return sig -def match_signature_allowing_placeholders(sig, args, kwargs): # noqa: C901 +def match_signature_allowing_placeholders( # noqa: C901 + sig: Signature, args: tuple, kwargs: dict +) -> None: # Let's face it. If this doesn't work out, we have to do it the hard # way and reimplement something like `sig.bind` with our specific # need for `...`, `*args`, and `**kwargs` support. @@ -80,8 +80,6 @@ def match_signature_allowing_placeholders(sig, args, kwargs): # noqa: C901 raise TypeError('no argument for *args left') if 'multiple values for argument' in error: raise - if 'too many keyword arguments' in error: # PY<3.5 - raise if 'got an unexpected keyword argument' in error: # PY>3.5 raise @@ -103,15 +101,13 @@ def match_signature_allowing_placeholders(sig, args, kwargs): # noqa: C901 # straight forward. sig.bind(*args, **kwargs) - return sig - -def positional_arguments(sig): +def positional_arguments(sig: Signature) -> int: return len([p for n, p in sig.parameters.items() if p.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD)]) -def has_var_keyword(sig): +def has_var_keyword(sig: Signature) -> bool: return any(p for n, p in sig.parameters.items() if p.kind is Parameter.VAR_KEYWORD) diff --git a/mockito/utils.py b/mockito/utils.py index 098693c..cc6ba6c 100644 --- a/mockito/utils.py +++ b/mockito/utils.py @@ -1,9 +1,10 @@ - import importlib import inspect import sys import types import re +import warnings +import functools NEEDS_OS_PATH_HACK = ( @@ -19,6 +20,25 @@ def newmethod(fn, obj): return types.MethodType(fn, obj) +try: + from warnings import deprecated +except ImportError: + def deprecated(message): # type: ignore[no-redef] + """Decorator to mark functions as deprecated. + + Emits a DeprecationWarning when the decorated function is called. + """ + def wrapper(func): + @functools.wraps(func) + def wrapped(*args, **kwargs): + warnings.warn(message, DeprecationWarning, stacklevel=2) + return func(*args, **kwargs) + + return wrapped + + return wrapper + + def get_function_host(fn): """Destructure a given function into its host and its name. diff --git a/mockito/verification.py b/mockito/verification.py index c0864ee..6e7634f 100644 --- a/mockito/verification.py +++ b/mockito/verification.py @@ -18,7 +18,14 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import annotations + import operator +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .invocation import MatchingInvocation __all__ = ['never', 'VerificationError'] @@ -34,11 +41,25 @@ class VerificationError(AssertionError): __tracebackhide__ = operator.methodcaller("errisinstance", VerificationError) -class AtLeast(object): - def __init__(self, wanted_count): +class VerificationMode(ABC): + @abstractmethod + def verify( + self, invocation: MatchingInvocation, actual_count: int + ) -> None: + pass + + +class AtLeast(VerificationMode): + def __init__(self, wanted_count: int) -> None: self.wanted_count = wanted_count - def verify(self, invocation, actual_count): + def verify( + self, invocation: MatchingInvocation, actual_count: int + ) -> None: + if actual_count == 0: + msg = error_message_for_unmatched_invocation(invocation) + raise VerificationError(msg) + if actual_count < self.wanted_count: raise VerificationError("\nWanted at least: %i, actual times: %i" % (self.wanted_count, actual_count)) @@ -46,11 +67,13 @@ def verify(self, invocation, actual_count): def __repr__(self): return "<%s wanted=%s>" % (type(self).__name__, self.wanted_count) -class AtMost(object): - def __init__(self, wanted_count): +class AtMost(VerificationMode): + def __init__(self, wanted_count: int) -> None: self.wanted_count = wanted_count - def verify(self, invocation, actual_count): + def verify( + self, invocation: MatchingInvocation, actual_count: int + ) -> None: if actual_count > self.wanted_count: raise VerificationError("\nWanted at most: %i, actual times: %i" % (self.wanted_count, actual_count)) @@ -58,69 +81,80 @@ def verify(self, invocation, actual_count): def __repr__(self): return "<%s wanted=%s>" % (type(self).__name__, self.wanted_count) -class Between(object): - def __init__(self, wanted_from, wanted_to): +class Between(VerificationMode): + def __init__( + self, wanted_from: int, wanted_to: int | float = float('inf') + ) -> None: self.wanted_from = wanted_from self.wanted_to = wanted_to - def verify(self, invocation, actual_count): + def verify( + self, invocation: MatchingInvocation, actual_count: int + ) -> None: if actual_count < self.wanted_from or actual_count > self.wanted_to: raise VerificationError( - "\nWanted between: [%i, %i], actual times: %i" + "\nWanted between: [%s, %s], actual times: %s" % (self.wanted_from, self.wanted_to, actual_count)) def __repr__(self): - return "<%s [%s, %s]>" % ( - type(self).__name__, self.wanted_from, self.wanted_to) + return "" % (self.wanted_from, self.wanted_to) -class Times(object): - def __init__(self, wanted_count): +class Times(VerificationMode): + def __init__(self, wanted_count: int) -> None: self.wanted_count = wanted_count - def verify(self, invocation, actual_count): + def verify( + self, invocation: MatchingInvocation, actual_count: int + ) -> None: if actual_count == self.wanted_count: return if actual_count == 0: - invocations = ( - [ - invoc - for invoc in invocation.mock.invocations - if invoc.method_name == invocation.method_name - ] - or invocation.mock.invocations - ) - wanted_section = ( - "\nWanted but not invoked:\n\n %s\n" % invocation - ) - instead_section = ( - "\nInstead got:\n\n %s\n" - % "\n ".join(map(str, invocations)) - ) if invocations else "" + msg = error_message_for_unmatched_invocation(invocation) + raise VerificationError(msg) + if self.wanted_count == 0: raise VerificationError( - "%s%s\n" % (wanted_section, instead_section)) - + "\nUnwanted invocation of %s, times: %i" + % (invocation, actual_count)) else: - if self.wanted_count == 0: - raise VerificationError( - "\nUnwanted invocation of %s, times: %i" - % (invocation, actual_count)) - else: - raise VerificationError("\nWanted times: %i, actual times: %i" - % (self.wanted_count, actual_count)) + raise VerificationError("\nWanted times: %i, actual times: %i" + % (self.wanted_count, actual_count)) def __repr__(self): return "<%s wanted=%s>" % (type(self).__name__, self.wanted_count) -class InOrder(object): + +def error_message_for_unmatched_invocation( + invocation: MatchingInvocation +) -> str: + invocations = ( + [ + invoc + for invoc in invocation.mock.invocations + if invoc.method_name == invocation.method_name + ] + or invocation.mock.invocations + ) + wanted_section = ( + "\nWanted but not invoked:\n\n %s\n" % invocation + ) + instead_section = ( + "\nInstead got:\n\n %s\n" + % "\n ".join(map(str, invocations)) + ) if invocations else "" + + return "%s%s\n" % (wanted_section, instead_section) + + +class InOrder(VerificationMode): '''Verifies invocations in order. Verifies if invocation was in expected order, and if yes -- degrades to original Verifier (AtLeast, Times, Between, ...). ''' - def __init__(self, original_verification): + def __init__(self, original_verification: VerificationMode) -> None: ''' @param original_verification: Original verification to degrade to if @@ -128,7 +162,9 @@ def __init__(self, original_verification): ''' self.original_verification = original_verification - def verify(self, wanted_invocation, count): + def verify( + self, wanted_invocation: MatchingInvocation, count: int + ) -> None: for invocation in wanted_invocation.mock.invocations: if not invocation.verified_inorder: if not wanted_invocation.matches(invocation): diff --git a/pyproject.toml b/pyproject.toml index 05ca470..b87c099 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mockito" -version = "1.6.0-dev" +version = "2.0.0-dev" description = "Spying framework" authors = [ { name = "herr kaste", email = "herr.kaste@gmail.com" } @@ -22,6 +22,10 @@ dev-dependencies = [ "bump2version>=0.5.11", "Sphinx>=5.3.0", "sphinx-autobuild>=2021.3.14", + "mypy>=1.14.1", + "flake8>=7.1.1", + "codespell>=2.4.1", + "ipython>=8.32.0", ] [tool.hatch.metadata] diff --git a/requirements-dev.lock b/requirements-dev.lock index 37cd8a9..c5df41e 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,6 +12,8 @@ -e file:. alabaster==0.7.13 # via sphinx +asttokens==3.0.0 + # via stack-data babel==2.13.1 # via sphinx bump2version==1.0.1 @@ -19,31 +21,59 @@ certifi==2023.7.22 # via requests charset-normalizer==3.3.2 # via requests +codespell==2.4.1 colorama==0.4.6 + # via ipython # via pytest # via sphinx # via sphinx-autobuild +decorator==5.1.1 + # via ipython docutils==0.20.1 # via sphinx +executing==2.2.0 + # via stack-data +flake8==7.1.1 idna==3.4 # via requests imagesize==1.4.1 # via sphinx iniconfig==2.0.0 # via pytest +ipython==8.32.0 +jedi==0.19.2 + # via ipython jinja2==3.1.2 # via sphinx livereload==2.6.3 # via sphinx-autobuild markupsafe==2.1.3 # via jinja2 +matplotlib-inline==0.1.7 + # via ipython +mccabe==0.7.0 + # via flake8 +mypy==1.14.1 +mypy-extensions==1.0.0 + # via mypy numpy==2.1.3 packaging==23.2 # via pytest # via sphinx +parso==0.8.4 + # via jedi pluggy==1.3.0 # via pytest +prompt-toolkit==3.0.50 + # via ipython +pure-eval==0.2.3 + # via stack-data +pycodestyle==2.12.1 + # via flake8 +pyflakes==3.2.0 + # via flake8 pygments==2.16.1 + # via ipython # via sphinx pytest==7.4.3 requests==2.31.0 @@ -74,7 +104,16 @@ sphinxcontrib-qthelp==1.0.6 # via sphinx sphinxcontrib-serializinghtml==1.1.9 # via sphinx +stack-data==0.6.3 + # via ipython tornado==6.3.3 # via livereload +traitlets==5.14.3 + # via ipython + # via matplotlib-inline +typing-extensions==4.12.2 + # via mypy urllib3==2.1.0 # via requests +wcwidth==0.2.13 + # via prompt-toolkit diff --git a/tests/context_manager_exit_checks_test.py b/tests/context_manager_exit_checks_test.py new file mode 100644 index 0000000..6b9460f --- /dev/null +++ b/tests/context_manager_exit_checks_test.py @@ -0,0 +1,64 @@ +import pytest +from mockito import expect, when, VerificationError +from mockito.invocation import InvocationError + + +class Dog(object): + def waggle(self): + return 'Unsure' + + def bark(self, sound='Wuff'): + return sound + + +class TestContextManagerExitStrategyForExpect: + def testExpectWithDefaultPassesIfUsed(self): + rex = Dog() + with expect(rex).waggle().thenReturn('Yup'): + assert rex.waggle() == 'Yup' + + def testExpectWithDefaultScreamsIfUnused(self): + rex = Dog() + with pytest.raises(VerificationError): + with expect(rex).waggle().thenReturn('Yup'): + pass + + def testUseEnvSwitchToBypassUsageCheck(self, monkeypatch): + monkeypatch.setenv("MOCKITO_CONTEXT_MANAGERS_CHECK_USAGE", "0") + rex = Dog() + with expect(rex).waggle().thenReturn('Yup'): + pass + + def testEnvSwitchDoesNotBypassExplicitExpectation(self, monkeypatch): + monkeypatch.setenv("MOCKITO_CONTEXT_MANAGERS_CHECK_USAGE", "0") + rex = Dog() + with pytest.raises(VerificationError): + with expect(rex, times=1).waggle().thenReturn('Yup'): + pass + + def testScreamIfUnderUsed(self): + rex = Dog() + with pytest.raises(VerificationError): + with expect(rex, times=2).waggle().thenReturn('Yup'): + rex.waggle() + + def testScreamIfOverUsed(self): + rex = Dog() + with pytest.raises(InvocationError): + with expect(rex, times=1).waggle().thenReturn('Yup'): + rex.waggle() + rex.waggle() + + +class TestContextManagerExitStrategyForWhen: + def testScreamIfUnusedByDefault(self): + rex = Dog() + with pytest.raises(VerificationError): + with when(rex).waggle().thenReturn('Yup'): + pass + + def testUseEnvSwitchToBypassUsageCheck(self, monkeypatch): + monkeypatch.setenv("MOCKITO_CONTEXT_MANAGERS_CHECK_USAGE", "0") + rex = Dog() + with when(rex).waggle().thenReturn('Yup'): + pass diff --git a/tests/instancemethods_test.py b/tests/instancemethods_test.py index e252516..fb276b1 100644 --- a/tests/instancemethods_test.py +++ b/tests/instancemethods_test.py @@ -22,8 +22,8 @@ from .test_base import TestBase from mockito import ( - mock, when, expect, unstub, ANY, verify, verifyNoMoreInteractions, - verifyZeroInteractions, verifyNoUnwantedInteractions, + mock, when, expect, unstub, ANY, verify, ensureNoUnverifiedInteractions, + verifyZeroInteractions, verifyExpectedInteractions, verifyStubbedInvocationsAreUsed) from mockito.invocation import InvocationError from mockito.verification import VerificationError @@ -204,28 +204,28 @@ def testForgottenThenReturnMeansReturnNone(self): class TestVerifyInteractions: class TestZeroInteractions: - def testVerifyNoMoreInteractionsWorks(self): + def testEnsureNoUnverifiedInteractionsWorks(self): when(Dog).bark('Miau') - verifyNoMoreInteractions(Dog) + ensureNoUnverifiedInteractions(Dog) def testVerifyZeroInteractionsWorks(self): when(Dog).bark('Miau') verifyZeroInteractions(Dog) class TestOneInteraction: - def testNothingVerifiedVerifyNoMoreInteractionsRaises(self): + def testNothingVerifiedEnsureNoUnverifiedInteractionsRaises(self): when(Dog).bark('Miau') rex = Dog() rex.bark('Miau') with pytest.raises(VerificationError): - verifyNoMoreInteractions(Dog) + ensureNoUnverifiedInteractions(Dog) - def testIfVerifiedVerifyNoMoreInteractionsPasses(self): + def testIfVerifiedEnsureNoUnverifiedInteractionsPasses(self): when(Dog).bark('Miau') rex = Dog() rex.bark('Miau') verify(Dog).bark('Miau') - verifyNoMoreInteractions(Dog) + ensureNoUnverifiedInteractions(Dog) def testNothingVerifiedVerifyZeroInteractionsRaises(self): when(Dog).bark('Miau') @@ -281,7 +281,7 @@ def testPassIfVerifiedZeroInteractions(self): def testPassIfVerifiedNoMoreInteractions(self): dog = mock() when(dog).waggle(1).thenReturn('Sure') - verifyNoMoreInteractions(dog) + ensureNoUnverifiedInteractions(dog) verifyStubbedInvocationsAreUsed(dog) @@ -384,47 +384,82 @@ def testFailImmediatelyIfWantedCountExceeds(self, verification): with pytest.raises(InvocationError): rex.bark('Miau') - def testVerifyNoMoreInteractionsWorks(self, verification): + class TestErrorMessagesOfInvocationError: + def testUsingTimes(self): + dog = mock() + expect(dog, times=1).bark('Miau').thenReturn('Wuff') + dog.bark('Miau') + with pytest.raises(InvocationError) as exc: + dog.bark('Miau') + + assert str(exc.value) == ( + "\nWanted times: %i, actual times: %i" % (1, 2) + ) + + def testUsingAtMost(self): + dog = mock() + expect(dog, atmost=1).bark('Miau').thenReturn('Wuff') + dog.bark('Miau') + with pytest.raises(InvocationError) as exc: + dog.bark('Miau') + + assert str(exc.value) == ( + "\nWanted at most: %i, actual times: %i" % (1, 2) + ) + + def testUsingBetween(self): + dog = mock() + expect(dog, between=(1, 2)).bark('Miau').thenReturn('Wuff') + dog.bark('Miau') + dog.bark('Miau') + with pytest.raises(InvocationError) as exc: + dog.bark('Miau') + + assert str(exc.value) == ( + "\nWanted between: [%i, %i], actual times: %i" % (1, 2, 3) + ) + + def testEnsureNoUnverifiedInteractionsWorks(self, verification): rex = Dog() expect(rex, **verification).bark('Miau').thenReturn('Wuff') rex.bark('Miau') rex.bark('Miau') - verifyNoMoreInteractions(rex) + ensureNoUnverifiedInteractions(rex) - def testNoUnwantedInteractionsWorks(self, verification): + def testVerifyExpectationsWorks(self, verification): rex = Dog() expect(rex, **verification).bark('Miau').thenReturn('Wuff') rex.bark('Miau') rex.bark('Miau') - verifyNoUnwantedInteractions(rex) + verifyExpectedInteractions(rex) @pytest.mark.parametrize('verification', [ {'times': 2}, {'atleast': 2}, {'between': [1, 2]} ], ids=['times', 'atleast', 'between']) - def testVerifyNoMoreInteractionsBarksIfUnsatisfied(self, verification): + def testEnsureNoUnverifiedInteractionsBarksIfUnsatisfied(self, verification): # noqa: E501 rex = Dog() expect(rex, **verification).bark('Miau').thenReturn('Wuff') with pytest.raises(VerificationError): - verifyNoMoreInteractions(rex) + ensureNoUnverifiedInteractions(rex) @pytest.mark.parametrize('verification', [ {'times': 2}, {'atleast': 2}, {'between': [1, 2]} ], ids=['times', 'atleast', 'between']) - def testNoUnwantedInteractionsBarksIfUnsatisfied(self, verification): + def testVerifyExpectationsBarksIfUnsatisfied(self, verification): rex = Dog() expect(rex, **verification).bark('Miau').thenReturn('Wuff') with pytest.raises(VerificationError): - verifyNoUnwantedInteractions(rex) + verifyExpectedInteractions(rex) - def testNoUnwantedInteractionsForAllRegisteredObjects(self): + def testVerifyExpectationsForAllRegisteredObjects(self): rex = Dog() mox = Dog() @@ -434,9 +469,9 @@ def testNoUnwantedInteractionsForAllRegisteredObjects(self): rex.bark('Miau') mox.bark('Miau') - verifyNoUnwantedInteractions() + verifyExpectedInteractions() - def testUseWhenAndExpectTogetherVerifyNoUnwatedInteractions(self): + def testUseWhenAndExpectAndVerifyExpectations(self): rex = Dog() when(rex).waggle() expect(rex, times=1).bark('Miau') @@ -444,16 +479,16 @@ def testUseWhenAndExpectTogetherVerifyNoUnwatedInteractions(self): rex.waggle() rex.bark('Miau') - verifyNoUnwantedInteractions() + verifyExpectedInteractions() def testExpectWitoutVerification(self): rex = Dog() expect(rex).bark('Miau').thenReturn('Wuff') - verifyNoMoreInteractions(rex) + ensureNoUnverifiedInteractions(rex) rex.bark('Miau') with pytest.raises(VerificationError): - verifyNoMoreInteractions(rex) + ensureNoUnverifiedInteractions(rex) # Where to put this test? During first implementation I broke this def testEnsureWhenGetsNotConfused(self): @@ -461,7 +496,7 @@ def testEnsureWhenGetsNotConfused(self): when(m).foo(1).thenReturn() m.foo(1) with pytest.raises(VerificationError): - verifyNoMoreInteractions(m) + ensureNoUnverifiedInteractions(m) def testEnsureMultipleExpectsArentConfused(self): rex = Dog() diff --git a/tests/unstub_test.py b/tests/unstub_test.py index e9cccee..0e738f7 100644 --- a/tests/unstub_test.py +++ b/tests/unstub_test.py @@ -38,7 +38,8 @@ def testUnconfigureMock(self): unstub(m) assert m.foo() is None -class TestAutomaticUnstubbing: + +class TestContextManagerUnstubStrategy: def testWith1(self): rex = Dog() @@ -76,6 +77,7 @@ def testNesting(self): with when(rex).waggle().thenReturn('Yup'): with when(mox).waggle().thenReturn('Nope'): assert rex.waggle() == 'Yup' + assert mox.waggle() == 'Nope' assert rex.waggle() == 'Unsure' assert mox.waggle() == 'Unsure' diff --git a/tests/verification_errors_test.py b/tests/verification_errors_test.py index 009b722..0a4d43c 100644 --- a/tests/verification_errors_test.py +++ b/tests/verification_errors_test.py @@ -20,8 +20,16 @@ import pytest -from mockito import (mock, when, verify, VerificationError, - verifyNoMoreInteractions, verification) +from mockito import ( + mock, + when, + ensureNoUnverifiedInteractions, + verification, + verify, + verifyNoMoreInteractions, + verifyNoUnwantedInteractions, + VerificationError, +) from mockito.verification import never @@ -120,7 +128,7 @@ def testPrintsUnwantedInteraction(self): theMock = mock() theMock.foo(1, 'foo') with pytest.raises(VerificationError) as exc: - verifyNoMoreInteractions(theMock) + ensureNoUnverifiedInteractions(theMock) assert "\nUnwanted interaction: foo(1, 'foo')" == str(exc.value) def testPrintsNeverWantedInteractionsNicely(self): @@ -147,3 +155,31 @@ def testAtMost(self): def testBetween(self): between = verification.Between(1, 2) assert repr(between) == "" + + def testBetweenOpenRange(self): + between = verification.Between(1, float('inf')) + assert repr(between) == "" + between = verification.Between(1) + assert repr(between) == "" + + def testVerifyNoUnwantedInteractionsIsDeprecated(self): + theMock = mock() + with pytest.warns(DeprecationWarning) as record: + verifyNoUnwantedInteractions(theMock) + + assert len(record) == 1 + assert str(record[0].message) == ( + "'verifyNoUnwantedInteractions' is deprecated. " + "Use 'verifyExpectedInteractions' instead." + ) + + def testVerifyNoMoreInteractionsIsDeprecated(self): + theMock = mock() + with pytest.warns(DeprecationWarning) as record: + verifyNoMoreInteractions(theMock) + + assert len(record) == 1 + assert str(record[0].message) == ( + "'verifyNoMoreInteractions' is deprecated. " + "Use 'ensureNoUnverifiedInteractions' instead." + ) diff --git a/tests/verifications_test.py b/tests/verifications_test.py index 0141162..6fa6f1c 100644 --- a/tests/verifications_test.py +++ b/tests/verifications_test.py @@ -23,8 +23,8 @@ from .test_base import TestBase from mockito import ( mock, when, verify, forget_invocations, inorder, VerificationError, - ArgumentError, verifyNoMoreInteractions, verifyZeroInteractions, - verifyNoUnwantedInteractions, verifyStubbedInvocationsAreUsed, + ArgumentError, ensureNoUnverifiedInteractions, verifyZeroInteractions, + verifyExpectedInteractions, verifyStubbedInvocationsAreUsed, any) from mockito.verification import never @@ -186,6 +186,14 @@ def testVerifiesBetween(self): self.verification_function(self.mock, between=[1, 5]).foo() self.verification_function(self.mock, between=[2, 2]).foo() + self.verification_function(self.mock, between=[0, ]).foo() + self.verification_function(self.mock, between=[1, ]).foo() + self.verification_function(self.mock, between=[2, ]).foo() + + self.verification_function(self.mock, between=[0, float('inf')]).foo() + self.verification_function(self.mock, between=[1, float('inf')]).foo() + self.verification_function(self.mock, between=[2, float('inf')]).foo() + def testFailsVerificationWithBetween(self): self.mock.foo() self.mock.foo() @@ -215,6 +223,8 @@ def testFailsAtMostAtLeastAndBetweenVerificationWithWrongArguments(self): self.mock, between=(0, 1, 2)) self.assertRaises(ArgumentError, self.verification_function, self.mock, between=0) + self.assertRaises(ArgumentError, self.verification_function, + self.mock, between=(-1,)) self.assertRaises(ArgumentError, self.verification_function, self.mock, atleast=5, atmost=5) self.assertRaises(ArgumentError, self.verification_function, @@ -307,12 +317,13 @@ def testVerifies(self): verify(mockOne).foo() verify(mockTwo).bar() - verifyNoMoreInteractions(mockOne, mockTwo) + ensureNoUnverifiedInteractions(mockOne, mockTwo) def testFails(self): theMock = mock() theMock.foo() - self.assertRaises(VerificationError, verifyNoMoreInteractions, theMock) + self.assertRaises( + VerificationError, ensureNoUnverifiedInteractions, theMock) class VerifyZeroInteractionsTest(TestBase): @@ -321,7 +332,7 @@ def testVerifies(self): verifyZeroInteractions(theMock) theMock.foo() self.assertRaises( - VerificationError, verifyNoMoreInteractions, theMock) + VerificationError, ensureNoUnverifiedInteractions, theMock) class ClearInvocationsTest(TestBase): @@ -352,9 +363,9 @@ def testPreservesStubs(self): class TestRaiseOnUnknownObjects: @pytest.mark.parametrize('verification_fn', [ verify, - verifyNoMoreInteractions, + ensureNoUnverifiedInteractions, verifyZeroInteractions, - verifyNoUnwantedInteractions, + verifyExpectedInteractions, verifyStubbedInvocationsAreUsed ]) def testVerifyShouldRaise(self, verification_fn): diff --git a/tests/when2_test.py b/tests/when2_test.py index 84deafc..715f013 100644 --- a/tests/when2_test.py +++ b/tests/when2_test.py @@ -81,13 +81,13 @@ def testWhenSplitOnNextLine(self): def testEnsureWithWhen2SameLine(self): with when2(os.path.commonprefix, '/Foo'): - pass + os.path.commonprefix("/Foo") def testEnsureWithWhen2SplitLine(self): # fmt: off with when2( os.path.commonprefix, '/Foo'): - pass + os.path.commonprefix("/Foo") # fmt: on def testEnsureToResolveMethodOnClass(self): diff --git a/tests/when_interface_test.py b/tests/when_interface_test.py index aad9546..d054ea4 100644 --- a/tests/when_interface_test.py +++ b/tests/when_interface_test.py @@ -2,7 +2,7 @@ import pytest from mockito import (when, when2, expect, verify, patch, mock, spy2, - verifyNoUnwantedInteractions) + verifyExpectedInteractions) from mockito.invocation import InvocationError class Dog(object): @@ -117,7 +117,7 @@ def testReconfigureLooseMock(self): class TestEnsureAddedAttributesGetRemovedOnUnstub: def testWhenPatchingTheClass(self): with when(Dog, strict=False).wggle(): - pass + Dog().wggle() with pytest.raises(AttributeError): Dog.wggle @@ -125,7 +125,7 @@ def testWhenPatchingTheClass(self): def testWhenPatchingAnInstance(self): dog = Dog() with when(dog, strict=False).wggle(): - pass + dog.wggle() with pytest.raises(AttributeError): dog.wggle @@ -152,7 +152,7 @@ def testExpect(self): import os.path os.path.exists('/Foo') assert os.path.exists('/Foo') - verifyNoUnwantedInteractions() + verifyExpectedInteractions() def testPatch(self): dummy = mock()