From b72e083e274c4440537c2455d5db6e1a7fffcbe4 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Mon, 17 Nov 2025 22:12:41 +0100 Subject: [PATCH 01/11] Implement InOrder Authored-by: Christian Mancini Co-authored-by: herr kaste --- mockito/inorder.py | 67 +++++++++++++++++++++++++ mockito/mocking.py | 12 ++++- tests/in_order_test.py | 109 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 tests/in_order_test.py diff --git a/mockito/inorder.py b/mockito/inorder.py index f1ed6db..75fd500 100644 --- a/mockito/inorder.py +++ b/mockito/inorder.py @@ -17,11 +17,78 @@ # 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 __future__ import annotations +from collections import Counter, deque +from typing import Tuple, Deque + +from .verification import VerificationError +from .invocation import RealInvocation +from .mocking import Mock from .mockito import verify as verify_main +from .mock_registry import mock_registry def verify(object, *args, **kwargs): kwargs['inorder'] = True return verify_main(object, *args, **kwargs) + +class InOrder: + + def __init__(self, *mocks: Mock): + counter = Counter(mocks) + duplicates = [d for d, freq in counter.items() if freq > 1] + if duplicates: + raise ValueError( + f"The following Mocks are duplicated: " + f"{[str(d) for d in duplicates]}" + ) + self._mocks = mocks + for mock in mocks: + m = mock_registry.mock_for(mock) + if m: + m.attach(self) + + self.ordered_invocations: Deque[RealInvocation] = deque() + + def update(self, invocation: RealInvocation) -> None: + self.ordered_invocations.append(invocation) + + def verify(self, mock): + """ + Central method of InOrder class. + Use this method to verify the calling order of observed mocks. + :param mock: mock to verify the ordered invocation + + """ + + if mock not in self._mocks: + raise VerificationError( + f"InOrder Verification Error! " + f"Unexpected call from not observed {mock.mocked_obj}." + ) + + if not self.ordered_invocations: + raise VerificationError( + f"Trying to verify ordered invocation of {mock.mocked_obj}, " + f"but no other invocations have been recorded." + ) + invocation = self.ordered_invocations.popleft() + called_mock = invocation.mock + + expected_mock = mock_registry.mock_for(mock) + if called_mock != expected_mock: + raise VerificationError( + f"InOrder verification error! " + f"Wanted a call from {mock}, but " + f"got {invocation} from {called_mock} instead!" + ) + return verify_main(obj=mock, atleast=1, inorder=True) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + for mock in self._mocks: + mock.detach(self) diff --git a/mockito/mocking.py b/mockito/mocking.py index aed2741..72f3158 100644 --- a/mockito/mocking.py +++ b/mockito/mocking.py @@ -51,7 +51,7 @@ def remembered_invocation_builder( return invoc(*args, **kwargs) -class Mock(object): +class Mock: def __init__( self, mocked_obj: object, @@ -69,8 +69,18 @@ def __init__( self._methods_to_unstub: dict[str, Callable | None] = {} self._signatures_store: dict[str, signature.Signature | None] = {} + self._observers: list = [] + + def attach(self, observer) -> None: + self._observers.append(observer) + + def detach(self, observer) -> None: + self._observers.remove(observer) + def remember(self, invocation: invocation.RealInvocation) -> None: self.invocations.append(invocation) + for observer in self._observers: + observer.update(invocation) def finish_stubbing( self, stubbed_invocation: invocation.StubbedInvocation diff --git a/tests/in_order_test.py b/tests/in_order_test.py new file mode 100644 index 0000000..d695464 --- /dev/null +++ b/tests/in_order_test.py @@ -0,0 +1,109 @@ +import pytest + +from mockito import mock, VerificationError +from mockito.inorder import InOrder +from mockito import verify + +def test_observing_the_same_mock_twice_should_raise(): + a = mock() + with pytest.raises(ValueError) as e: + InOrder(a, a) + assert str(e.value) == ("The following Mocks are duplicated: " + f"['{a}']") + +def test_correct_order_declaration_should_pass(): + cat = mock() + dog = mock() + + in_order: InOrder = InOrder(cat, dog) + cat.meow() + dog.bark() + + in_order.verify(cat).meow() + in_order.verify(dog).bark() + + +def test_incorrect_order_declaration_should_fail(): + dog = mock() + cat = mock() + + in_order: InOrder = InOrder(cat, dog) + dog.bark() + cat.meow() + + with pytest.raises(VerificationError) as e: + in_order.verify(cat).meow() + # assert str(e.value) == ( + # "InOrder verification error! " + # f"Wanted a call from {cat}, but got " + # f"bark() from {dog} instead!" + # ) + + +def test_verifing_not_observed_mocks_should_raise(): + cat = mock() + to_ignore = mock() + + in_order: InOrder = InOrder(cat) + to_ignore.bark() + + with pytest.raises(VerificationError) as e: + in_order.verify(to_ignore).bark() + assert str(e.value) == ( + f"InOrder Verification Error! " + f"Unexpected call from not observed {to_ignore.mocked_obj}." + ) + +def test_can_verify_multiple_orders(): + cat = mock() + dog = mock() + + + in_order: InOrder = InOrder(cat, dog) + cat.meow() + dog.bark() + cat.meow() + + in_order.verify(cat).meow() + in_order.verify(dog).bark() + in_order.verify(cat).meow() + +def test_can_verify_multiple_arguments(): + cat = mock() + dog = mock() + + in_order: InOrder = InOrder(cat, dog) + cat.meow("Meow!") + dog.bark() + cat.meow("Rrrr") + + in_order.verify(cat).meow("Meow!") + in_order.verify(dog).bark() + in_order.verify(cat).meow("Rrrr") + +def test_in_order_context_manager(): + cat = mock() + dog = mock() + + with InOrder(cat, dog) as in_order: + cat.meow() + dog.bark() + + in_order.verify(cat).meow() + in_order.verify(dog).bark() + + +def test_exiting_context_manager_should_detatch_mocks(): + cat = mock() + dog = mock() + + with InOrder(cat, dog) as in_order: + cat.meow() + dog.bark() + + in_order.verify(cat).meow() + in_order.verify(dog).bark() + + # can still verify after leaving the context manager + verify(cat).meow() + verify(dog).bark() From 04211367470d7476a5d9ef55968d37c19e2476b6 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Mon, 17 Nov 2025 11:03:11 +0100 Subject: [PATCH 02/11] Ensure invocations do not get recorded after detach --- tests/in_order_test.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/in_order_test.py b/tests/in_order_test.py index d695464..a6ba413 100644 --- a/tests/in_order_test.py +++ b/tests/in_order_test.py @@ -105,5 +105,14 @@ def test_exiting_context_manager_should_detatch_mocks(): in_order.verify(dog).bark() # can still verify after leaving the context manager - verify(cat).meow() + verify(cat, times=1).meow() verify(dog).bark() + + +def test_do_not_record_after_detach(): + cat = mock() + with InOrder(cat) as in_order: + pass + cat.meow() + with pytest.raises(VerificationError): + in_order.verify(cat).meow() From c5c10b1705fe3f96972781729e8468cc6a7b2eaa Mon Sep 17 00:00:00 2001 From: herr kaste Date: Mon, 17 Nov 2025 23:07:21 +0100 Subject: [PATCH 03/11] Have only the public objects in the error messages ... still doesn't make a good error message --- mockito/inorder.py | 9 ++++++--- tests/in_order_test.py | 12 ++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/mockito/inorder.py b/mockito/inorder.py index 75fd500..90e3391 100644 --- a/mockito/inorder.py +++ b/mockito/inorder.py @@ -66,23 +66,26 @@ def verify(self, mock): if mock not in self._mocks: raise VerificationError( f"InOrder Verification Error! " - f"Unexpected call from not observed {mock.mocked_obj}." + f"Unexpected call from not observed {mock}." ) if not self.ordered_invocations: raise VerificationError( - f"Trying to verify ordered invocation of {mock.mocked_obj}, " + f"Trying to verify ordered invocation of {mock}, " f"but no other invocations have been recorded." ) invocation = self.ordered_invocations.popleft() called_mock = invocation.mock + # Basically we need a reverse map here. + # `mock_for` is a find_mock_for_obj and we do a find_obj_for_mock + obj = next(o for o, m in mock_registry.mocks._store if m == called_mock) expected_mock = mock_registry.mock_for(mock) if called_mock != expected_mock: raise VerificationError( f"InOrder verification error! " f"Wanted a call from {mock}, but " - f"got {invocation} from {called_mock} instead!" + f"got {invocation} from {obj} instead!" ) return verify_main(obj=mock, atleast=1, inorder=True) diff --git a/tests/in_order_test.py b/tests/in_order_test.py index a6ba413..e844364 100644 --- a/tests/in_order_test.py +++ b/tests/in_order_test.py @@ -33,11 +33,11 @@ def test_incorrect_order_declaration_should_fail(): with pytest.raises(VerificationError) as e: in_order.verify(cat).meow() - # assert str(e.value) == ( - # "InOrder verification error! " - # f"Wanted a call from {cat}, but got " - # f"bark() from {dog} instead!" - # ) + assert str(e.value) == ( + "InOrder verification error! " + f"Wanted a call from {cat}, but got " + f"bark() from {dog} instead!" + ) def test_verifing_not_observed_mocks_should_raise(): @@ -51,7 +51,7 @@ def test_verifing_not_observed_mocks_should_raise(): in_order.verify(to_ignore).bark() assert str(e.value) == ( f"InOrder Verification Error! " - f"Unexpected call from not observed {to_ignore.mocked_obj}." + f"Unexpected call from not observed {to_ignore}." ) def test_can_verify_multiple_orders(): From d50c78d44794c48dcace9afcb865766b5106c13f Mon Sep 17 00:00:00 2001 From: herr kaste Date: Tue, 18 Nov 2025 09:40:29 +0100 Subject: [PATCH 04/11] Tune error messages to not begin with just a newline --- mockito/inorder.py | 10 ++++------ tests/in_order_test.py | 8 +++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/mockito/inorder.py b/mockito/inorder.py index 90e3391..860182f 100644 --- a/mockito/inorder.py +++ b/mockito/inorder.py @@ -41,7 +41,7 @@ def __init__(self, *mocks: Mock): duplicates = [d for d, freq in counter.items() if freq > 1] if duplicates: raise ValueError( - f"The following Mocks are duplicated: " + f"\nThe following Mocks are duplicated: " f"{[str(d) for d in duplicates]}" ) self._mocks = mocks @@ -65,13 +65,12 @@ def verify(self, mock): if mock not in self._mocks: raise VerificationError( - f"InOrder Verification Error! " - f"Unexpected call from not observed {mock}." + f"\nUnexpected call from not observed {mock}." ) if not self.ordered_invocations: raise VerificationError( - f"Trying to verify ordered invocation of {mock}, " + f"\nTrying to verify ordered invocation of {mock}, " f"but no other invocations have been recorded." ) invocation = self.ordered_invocations.popleft() @@ -83,8 +82,7 @@ def verify(self, mock): expected_mock = mock_registry.mock_for(mock) if called_mock != expected_mock: raise VerificationError( - f"InOrder verification error! " - f"Wanted a call from {mock}, but " + f"\nWanted a call from {mock}, but " f"got {invocation} from {obj} instead!" ) return verify_main(obj=mock, atleast=1, inorder=True) diff --git a/tests/in_order_test.py b/tests/in_order_test.py index e844364..6b795e3 100644 --- a/tests/in_order_test.py +++ b/tests/in_order_test.py @@ -8,7 +8,7 @@ def test_observing_the_same_mock_twice_should_raise(): a = mock() with pytest.raises(ValueError) as e: InOrder(a, a) - assert str(e.value) == ("The following Mocks are duplicated: " + assert str(e.value) == ("\nThe following Mocks are duplicated: " f"['{a}']") def test_correct_order_declaration_should_pass(): @@ -34,8 +34,7 @@ def test_incorrect_order_declaration_should_fail(): with pytest.raises(VerificationError) as e: in_order.verify(cat).meow() assert str(e.value) == ( - "InOrder verification error! " - f"Wanted a call from {cat}, but got " + f"\nWanted a call from {cat}, but got " f"bark() from {dog} instead!" ) @@ -50,8 +49,7 @@ def test_verifing_not_observed_mocks_should_raise(): with pytest.raises(VerificationError) as e: in_order.verify(to_ignore).bark() assert str(e.value) == ( - f"InOrder Verification Error! " - f"Unexpected call from not observed {to_ignore}." + f"\nUnexpected call from not observed {to_ignore}." ) def test_can_verify_multiple_orders(): From 6f969e2f043214dca5b499ab355cac5f4df2390b Mon Sep 17 00:00:00 2001 From: herr kaste Date: Tue, 18 Nov 2025 10:07:33 +0100 Subject: [PATCH 05/11] Fix: `InOrder` and `verify` must accept `object` --- mockito/inorder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mockito/inorder.py b/mockito/inorder.py index 860182f..01fd37d 100644 --- a/mockito/inorder.py +++ b/mockito/inorder.py @@ -36,7 +36,7 @@ def verify(object, *args, **kwargs): class InOrder: - def __init__(self, *mocks: Mock): + def __init__(self, *mocks: object): counter = Counter(mocks) duplicates = [d for d, freq in counter.items() if freq > 1] if duplicates: @@ -55,7 +55,7 @@ def __init__(self, *mocks: Mock): def update(self, invocation: RealInvocation) -> None: self.ordered_invocations.append(invocation) - def verify(self, mock): + def verify(self, mock: object): """ Central method of InOrder class. Use this method to verify the calling order of observed mocks. From 7424d5e1155d59637c761b117ee480c88cdc0305 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Tue, 18 Nov 2025 10:08:59 +0100 Subject: [PATCH 06/11] Change error message to '.' style --- mockito/inorder.py | 2 +- tests/in_order_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mockito/inorder.py b/mockito/inorder.py index 01fd37d..92e3732 100644 --- a/mockito/inorder.py +++ b/mockito/inorder.py @@ -83,7 +83,7 @@ def verify(self, mock: object): if called_mock != expected_mock: raise VerificationError( f"\nWanted a call from {mock}, but " - f"got {invocation} from {obj} instead!" + f"got {obj}.{invocation} instead!" ) return verify_main(obj=mock, atleast=1, inorder=True) diff --git a/tests/in_order_test.py b/tests/in_order_test.py index 6b795e3..bc39605 100644 --- a/tests/in_order_test.py +++ b/tests/in_order_test.py @@ -35,7 +35,7 @@ def test_incorrect_order_declaration_should_fail(): in_order.verify(cat).meow() assert str(e.value) == ( f"\nWanted a call from {cat}, but got " - f"bark() from {dog} instead!" + f"{dog}.bark() instead!" ) From 3e72c4a0affbf209f7e3f5519a963f883b2aab19 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Tue, 18 Nov 2025 10:43:49 +0100 Subject: [PATCH 07/11] Get the basic messages right --- mockito/inorder.py | 34 +++++++++++++++++++++-------- tests/in_order_test.py | 49 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/mockito/inorder.py b/mockito/inorder.py index 92e3732..0f8ba0a 100644 --- a/mockito/inorder.py +++ b/mockito/inorder.py @@ -62,28 +62,42 @@ def verify(self, mock: object): :param mock: mock to verify the ordered invocation """ + expected_mock = mock_registry.mock_for(mock) + if expected_mock is None: + raise VerificationError( + f"\n{mock} is not setup with any stubbings or expectations." + ) if mock not in self._mocks: raise VerificationError( - f"\nUnexpected call from not observed {mock}." + f"\n{mock} is not part of that InOrder." ) if not self.ordered_invocations: raise VerificationError( - f"\nTrying to verify ordered invocation of {mock}, " - f"but no other invocations have been recorded." + "\nThere are no recorded invocations." ) - invocation = self.ordered_invocations.popleft() - called_mock = invocation.mock + + # Find the next invocation in global order that hasn't been used + # for "in-order" verification yet. + next_invocation = next( + (inv for inv in self.ordered_invocations if not inv.verified_inorder), + None, + ) + if next_invocation is None: + raise VerificationError( + "\nThere are no more recorded invocations." + ) + + called_mock = next_invocation.mock # Basically we need a reverse map here. # `mock_for` is a find_mock_for_obj and we do a find_obj_for_mock obj = next(o for o, m in mock_registry.mocks._store if m == called_mock) - expected_mock = mock_registry.mock_for(mock) if called_mock != expected_mock: raise VerificationError( f"\nWanted a call from {mock}, but " - f"got {obj}.{invocation} instead!" + f"got {obj}.{next_invocation} instead!" ) return verify_main(obj=mock, atleast=1, inorder=True) @@ -91,5 +105,7 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): - for mock in self._mocks: - mock.detach(self) + for obj in self._mocks: + m = mock_registry.mock_for(obj) + if m: + m.detach(self) diff --git a/tests/in_order_test.py b/tests/in_order_test.py index bc39605..1d129cf 100644 --- a/tests/in_order_test.py +++ b/tests/in_order_test.py @@ -1,9 +1,18 @@ import pytest -from mockito import mock, VerificationError +from mockito import expect, mock, VerificationError from mockito.inorder import InOrder from mockito import verify + +class Dog: + def say(self, what): + return what + + def __str__(self): + return "" + + def test_observing_the_same_mock_twice_should_raise(): a = mock() with pytest.raises(ValueError) as e: @@ -39,6 +48,42 @@ def test_incorrect_order_declaration_should_fail(): ) +def test_error_message_for_unknown_objects(): + bob = Dog() + bob.say("Grrr!") + with InOrder(bob) as in_order: + with pytest.raises(VerificationError) as e: + in_order.verify(bob).say("Wuff!") + assert str(e.value) == ( + f"\n{bob} is not setup with any stubbings or expectations." + ) + +def test_error_message_if_queue_was_never_not_empty(): + bob = Dog() + expect(bob).say(...) + with InOrder(bob) as in_order: + with pytest.raises(VerificationError) as e: + in_order.verify(bob).say(...) + + assert str(e.value) == ( + "\nThere are no recorded invocations." + ) + +def test_error_message_if_queue_is_empty(): + bob = Dog() + rob = Dog() + expect(bob).say(...) + expect(rob).say(...) + with InOrder(bob, rob) as in_order: + bob.say("Wuff!") + in_order.verify(bob).say(...) + with pytest.raises(VerificationError) as e: + in_order.verify(rob).say(...) + + assert str(e.value) == ( + "\nThere are no more recorded invocations." + ) + def test_verifing_not_observed_mocks_should_raise(): cat = mock() to_ignore = mock() @@ -49,7 +94,7 @@ def test_verifing_not_observed_mocks_should_raise(): with pytest.raises(VerificationError) as e: in_order.verify(to_ignore).bark() assert str(e.value) == ( - f"\nUnexpected call from not observed {to_ignore}." + f"\n{to_ignore} is not part of that InOrder." ) def test_can_verify_multiple_orders(): From 886fe8fc57f17e5896bf8b3dadcc1b00a6ddf0ec Mon Sep 17 00:00:00 2001 From: herr kaste Date: Tue, 18 Nov 2025 10:51:39 +0100 Subject: [PATCH 08/11] Get all the variable names right --- mockito/inorder.py | 43 ++++++++++++++++++++-------------------- mockito/mock_registry.py | 9 +++++++++ 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/mockito/inorder.py b/mockito/inorder.py index 0f8ba0a..8c36091 100644 --- a/mockito/inorder.py +++ b/mockito/inorder.py @@ -36,18 +36,17 @@ def verify(object, *args, **kwargs): class InOrder: - def __init__(self, *mocks: object): - counter = Counter(mocks) + def __init__(self, *objects: object): + counter = Counter(objects) duplicates = [d for d, freq in counter.items() if freq > 1] if duplicates: raise ValueError( f"\nThe following Mocks are duplicated: " f"{[str(d) for d in duplicates]}" ) - self._mocks = mocks - for mock in mocks: - m = mock_registry.mock_for(mock) - if m: + self._objects = objects + for obj in objects: + if m := mock_registry.mock_for(obj): m.attach(self) self.ordered_invocations: Deque[RealInvocation] = deque() @@ -55,22 +54,22 @@ def __init__(self, *mocks: object): def update(self, invocation: RealInvocation) -> None: self.ordered_invocations.append(invocation) - def verify(self, mock: object): + def verify(self, obj: object): """ Central method of InOrder class. Use this method to verify the calling order of observed mocks. - :param mock: mock to verify the ordered invocation + :param obj: obj to verify the ordered invocation """ - expected_mock = mock_registry.mock_for(mock) + expected_mock = mock_registry.mock_for(obj) if expected_mock is None: raise VerificationError( - f"\n{mock} is not setup with any stubbings or expectations." + f"\n{obj} is not setup with any stubbings or expectations." ) - if mock not in self._mocks: + if obj not in self._objects: raise VerificationError( - f"\n{mock} is not part of that InOrder." + f"\n{obj} is not part of that InOrder." ) if not self.ordered_invocations: @@ -90,22 +89,22 @@ def verify(self, mock: object): ) called_mock = next_invocation.mock - # Basically we need a reverse map here. - # `mock_for` is a find_mock_for_obj and we do a find_obj_for_mock - obj = next(o for o, m in mock_registry.mocks._store if m == called_mock) - if called_mock != expected_mock: + called_obj = mock_registry.obj_for(called_mock) + if called_obj is None: + raise RuntimeError( + f"{called_mock} is not in the registry (anymore)." + ) raise VerificationError( - f"\nWanted a call from {mock}, but " - f"got {obj}.{next_invocation} instead!" + f"\nWanted a call from {obj}, but " + f"got {called_obj}.{next_invocation} instead!" ) - return verify_main(obj=mock, atleast=1, inorder=True) + return verify_main(obj=obj, atleast=1, inorder=True) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): - for obj in self._mocks: - m = mock_registry.mock_for(obj) - if m: + for obj in self._objects: + if m := mock_registry.mock_for(obj): m.detach(self) diff --git a/mockito/mock_registry.py b/mockito/mock_registry.py index 638442c..3e8e4d6 100644 --- a/mockito/mock_registry.py +++ b/mockito/mock_registry.py @@ -41,6 +41,9 @@ def register(self, obj: object, mock: Mock) -> None: def mock_for(self, obj: object) -> Mock | None: return self.mocks.get(obj, None) + def obj_for(self, mock: Mock) -> object | None: + return self.mocks.lookup(mock) + def unstub(self, obj: object) -> None: try: mock = self.mocks.pop(obj) @@ -84,6 +87,12 @@ def get(self, key, default=None): return value return default + def lookup(self, value, default=None): + for key, v in self._store: + if v is value: + return key + return default + def values(self): return [v for k, v in self._store] From 21a269b75848837a38fe87f156ef347aea5975fb Mon Sep 17 00:00:00 2001 From: herr kaste Date: Tue, 18 Nov 2025 12:21:27 +0100 Subject: [PATCH 09/11] Towards the standard verify signature --- mockito/inorder.py | 108 ++++++++++++++++++++++++++++++++--------- mockito/mockito.py | 16 ++++-- tests/in_order_test.py | 68 ++++++++++++++++++++++++-- 3 files changed, 162 insertions(+), 30 deletions(-) diff --git a/mockito/inorder.py b/mockito/inorder.py index 8c36091..9b787f5 100644 --- a/mockito/inorder.py +++ b/mockito/inorder.py @@ -20,12 +20,16 @@ from __future__ import annotations from collections import Counter, deque -from typing import Tuple, Deque +from functools import partial +from typing import Deque from .verification import VerificationError -from .invocation import RealInvocation -from .mocking import Mock -from .mockito import verify as verify_main +from .invocation import ( + RealInvocation, + VerifiableInvocation, + verification_has_lower_bound_of_zero, +) +from .mockito import ArgumentError, verify as verify_main from .mock_registry import mock_registry @@ -54,7 +58,14 @@ def __init__(self, *objects: object): def update(self, invocation: RealInvocation) -> None: self.ordered_invocations.append(invocation) - def verify(self, obj: object): + def verify( + self, + obj: object, + times=None, + atleast=None, + atmost=None, + between=None, + ): """ Central method of InOrder class. Use this method to verify the calling order of observed mocks. @@ -63,48 +74,99 @@ def verify(self, obj: object): """ expected_mock = mock_registry.mock_for(obj) if expected_mock is None: - raise VerificationError( + raise ArgumentError( f"\n{obj} is not setup with any stubbings or expectations." ) if obj not in self._objects: - raise VerificationError( + raise ArgumentError( f"\n{obj} is not part of that InOrder." ) - if not self.ordered_invocations: + return verify_main( + obj=obj, + times=times, + atleast=atleast, + atmost=atmost, + between=between, + _factory=partial(InOrderVerifiableInvocation, inorder=self), + ) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + for obj in self._objects: + if m := mock_registry.mock_for(obj): + m.detach(self) + + +class InOrderVerifiableInvocation(VerifiableInvocation): + def __init__(self, mock, method_name, verification, inorder: InOrder): + super().__init__(mock, method_name, verification) + self._inorder = inorder + + def __call__(self, *params, **named_params): # noqa: C901 + self._remember_params(params, named_params) + + ordered = self._inorder.ordered_invocations + + if not ordered: raise VerificationError( "\nThere are no recorded invocations." ) - # Find the next invocation in global order that hasn't been used + # Find first invocation in global order that hasn't been used # for "in-order" verification yet. - next_invocation = next( - (inv for inv in self.ordered_invocations if not inv.verified_inorder), - None, - ) - if next_invocation is None: + try: + start_idx, next_invocation = next( + (i, inv) + for i, inv in enumerate(ordered) + if not inv.verified_inorder + ) + except StopIteration: raise VerificationError( "\nThere are no more recorded invocations." ) called_mock = next_invocation.mock - if called_mock != expected_mock: + if called_mock is not self.mock: called_obj = mock_registry.obj_for(called_mock) if called_obj is None: raise RuntimeError( f"{called_mock} is not in the registry (anymore)." ) + expected_obj = mock_registry.obj_for(self.mock) raise VerificationError( - f"\nWanted a call from {obj}, but " + f"\nWanted a call from {expected_obj}, but " f"got {called_obj}.{next_invocation} instead!" ) - return verify_main(obj=obj, atleast=1, inorder=True) - def __enter__(self): - return self + matched_invocations = [] - def __exit__(self, exc_type, exc_val, exc_tb): - for obj in self._objects: - if m := mock_registry.mock_for(obj): - m.detach(self) + # Walk the contiguous block of this mock in the global queue. + for inv in list(ordered)[start_idx:]: + if inv.verified_inorder: + continue + if inv.mock is not self.mock: + break + + if not self.matches(inv): + raise VerificationError( + "\nWanted %s to be invoked,\n" + "got %s instead." % (self, inv) + ) + + self.capture_arguments(inv) + matched_invocations.append(inv) + + self.verification.verify(self, len(matched_invocations)) + + for inv in matched_invocations: + inv.verified = True + inv.verified_inorder = True + + if verification_has_lower_bound_of_zero(self.verification): + for stub in self.mock.stubbed_invocations: + if stub.matches(self) or self.matches(stub): + stub.allow_zero_invocations = True diff --git a/mockito/mockito.py b/mockito/mockito.py index 759533a..e136ee4 100644 --- a/mockito/mockito.py +++ b/mockito/mockito.py @@ -108,8 +108,15 @@ def _get_mock_or_raise(obj: object) -> Mock: raise ArgumentError("obj '%s' is not registered" % obj) return theMock -def verify(obj, times=None, atleast=None, atmost=None, between=None, - inorder=False): +def verify( + obj, + times=None, + atleast=None, + atmost=None, + between=None, + inorder=False, + _factory=None, +): """Central interface to verify interactions. `verify` uses a fluent interface:: @@ -145,10 +152,11 @@ def verify(obj, times=None, atleast=None, atmost=None, between=None, theMock = _get_mock_or_raise(obj) + factory = _factory or invocation.VerifiableInvocation + class Verify(object): def __getattr__(self, method_name): - return invocation.VerifiableInvocation( - theMock, method_name, verification_fn) + return factory(theMock, method_name, verification_fn) return Verify() diff --git a/tests/in_order_test.py b/tests/in_order_test.py index 1d129cf..459e72c 100644 --- a/tests/in_order_test.py +++ b/tests/in_order_test.py @@ -1,6 +1,6 @@ import pytest -from mockito import expect, mock, VerificationError +from mockito import expect, mock, ArgumentError, VerificationError from mockito.inorder import InOrder from mockito import verify @@ -52,7 +52,7 @@ def test_error_message_for_unknown_objects(): bob = Dog() bob.say("Grrr!") with InOrder(bob) as in_order: - with pytest.raises(VerificationError) as e: + with pytest.raises(ArgumentError) as e: in_order.verify(bob).say("Wuff!") assert str(e.value) == ( f"\n{bob} is not setup with any stubbings or expectations." @@ -91,7 +91,7 @@ def test_verifing_not_observed_mocks_should_raise(): in_order: InOrder = InOrder(cat) to_ignore.bark() - with pytest.raises(VerificationError) as e: + with pytest.raises(ArgumentError) as e: in_order.verify(to_ignore).bark() assert str(e.value) == ( f"\n{to_ignore} is not part of that InOrder." @@ -159,3 +159,65 @@ def test_do_not_record_after_detach(): cat.meow() with pytest.raises(VerificationError): in_order.verify(cat).meow() + + +def test_in_order_verify_times_across_mocks(): + cat = mock() + dog = mock() + + in_order: InOrder = InOrder(cat, dog) + cat.meow() + dog.bark() + cat.meow() + cat.meow() + + in_order.verify(cat, times=1).meow() + in_order.verify(dog).bark() + in_order.verify(cat, times=2).meow() + + +def test_in_order_verify_atleast(): + cat = mock() + dog = mock() + + in_order: InOrder = InOrder(cat, dog) + cat.meow() + cat.meow() + cat.meow() + dog.bark() + cat.meow() + + in_order.verify(cat, atleast=2).meow() + in_order.verify(dog).bark() + in_order.verify(cat, atleast=1).meow() + + +def test_in_order_verify_atmost(): + cat = mock() + dog = mock() + + in_order: InOrder = InOrder(cat, dog) + cat.meow() + cat.meow() + dog.bark() + cat.meow() + + in_order.verify(cat, atmost=2).meow() + in_order.verify(dog).bark() + in_order.verify(cat, atmost=1).meow() + + +def test_in_order_verify_between(): + cat = mock() + dog = mock() + + in_order: InOrder = InOrder(cat, dog) + cat.meow() + cat.meow() + dog.bark() + cat.meow() + cat.meow() + + in_order.verify(cat, between=(1, 3)).meow() + in_order.verify(dog).bark() + in_order.verify(cat, between=(1, 3)).meow() From a36a036c7670bd6540171b84f1f38693ae74c580 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Tue, 18 Nov 2025 12:47:26 +0100 Subject: [PATCH 10/11] Allow multiple entrance --- mockito/inorder.py | 9 ++++++--- mockito/mocking.py | 8 ++++++-- tests/in_order_test.py | 11 +++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/mockito/inorder.py b/mockito/inorder.py index 9b787f5..fadb0b4 100644 --- a/mockito/inorder.py +++ b/mockito/inorder.py @@ -49,12 +49,14 @@ def __init__(self, *objects: object): f"{[str(d) for d in duplicates]}" ) self._objects = objects - for obj in objects: + self._attach_all() + self.ordered_invocations: Deque[RealInvocation] = deque() + + def _attach_all(self): + for obj in self._objects: if m := mock_registry.mock_for(obj): m.attach(self) - self.ordered_invocations: Deque[RealInvocation] = deque() - def update(self, invocation: RealInvocation) -> None: self.ordered_invocations.append(invocation) @@ -93,6 +95,7 @@ def verify( ) def __enter__(self): + self._attach_all() return self def __exit__(self, exc_type, exc_val, exc_tb): diff --git a/mockito/mocking.py b/mockito/mocking.py index 72f3158..823d981 100644 --- a/mockito/mocking.py +++ b/mockito/mocking.py @@ -72,10 +72,14 @@ def __init__( self._observers: list = [] def attach(self, observer) -> None: - self._observers.append(observer) + if observer not in self._observers: + self._observers.append(observer) def detach(self, observer) -> None: - self._observers.remove(observer) + try: + self._observers.remove(observer) + except ValueError: + pass def remember(self, invocation: invocation.RealInvocation) -> None: self.invocations.append(invocation) diff --git a/tests/in_order_test.py b/tests/in_order_test.py index 459e72c..2409f86 100644 --- a/tests/in_order_test.py +++ b/tests/in_order_test.py @@ -161,6 +161,17 @@ def test_do_not_record_after_detach(): in_order.verify(cat).meow() +def test_allow_double_entrance(): + cat = mock() + in_order = InOrder(cat) + with in_order: + pass + cat.meow() + with in_order: + cat.meow() + in_order.verify(cat, times=1).meow() + + def test_in_order_verify_times_across_mocks(): cat = mock() dog = mock() From 07df1231d86d8aed7d1e0adfedce17fd41f0bb88 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Tue, 18 Nov 2025 13:03:29 +0100 Subject: [PATCH 11/11] Ensure the constructor works with unhashable objects --- mockito/inorder.py | 16 +++++++--------- tests/in_order_test.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/mockito/inorder.py b/mockito/inorder.py index fadb0b4..0d59cfa 100644 --- a/mockito/inorder.py +++ b/mockito/inorder.py @@ -19,7 +19,7 @@ # THE SOFTWARE. from __future__ import annotations -from collections import Counter, deque +from collections import deque from functools import partial from typing import Deque @@ -41,14 +41,12 @@ def verify(object, *args, **kwargs): class InOrder: def __init__(self, *objects: object): - counter = Counter(objects) - duplicates = [d for d, freq in counter.items() if freq > 1] - if duplicates: - raise ValueError( - f"\nThe following Mocks are duplicated: " - f"{[str(d) for d in duplicates]}" - ) - self._objects = objects + objects_ = [] + for obj in objects: + if obj in objects_: + raise ValueError(f"{obj} is provided more than once") + objects_.append(obj) + self._objects = objects_ self._attach_all() self.ordered_invocations: Deque[RealInvocation] = deque() diff --git a/tests/in_order_test.py b/tests/in_order_test.py index 2409f86..a4b1630 100644 --- a/tests/in_order_test.py +++ b/tests/in_order_test.py @@ -17,8 +17,14 @@ def test_observing_the_same_mock_twice_should_raise(): a = mock() with pytest.raises(ValueError) as e: InOrder(a, a) - assert str(e.value) == ("\nThe following Mocks are duplicated: " - f"['{a}']") + assert str(e.value) == f"{a} is provided more than once" + + +def test_observing_the_same_mock_twice_should_raise_unhashable_obj(): + a = dict() # type: ignore[var-annotated] + with pytest.raises(ValueError): + InOrder(a, a) + def test_correct_order_declaration_should_pass(): cat = mock()