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

James Cooke me at jamescooke.info
Fri Feb 17 01:48:37 PST 2017


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/



More information about the testing-in-python mailing list