简体   繁体   中英

Python `unittest.mock` assertion on multiple method calls with mutated arguments?

Today I spent pretty much time on some tricky unit test issue, where I tried to properly assert two calls on the same method and getting very strange behavior from unittest.mock 's assert_has_calls method.

Here there is very simplified example how I tried to assert some calls:

class Foo():
    def __init__(self):
        pass

    # Method that I testing!
    def bar(self, d):
        # doing something with dictionary
        print(d)


def baz():
    f = Foo()
    d = {1: 2}

    # first call
    f.bar(d)

    # updated dictionary
    d[3] = 4

    # second call, after dictionary mutation
    f.bar(d)


@mock.patch('foo.Foo')
def test_baz(foo_mock):
    baz()

    foo_mock.return_value.bar.assert_has_calls(
        [
            mock.call({1: 2}),
            mock.call({1: 2, 3: 4})
        ]
    )

Above very simple test (ie test_baz ) failing with error:

E               AssertionError: Calls not found.
E               Expected: [call({1: 2}), call({1: 2, 3: 4})]
E               Actual: [call({1: 2, 3: 4}), call({1: 2, 3: 4})]

Reason is mutation of d dictionary in tested method between two calls and assert_has_calls somehow doesn't capture calls history properly, ie it just captures last dictionary state for all calls!

This looks to me like a bug in unittest.mock , but maybe I'm missing something here (ie using test framework improperly or so)?

It's pretty trivial unit test, but I have no other way to properly assert output of tested method (otherwise test would be useless). Does anybody faced with something like this and maybe have some workaround to propose?

The only solution that I see here is to change tested code (ie baz function) and create copy of mutated dictionary ( d ) prior to passing to method, but I would like to avoid that because it could be pretty large.

To answer myself (maybe it will be helpful to others, too) - I found solution thanks to idea that I got in comments (thanks @MrBeanBremen).

So, this is not a bug in unittest.mock (as expected), but described behavior is by design. More on this problematic can be found in mock getting started guide .

Depending on the specific use case, there are multiple workarounds (as described in the linked guide), and for my use case I decided to use helper function and side effects:

import mock
from copy import deepcopy

from pylib.pytest_examples.foo import baz


def copy_call_args(original_mock):
    new_mock = mock.Mock()

    def side_effect(*args, **kwargs):
        args = deepcopy(args)
        kwargs = deepcopy(kwargs)
        new_mock(*args, **kwargs)
        return mock.DEFAULT
    original_mock.side_effect = side_effect
    return new_mock


@mock.patch('foo.Foo')
def test_baz(foo_mock):
    bar_mock = copy_call_args(foo_mock.return_value.bar)
    baz()

    bar_mock.assert_has_calls(
        [
            mock.call({1: 2}),
            mock.call({1: 2, 3: 4})
        ]
    )

This way assert_has_calls assertion works properly.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM