Skip to content

Conversation

cMancio00
Copy link

@cMancio00 cMancio00 commented Sep 23, 2025

This is the PR for #98.

The idea is to make Mock observable so we can register ordered invocations from different mocks.
The mocking_registry helps to get the mocks for comparing.

Features

  • Mocks should be observable from an InOrder object to register the order of invocations
  • Trying to observe the same Mock more than once should be fine
  • Should work with specs and no-specs Mocks
    Should work with spy objects (Maybe in an other PR)
  • Mocks should be detached automatically with a context manager and also when the Queue call is empty
  • Verification errors should be meaningful

@kaste Feel free to add elements to this list

@cMancio00
Copy link
Author

cMancio00 commented Sep 23, 2025

I should not use @override annotation since is not supported before Python 3.12 and also use mypy locally first.

@kaste
Copy link
Owner

kaste commented Sep 24, 2025

Very promising.

For the tests:

  • Don't do the friends things if it only hides the actual usage of the mocks in the tests.

  • Maybe even the setup is limiting here. Sometimes we test a specced mock, sometimes
    not.

cat = mock()
in_order = InOrder([cat])
cat.meow("HI")
cat.meow("HO")

a)
in_order.verify(cat).meow("HO")  # raises

b) 
in_order.verify(cat).meow("HI")  # passes
in_order.verify(cat).meow("HO")  # passes
in_order.verify(cat).meow("HO")  # raises, no?

b1) 
in_order.verify(cat).meow("HI")  # passes
in_order.verify(cat).meow("HO")  # passes
in_order.verify(cat).meow("HI")  # raises

c)
in_order.verify(cat).meow(...)  # passes
in_order.verify(cat).meow(...)  # raises

d)
in_order.verify(cat).bark(...)  # raises
  • implement context manager to "detach" automatically.
cat = mock()
with InOrder([cat]) as in_order:
    cat.meow("HI")
cat.meow("HO")

...

  • Check the actual error messages in the tests. The most important part is to
    have good error messages. E.g. user expected bark(...) but next invocation was
    actually meow("HI").

@kaste
Copy link
Owner

kaste commented Sep 24, 2025

(I usually do the tests first. E.g what behavior do I even expect for all the cases. Do they all make sense in concerto.)

@kaste
Copy link
Owner

kaste commented Sep 24, 2025

a)
probably valid and we have two separate "lines" of recorded executions
with InOrder(cat) as a, InOrder(dog) as b:

b)
a = InOrder(cat)
b = InOrder(cat)   # raises? already registered

@cMancio00
Copy link
Author

  • Don't do the friends things if it only hides the actual usage of the mocks in the tests.

Okay, I'edit the set up to use a function that calls both the interfaces

  • Maybe even the setup is limiting here. Sometimes we test a specced mock, sometimes
    not.

I'll also test non specs mocks

(I usually do the tests first. E.g what behavior do I even expect for all the cases. Do they all make sense in concerto.)

I'll try to implement it in TDD

a = InOrder(cat)
b = InOrder(cat) # raises? already registered

I thought that if we use a set as a container for observed subjects we can not have problems with already registered objects and the scope of an InOrder object is just in a single test case


def remember(self, invocation):
self.invocations.append(invocation)
self.notify()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it reads much better when we could do, for each observer. oberserver.notify(invocation). This is semantically equivalent to: for each observer. observer.remember(invocation). As if the invocations here are a specialized variant of it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look in to it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still: for each observer. observer.remember(invocation)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️

@kaste
Copy link
Owner

kaste commented Sep 27, 2025

cat = mock()
dog = mock()

with InOrder(cat) as in_order:
    dog.bark()
    cat.meow()

in_order.verify(dog).bar()  #  raises

@kaste
Copy link
Owner

kaste commented Sep 27, 2025

a = InOrder(cat)
b = InOrder(cat) # raises? already registered

Yeah, that should probably not raise. Nesting should ideally work, we need to ensure instead that nesting works.

@cMancio00
Copy link
Author

cMancio00 commented Sep 28, 2025

Added a testing library to express better assertions with containers...I'm working in the various steps
@kaste Isn't sufficient to use

rye add --dev assertpy

@kaste
Copy link
Owner

kaste commented Sep 29, 2025

Generally, this project predates any of the modern tools, like rye, uv etc.

Don't do these distracting things here. They don't help you implement the actual thing.

For this new feature, just use pytest and plain asserts. You don't even need to subclass unittest.TestCase. Look at numpy_test.py as an example. Do not write test around in_order.mocks and in_order.ordered_invocations (=remembered_invocations). These are likely implementation details. Users shouldn't need those. Again: Try to use dumb mocks. Inline everything, don't be DRY here.

Already think about using InOrder(*mocks) to support the shorter and easier to write InOrder(mock()).

@cMancio00
Copy link
Author

Generally, this project predates any of the modern tools, like rye, uv etc.

Don't do these distracting things here. They don't help you implement the actual thing.

For this new feature, just use pytest and plain asserts. You don't even need to subclass unittest.TestCase. Look at numpy_test.py as an example. Do not write test around in_order.mocks and in_order.ordered_invocations (=remembered_invocations). These are likely implementation details. Users shouldn't need those. Again: Try to use dumb mocks. Inline everything, don't be DRY here.

Already think about using InOrder(*mocks) to support the shorter and easier to write InOrder(mock()).

