kaste / mockito-python

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

Feature request: add a `.thenDoNothing()` method to `AnswerSelector`? #69

Open fge opened 9 months ago

fge commented 9 months ago

The original mockito in Java has that and it's very convenient; can this be implemented in python mockito as well?

I'm a beginner in python but AFAICS this can be implemented as such:

def thenDoNothing(self):
    thenAnswer(lambda: *args = None)
kaste commented 9 months ago

Interesting. thenDoNothing is just there because it reads nice, I guess. Or is there a specific reason to include it?

Afaik we currently have:

    # omit thenReturn and it will always just return None
    when(dog).bark(...)  
    when(dog).bark(...).thenReturn(None)  # explicit

Omitting thenReturn doesn't read nice. I think it is there because we also have expect(dog, times=2).bark(...) et.al.

But besides that, when(dog).bark(...).thenReturn(None) would be exactly what thenDoNothing() does. But maybe I'm reading this wrong since stubbed methods do not call the original implementation, hence "do nothing", by default. What I mean is, when you when(dog).bark(...).thenReturn(2) calling such a method does nothing but returns 2 immediately.

fge commented 9 months ago

I was thinking about functions not meaning to return any value :) Java's mockito has that shortcut for functions returning void although the syntax is not exactly the same (doNothing().when(etc etc)).

But I didn't know that you could just specify no .thenAnswer() and it would have the same effect... Nice trick :)

Ultimately that's syntactic sugar, of course, so it's your call :)

kaste commented 9 months ago

Oh, didn't realize that thenReturn() or thenAnswer() without any args also works.

fge commented 9 months ago

Oh, didn't realize that thenReturn() or thenAnswer() without any args also works.

That's not what I meant; I meant to just omit.thenReturn()/.thenAnswer() as you wrote -- but it is also written in the docs and I missed this part...

fge commented 9 months ago

Ohwell, I don't think it would be of any use really, now that I know the trick of not specifying .then*().

Also, I'm too much influenced by Java... One thing I am starting to fully realise since I first posted this request is that python has no void, in addition to not having null either.

Sorry for the noise :)

fge commented 9 months ago

OK, I thought I had closed this issue... Anyway, as said in my previous comment, no need for that after all

fge commented 8 months ago

Well, I am on the Python discord and I've been asked this question about this code (values is a deque here -- I use Python 3.11 if that can help):

    def test_add_updates_deque(self):
        sample = 1.0
        mean_value = MeanValue()
        values = mock(deque)
        when(values).appendleft(sample)    # <--- This line
        mean_value.values = values
        mean_value.add(sample)
        verify(values, times = 1).appendleft(sample)

The guy who asked me about this line is pretty experienced in Python (much more so than I am) and he didn't get the hang of what this line did.

The way I explained it to him is as such:

You can read this as when(values).appendleft(sample).thenDoNothing(), which is more explicit; and it does what it reads like; "when you call the appendleft method of values with exactly what sample... Well... is (therefore, object identity; anything else will not match), then do nothing -- but do record, as a side effect, that this method was called with this very argument"

The "which is more explicit" is of course my opinion only, of course; but can we just have that method defined which just returns self? :)

kaste commented 8 months ago

Well, when(values).appendleft(sample) doesn't read good and I would probably not write it as well. I think expect(values, times=1).appendleft(sample) would read nice though. Because here expect means: be aware and prepared that we call this.

The main problem with thenDoNothing as you would use it here is the confusion that mocks and stubbed callables do nothing in general. That's their point. You turn function calls into constants basically. So when(values).appendleft(sample).thenReturn(None) actually reads fine like so: whenever you see a values.appendleft(sample) just return None.

thenDoNothing would make more sense if you just record the interaction with an object, like the spy which always calls the original implementation/function but also records its usage. E.g. (iirc)

    def test_add_updates_deque(self):
        sample = 1.0
        mean_value = MeanValue()
        mean_value.values = spy(deque)
        mean_value.add(sample)
        verify(mean_value.values, times = 1).appendleft(sample)

This one also reads fine, imo:

    def test_add_updates_deque(self):
        sample = 1.0
        mean_value = MeanValue()
        mean_value.values = mock(deque)

        with expect(mean_value.values, times=1).appendleft(sample):
            mean_value.add(sample)

But since .values is part of the interface of MeanValue in your example,

    def test_add_updates_deque(self):
        sample = 1.0
        mean_value = MeanValue()
        with expect(mean_value.values, times=1).appendleft(sample):
            mean_value.add(sample)

probably works as well. The constructor MeanValue likely initializes the object at .values and we just capture the one interaction with it.