mockito-python icon indicating copy to clipboard operation
mockito-python copied to clipboard

Inorder implementation

Open cMancio00 opened this issue 3 months ago • 19 comments

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

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

@kaste Feel free to add elements to this list

cMancio00 avatar Sep 23 '25 22:09 cMancio00

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

cMancio00 avatar Sep 23 '25 22:09 cMancio00

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 avatar Sep 24 '25 09:09 kaste

(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 avatar Sep 24 '25 09:09 kaste

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

kaste avatar Sep 24 '25 09:09 kaste

  • 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

cMancio00 avatar Sep 27 '25 13:09 cMancio00

cat = mock()
dog = mock()

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

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

kaste avatar Sep 27 '25 17:09 kaste

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.

kaste avatar Sep 27 '25 17:09 kaste

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

cMancio00 avatar Sep 28 '25 14:09 cMancio00

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()).

kaste avatar Sep 29 '25 10:09 kaste

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

cMancio00 avatar Sep 29 '25 11:09 cMancio00

@kaste Hello, what do you think? I still need to implement the context manager and the unstub.

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

cMancio00 avatar Oct 05 '25 08:10 cMancio00

Isn't that the meat of the re-implementation of InOrder?

cat = mock()
dog = mock()

when InOrder(cat, dog) as in_order:
    cat.meow()
    dog.bark()
    cat.meow()

in_order.verify(cat).meow()
in_order.verify(dof).bark()
in_order.verify(cat).meow()

The old InOrder supported only one mock, and your implementation supports proper cross-mock interactions?

kaste avatar Oct 05 '25 09:10 kaste

I pushed master to update to modern tooling (uv) and generally towards v2. Please try a rebase. E.g. RememberedInvocation | RememberedProxyInvocation is now invocation.RealInvocation.

kaste avatar Oct 05 '25 11:10 kaste

The old InOrder supported only one mock, and your implementation supports proper cross-mock interactions?

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. But this is easy to fix (I think). After checking that the specific instance of a mock is called I delegate to the original implementation of verify with times=1. I think changing it to atleast=1 solves the issue.

I pushed master to update to modern tooling (uv) and generally towards v2. Please try a rebase. E.g. RememberedInvocation | RememberedProxyInvocation is now invocation.RealInvocation.

I'll looking into that

cMancio00 avatar Oct 05 '25 14:10 cMancio00

@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). Best regards

P.S. I would like to help in an other PR with docs (even for all the project) and "fix" spy objects. If you also need help with the CI let me know.

cMancio00 avatar Oct 05 '25 16:10 cMancio00

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

What is the purpose of operator ...?

cMancio00 avatar Oct 12 '25 17:10 cMancio00

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 avatar Oct 12 '25 17:10 cMancio00

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

kaste avatar Oct 14 '25 00:10 kaste

Difficult to think this through... The standard verify should be supported at least.

kaste avatar Nov 18 '25 12:11 kaste