-
-
Notifications
You must be signed in to change notification settings - Fork 13
Inorder implementation #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
I should not use |
|
Very promising. For the tests:
|
|
(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.) |
|
Okay, I'edit the set up to use a function that calls both the interfaces
I'll also test non specs mocks
I'll try to implement it in TDD
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 |
mockito/mocking.py
Outdated
|
|
||
| def remember(self, invocation): | ||
| self.invocations.append(invocation) | ||
| self.notify() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
☝️
|
Yeah, that should probably not raise. Nesting should ideally work, we need to ensure instead that nesting works. |
|
Added a testing library to express better assertions with containers...I'm working in the various steps rye add --dev assertpy |
|
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 Already think about using |
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. 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 |
|
@kaste Hello, what do you think? Do you think that we should support a workflow like this?: a = mock()
b = mock()
when(a).method().thenReturn("Calling a")
when(b).other_method().thenReturn("Calling b")
in_order: InOrder = InOrder([a,b])
a.method()
b.other_method()
a.method()
in_order.verify(a).method()
in_order.verify(b).other_method()
in_order.verify(a).method()For now it is not supported. Best regards |
|
Isn't that the meat of the re-implementation of InOrder? The old InOrder supported only one mock, and your implementation supports proper cross-mock interactions? |
26ca517 to
70a7c8a
Compare
|
I pushed master to update to modern tooling ( |
My implementation supports cross-mock interaction like in_order.verify(cat).meow()
in_order.verify(dof).bark()but doesn't yet supports an interaction of this type: in_order.verify(cat).meow()
in_order.verify(dof).bark()
in_order.verify(cat).meow()Where a mock method is called twice (or more) in different orders.
I'll looking into that |
|
@kaste I' happy with the implementation and tests. Let me know if you need any changes or some documentation to put in the docs with examples (and how you wanted). P.S. |
mockito/inorder.py
Outdated
| def mocks(self): | ||
| return self._mocks | ||
|
|
||
| def update(self, subject: Mock) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-> remember(self, invocation: RealInvocation) -> None
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why you haven't done this?
mockito/inorder.py
Outdated
| :param subject: subject to be added to the list of ordered invocation | ||
| """ | ||
| self.ordered_invocations.append( | ||
| (subject, subject.invocations[-1]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
subject would have been too generic anyway, but subject.invocations[-1] is an implementation detail. Just pass the invocation in. self.invocations: deque[RealInvocations]
mockito/mocking.py
Outdated
| RealInvocation = Union[ | ||
| invocation.RememberedInvocation, | ||
| invocation.RememberedProxyInvocation | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RealInvocation is actually defined in invocation.py
tests/in_order_test.py
Outdated
| a = mock() | ||
| b = mock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't use a and b as names. Use cat and dog or some other names.
tests/in_order_test.py
Outdated
| f"Called {mock_registry.mock_for(a)}, " | ||
| f"but expected {mock_registry.mock_for(b)}!") |
There was a problem hiding this comment.
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.
What is the purpose of operator |
|
Things I can't do
|
mockito/inorder.py
Outdated
| """ | ||
|
|
||
| if not (mock in self.mocks): |
There was a problem hiding this comment.
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:
mockito/inorder.py
Outdated
| raise VerificationError( | ||
| f"InOrder verification error! " | ||
| f"Wanted a call from {str(expected_mock)}, but " | ||
| f"got {invocation} from {str(called_mock)} instead!" |
There was a problem hiding this comment.
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.
mockito/mocking.py
Outdated
| def __str__(self): | ||
| name: str = 'Dummy' | ||
| if self.spec: | ||
| name = self.spec.__name__ | ||
| return f"Mock<{name}>" |
There was a problem hiding this comment.
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.
mockito/mocking.py
Outdated
| def __str__(self): | ||
| name = 'Dummy' | ||
| if spec: | ||
| name = spec.__name__ | ||
| return f"Mock<{name}>" | ||
|
|
There was a problem hiding this comment.
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?
tests/in_order_test.py
Outdated
| with pytest.raises(ValueError) as e: | ||
| InOrder(a, a) | ||
| assert str(e.value) == ("The following Mocks are duplicated: " | ||
| "['Mock<Dummy>']") |
There was a problem hiding this comment.
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.
tests/in_order_test.py
Outdated
| "Wanted a call from Mock<Dummy>, but got " | ||
| "bark() from Mock<Dummy> instead!" |
There was a problem hiding this comment.
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.
| 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() | ||
|
|
There was a problem hiding this comment.
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.
mockito/inorder.py
Outdated
| if not self.ordered_invocations: | ||
| raise VerificationError( | ||
| f"Trying to verify ordered invocation of {mock}, " | ||
| f"but no other invocations have been recorded." |
There was a problem hiding this comment.
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.
mockito/inorder.py
Outdated
| expected_mock = mock_registry.mock_for(mock) | ||
| if called_mock != expected_mock: | ||
| raise VerificationError( | ||
| f"InOrder verification error! " |
There was a problem hiding this comment.
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".
mockito/inorder.py
Outdated
| f"Trying to verify ordered invocation of {mock}, " | ||
| f"but no other invocations have been recorded." | ||
| ) | ||
| ordered_invocation = self.ordered_invocations.popleft() |
There was a problem hiding this comment.
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.
|
77f5288 to
03f890b
Compare
|
Difficult to think this through... The standard verify should be supported at least. |
Authored-by: Christian Mancini <[email protected]> Co-authored-by: herr kaste <[email protected]>
... still doesn't make a good error message
66f219a to
07df123
Compare
This is the PR for #98.
The idea is to make
Mockobservable so we can register ordered invocations from different mocks.The
mocking_registryhelps to get the mocks for comparing.Features
InOrderobject to register the order of invocationsShould work with spy objects (Maybe in an other PR)@kaste Feel free to add elements to this list