I don't get it, Unit Tests should help me implementing the feature and serves other developers as a sort of documentation. Users should not reads our tests, but the documentation and the example in it (like doc tests). (In an extremis we can provide E2E tests with behave to get the high level functionality of a feature.
The assertion library that I wanted was to have better readability in the test cases and to have functions to have better tests while working with containers.

Anyway the repo is your so I'll do what you want. I'll remove the dependency, use dumb mock, inline everything (no setup like xUnit) and write the tests just with documentation purpuse

Comment on lines 77 to 85
a.method()
b.other_method()

with pytest.raises(VerificationError) as e:
in_order.verify(a).method()
in_order.verify(b).other_method()
assert str(e.value) == (f"Trying to verify ordered invocation "
f"of {b}, "
f"but no other invocations have been recorded.")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely what to_ignore is for. The error message is wrong because b is not even registered with the InOrder. verifying a is not necessary you just test for unknown/unregistered/unexpected mocks.

Comment on lines 144 to 151
with pytest.raises(VerificationError) as e:
with InOrder([a, b]) as in_order:
a.method()
b.other_method()

in_order.verify(a)
assert str(e.value) == (f"Some mocks have not been verified: "
f"{in_order.ordered_invocations}")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't raise actually. On contraire, we just detach the mocks. Test that verify works after leaving the context manager and (b) that using the mock after detach doesn't get recorded.

Copy link
Owner

@kaste kaste left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of stuff.

  • Generally, I want this verify to accept all keyword arguments as the original verify.
  • I actually had a lot of test cases in my first comment, and I don't see any of them here. Esp. you completely forgot to test different arguments. Like cat.make("meow"), cat.make("rrr") and if they can be verified. Also if verify(cat).say(...) catches all invocations.
  • The error messages should be more like e.g.

You expected
cat.meow()
but next remembered invocation is
dog.bark()

This is especially hard to do in a helpful way as we don't easily get the names cat and dog. I don't think you should expose the internal Mocks at least as these are also implementation details, in fact proxy objects. The user doesn't see them but only the mocked (wrapped) object.

Comment on lines 66 to 67
f"Called {mock_registry.mock_for(a)}, "
f"but expected {mock_registry.mock_for(b)}!")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To get a feeling for the error messages, these should be typed out. mock_for returns an internal helper anyway. So that must rather be You expected an invocation on b but the next invocation comes from a.

@cMancio00
Copy link
Author

cMancio00 commented Oct 12, 2025

  • Also if verify(cat).say(...) catches all invocations.

What is the purpose of operator ...?

@cMancio00
Copy link
Author

Things I can't do

  • Make the sign of inorder verify the same as verify because i delegate to the original with fixed arguments so changing arguments will lead to unwanted behavior
  • Things I don't understand from the past review

@cMancio00 cMancio00 requested a review from kaste October 12, 2025 17:19
"""

if not (mock in self.mocks):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if mock not in self.mocks:

raise VerificationError(
f"InOrder verification error! "
f"Wanted a call from {str(expected_mock)}, but "
f"got {invocation} from {str(called_mock)} instead!"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked you to not expose our internal Mock class. Users only care about their mocks and actually their real classes. (Actually most of the time, user just patch their real classes and don't use "mocks" at all.) If you have a Mock, most likely the user is interested in its mocked_obj and/or its spec. Our "Mock" has a proxy relation to the mocked object.

Comment on lines +259 to +263
def __str__(self):
name: str = 'Dummy'
if self.spec:
name = self.spec.__name__
return f"Mock<{name}>"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely not needed as Mock is an internal class, the standard repr implementation Python provides for free was always enough.

Comment on lines +373 to +378
def __str__(self):
name = 'Dummy'
if spec:
name = spec.__name__
return f"Mock<{name}>"

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't override the __repr__ just above that. Why is that useful?

with pytest.raises(ValueError) as e:
InOrder(a, a)
assert str(e.value) == ("The following Mocks are duplicated: "
"['Mock<Dummy>']")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you see that your new str implementation for mock isn't helpful as it is basically a static string for all dumb mocks.

Comment on lines +38 to +39
"Wanted a call from Mock<Dummy>, but got "
"bark() from Mock<Dummy> instead!"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These come from the Mock.__str__ implementation. Please note that this error message is not helpful for the end-user at all.

Ideally maybe

Wanted but not invoked cat.meow()
Next recorded invocation is: dog.bark() 

Is that too tricky?

Then maybe

Wanted a call from ...
Next recorded invocation is ...bark()

That shouldn't be that hard.

Comment on lines +96 to +106
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()

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The important thing is that invocation on cat do not get recorded after detach.

if not self.ordered_invocations:
raise VerificationError(
f"Trying to verify ordered invocation of {mock}, "
f"but no other invocations have been recorded."
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why "no other invocations".

Maybe

Wanted a call on ...
But nothing has been recorded yet.

expected_mock = mock_registry.mock_for(mock)
if called_mock != expected_mock:
raise VerificationError(
f"InOrder verification error! "
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, do not start with "InOrder verification error!" but just a newline "\n".

f"Trying to verify ordered invocation of {mock}, "
f"but no other invocations have been recorded."
)
ordered_invocation = self.ordered_invocations.popleft()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of popping them you can maybe take the next invocation that is still not .verified. IIRC verify(_main) sets that.

@kaste
Copy link
Owner

kaste commented Oct 14, 2025

  • What does it take to support the full signature of the original verify function?
  • ... means: any arguments
  • What "things" did you not understand?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants