kaste / mockito-python

Mockito is a spying framework
MIT License
123 stars 12 forks source link

Add a method `thenCallRealMethod` to AnswerSelector to call the original method #46

Closed shashankrnr32 closed 2 years ago

shashankrnr32 commented 2 years ago

Note: Please feel free to close the issue if this has been discussed before in any of the issue.

Currently the AnswerSelector interface provides the following methods to be used with when or when2 statement

  1. thenReturn: To return a mocked return value
  2. thenRaise: To raise a mock exception
  3. thenAnswer: To call a custom method (Eg: Lambdas)

I noticed the original method is stored in the __wrapped__ attribute when the method is stubbed.

https://github.com/kaste/mockito-python/blob/8e1771c5bb1c3bc5948c167c81768d2380c11c3b/mockito/mocking.py#L122

This issue is to provide another option thenCallRealMethod to AnswerSelector to call this original method instead of mocking a stub value so that the original method get called instead of the mock. This will support "Partial Mocking" of these methods by allowing the user to call the original method in case the original method is not modifiable.

I have a small work here to support this (untested for now)

(I don't need this as such for my usecase, just some improvement!.)

Example:

class Dog(object):
    def bark(self, sound):
        return "%s!" % sound

rex = Dog()
when(rex).bark('Miau').thenReturn('Wuff').thenCallRealMethod().thenReturn('Puff')

assert 'Wuff' == rex.bark('Miau')
assert "Miau!" == rex.bark("Miau")     # Calls the original method
assert 'Puff' == rex.bark('Miau')

Also see: https://javadoc.io/static/org.mockito/mockito-core/4.1.0/org/mockito/stubbing/OngoingStubbing.html#thenCallRealMethod--

Thank you.

kaste commented 2 years ago

Yes, I wanted to do that at some point. I don't know a good name. For thenCallRealMethod, I don't like the method in it as we're not restricted to methods in any way in Python. We generally work with callables. For example os.path.exists is not a classical method.

For the implementation, look how RememberedProxyInvocation does that for spy: https://github.com/kaste/mockito-python/blob/27e5587ac683262aa883bcb48130ed1a974df517/mockito/invocation.py#L144-L151

Generally, I think when(rex).bark('Miau').thenReturn('Wuff').thenCallRealMethod().thenReturn('Puff') is a very strange example. A bit more typical is for example to patch os.path.exists where you often want the original implementation intact except for the very call in your unittest.

If I remember correctly, the way to do this right now is:

spy2(os.path.exists)
when(os.path).exists("/foo/bar").thenReturn(True)

which does the trick but is not very accessible for users. The idea behind thenCallOriginalImplementation is to make this work like so:

when(os.path).exists(...).thenCallOriginalImplementation()
when(os.path).exists("/foo/bar").thenReturn(True)

Here when(os.path).exists(...).thenCallOriginalImplementation() is completely the same as and interchangeable with spy2(os.path.exists). T.i. we could reduce the API surface by deprecating spy2 and just relying on the one user endpoint when/2.

Can you come up with some real examples? when(emailService).send().thenReturn(True).thenCallThrough().thenReturn(False) doesn't sound correct.

shashankrnr32 commented 2 years ago

Agreed that

when(rex).bark('Miau').thenReturn('Wuff').thenCallRealMethod().thenReturn('Puff')

may not be the best way of using it, but it could be done (For whatever reasons!)

One simple example I can think of is of a method making 2 API calls, one to an external endpoint and another to the localhost.

def my_method():
    requests.get("https://unknownhost.com/path")
    requests.get("https://localhost:4443/path")

For the above tested method, my expectation would be that the invocation return a stubbed value when the request is made to External endpoint while the call may be made to the server running locally. I know that this is not a "Ideal" usecase that this may be used, but something similar might be the usecase in different scenarios.

My test method would be

when(requests).get("https://unknownhost.com/path").thenReturn(mock())
when(requests).get(...).thenCallOriginalImpl()

I will add more examples if I think of anymore!

kaste commented 2 years ago

This has been implemented by @avandierast and released.