[TIP] Is this a bug in unittest.mock, or am I missing something?

John W jwdevel at gmail.com
Fri Feb 17 11:03:17 PST 2017


Thank you James for the additional input. I've confirmed the same
behavior over here.
The fact that the "==" check you concocted behaves differently from
"assert_has_calls" for this case does push me towards thinking this is
a bug rather than a feature.
I too would be interested to hear the opinions of others on this list
re: that point.


Now to the other topic: the "why are you even doing that?" question... (:
I've been mostly trying to figure out how this library is supposed to
work (since I'm new to it) rather than try to perfect my testing
methodology. Call me a bottom-up learner (:
But since both you and that other fellow on SO seem to find my example
unconvincing, I'll expand upon it a bit and you can tell me what you
think:

Let's say I have a little "Downloader" class (this is "Foo" in my
original example):

    ### download.py
    class Downloader:
        def __init__(self, temp_dir):
            self.temp_dir = temp_dir

        def download_item(self, item):
            '''Downloads the item to self.temp_dir'''
            # code omitted...
            pass

Assume that that class is well-tested on its own — we know it connects
to the right servers, downloads the right file to the right place,
etc.

The Downloader is used somewhere else, as part of a larger program.

    ### main.py
    from download import Downloader
    def process_many_things():
        '''This function downloads various files, processes them,
        and outputs the result'''
        # code omitted...
        pass

I'd like to test `process_many_things`. I suppose this not really a
unit test, more like an end-to-end test with mocked-out dependenceies
— let's not get into a discussion of nuanced testing terminology (:
I just want to make sure that process_many_things downloads some
items, amongst other things.

So, in my test code, I might have:

    ### test_end_to_end.py
    from unittest.mock import patch, call
    import main

    @patch('main.Downloader' autospec=True)
    def test_happy_path(mockDownloader):
        # Do a bunch of stuff
        main.process_many_things()

        # Now confirm that it performed appropriate actions.
    	mockDownloader.assert_has_calls([
            call('expected/temp/dir').download_item('expected-item')
            # You can imagine other stuff here...
        ])

Now, I'm sure if you ask 10 developers how to write this test you
could get 10 different answers; there are many approaches.
But is this particular one flat-out wrong, somehow?
Ignoring the fact that this bug we've discussed prevents the above
from working as expected, is this somehow a cuckoobananas example
altogether?
I admit it's not perfect, but it tests some real business logic worth
testing, I'd say.

> The style of testing that I use at work and advocate is one that
> generally avoids `assert_has_calls`, and tends to check
> `call_count` and `call_args_list` and make use
> of `assert_called_once_with`.

Yes, I find myself leaning that way, too. While I was trying out
various approaches, I stumbled upon the weird behavior of
assert_has_calls, so went down a bit of a rabbit hole trying to
understand that (hence this thread, etc).
The code I have today does not use it, and I'll be sure to warn others, too (:

Thanks again
-John


On 2/17/17, James Cooke <me at jamescooke.info> wrote:
> Hi John,
>
> OK great - your `test_calls_match_2` works for me too. Sorry for
> injecting any confusion into the thread.
>
> I've had another look at your `MockFoo` example and I think that the
> "problem" is the way that calls are matched by `assert_has_calls`. That
> function is working on the `mock_calls` list
> (https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.mock_calls),
> and using that directly appears to work for me:
>
>     from unittest import mock
>
>     ...
>
>     def test_foo():
>         with mock.patch('test.Foo', autospec=True) as MockFoo:
>             m = Foo()
>             m.my_method(122)
>             expected_calls = [mock.call(), mock.call().my_method(122)]
>             assert MockFoo.mock_calls == expected_calls
>
> So, yes, it looks to me that there is a bug in the `assert_has_calls`
> matching, but there's probably a "good" reason that it works this way -
> maybe someone on this list can check this / validate my thinking?
>
> Meanwhile, I would argue that this example is attempting to mock out too
> much and it is not clear the context which would require this approach
> (IMO you can see this in the Stack Overflow comment thread on your
> original question). If this is *really* what you want to do, I would
> inject an instance and test the calls on the constructor and instance
> separately (explicit better than implicit) - something like this,
> although it's pretty dirty because the whole of `Foo` is mocked, when
> really it appears the goal is to test the call on `my_method`.
>
>
>     @mock.patch('test.Foo', autospec=True)
>     def test_foo(MockFoo):
>         instance = mock.Mock(spec=Foo)
>         instance.my_method.return_value = '__MY_METHOD__'
>         MockFoo.return_value = instance
>         m = Foo()
>
>         result = m.my_method(123)
>
>         assert result == '__MY_METHOD__'
>         MockFoo.assert_called_once_with()
>         instance.my_method.assert_called_once_with(123)
>
> The style of testing that I use at work and advocate is one that
> generally avoids `assert_has_calls`, and tends to check `call_count` and
> `call_args_list` and make use of `assert_called_once_with`. I've found
> these much more stable and provide expected results compared to
> `assert_has_calls`.
>
> Hope that helps.
>
> Cheers,
>
> James
>
>
> On Fri, 17 Feb 2017, at 12:38 AM, John W wrote:
>> To take your approach, and do what (I think) I'm trying to do, I'd write:
>>
>>     from unittest import mock
>>     def test_calls_match_2():
>>         """
>>         Calls on a Mock match expectation
>>         """
>>         constructor = mock.Mock()
>>         instance = constructor()
>>         instance.my_method(123)
>>
>>         constructor.assert_has_calls([mock.call(),
>>         mock.call().my_method(123)])
>>
>> That passes just fine for me, and seems reasonable.
>>
>> I think the reason you had "call()" vs "call" was that "m" in your
>> example had "my_method" called on it directly, rather than calling it
>> on the result of the initial call to "m()". That is - calling
>> my_method on the constructor, rather than the instance.
>>
>> So that all seems fine to me, and seems to work as expected.
>>
>> But then, when mock.patch and autospec get involved, as in my original
>> example, it all seems to go south...
>>
>> -John
>>
>> On 2/16/17, James Cooke <me at jamescooke.info> wrote:
>> > Hi John,
>> >
>> >
>> >
>> > This is interesting and thanks for sharing it. Running python 3.5.2, I
>> > firstly simplified your example and got a slightly different exception:
>> >
>> >
>> > from unittest import mock
>> >
>> >
>> >
>> >
>> >
>> > def test_calls_match():
>> >
>> >     """
>> >
>> >     Calls on a Mock match expectation
>> >
>> >     """
>> >
>> >     m = mock.Mock()
>> >
>> >     m()
>> >
>> >     m.my_method(123)
>> >
>> >
>> >
>> >     m.assert_has_calls([mock.call(), mock.call().my_method(123)])
>> >
>> >
>> >
>> > The exception is slightly different for me:
>> >
>> >
>> >
>> > E               AssertionError: Calls not found.
>> >
>> > E               Expected: [call(), call().my_method(123)]
>> >
>> > E               Actual: [call(), call.my_method(123)]
>> >
>> >
>> >
>> > When I remove the parentheses after the second `call`, then everything
>> > works for me, and the test passes.
>> >
>> >
>> >     m.assert_has_calls([mock.call(), mock.call.my_method(123)])
>> >
>> >
>> >
>> > My assumption is that MagicMock will operate like Mock and that your
>> > MockFoo is a MagicMock, so could you try the `mock.call.my_method` on
>> > Python 3.4?
>> >
>> >
>> > I'm interested to hear how that works for you.
>> >
>> >
>> >
>> > Cheers,
>> >
>> >
>> >
>> > James
>> >
>> >
>> >
>> >
>> >
>> >
>> >
>> >
>> >
>> > On Thu, 16 Feb 2017, at 07:06 PM, John W wrote:
>> >
>> >> They seem to be the same types.
>> >
>> >>
>> >
>> >> I added these prints:
>> >
>> >>
>> >
>> >>         print("type of call():", type(call()))
>> >
>> >>         print("type of call().my_method(123):",
>> >
>> >>         type(call().my_method(123)))
>> >
>> >>         for c in MockFoo.mock_calls:
>> >
>> >>             print("type of mock call '{}': {}".format(c, type(c)))
>> >
>> >>
>> >
>> >> And output is:
>> >
>> >>
>> >
>> >>     type of call(): <class 'unittest.mock._Call'>
>> >
>> >>     type of call().my_method(123): <class 'unittest.mock._Call'>
>> >
>> >>     type of mock call 'call()': <class 'unittest.mock._Call'>
>> >
>> >>     type of mock call 'call().my_method(123)': <class
>> >
>> >>     'unittest.mock._Call'>
>> >
>> >>
>> >
>> >>
>> >
>> >> Thanks for the suggestion though.
>> >
>> >> I've been getting crickets from every place I've asked this
>> >> question (:
>> >>
>> >
>> >> -John
>> >
>> >>
>> >
>> >> _______________________________________________
>> >
>> >> testing-in-python mailing list
>> >
>> >> testing-in-python at lists.idyll.org
>> >
>> >> http://lists.idyll.org/listinfo/testing-in-python
>> >
>> >
>> >
>> >
>> >
>> > --
>> >
>> > James Cooke
>> >
>> > Backend software developer
>> >
>> > CV PDF: http://jamescooke.info/docs/james_cooke_cv.pdf
>> >
>> > Website: http://jamescooke.info/
>> >
>
>
> --
> James Cooke
> Backend software developer
> CV PDF: http://jamescooke.info/docs/james_cooke_cv.pdf
> Website: http://jamescooke.info/
>
> _______________________________________________
> testing-in-python mailing list
> testing-in-python at lists.idyll.org
> http://lists.idyll.org/listinfo/testing-in-python
>



More information about the testing-in-python mailing list