6

Consider example:

def func_b(a):
    print a

def func_a():
    a = [-1]
    for i in xrange(0, 2):
        a[0] = i
        func_b(a)

And test function that tries to test func_a and mocks func_b:

import mock
from mock import call

def test_a():
    from dataTransform.test import func_a
    with mock.patch('dataTransform.test.func_b', autospec=True) as func_b_mock:
        func_a()
        func_b_mock.assert_has_calls([call(0), call(1)])

After func_a has executed I try to test if func_a made correct calls to func_b, but since in for loop I am mutating list in the end I get:

AssertionError: Calls not found.
Expected: [call(0), call(1)]
Actual: [call([1]), call([1])]
Žilvinas Rudžionis
  • 1,954
  • 20
  • 28
  • 1
    Your mock of `func_b()` will need to make copies or otherwise untouchable representations of the argument it gets. Then you can later check these copies for their values. – Alfe Apr 08 '15 at 13:45
  • Does this answer your question? [How can i check call arguments if they will change with unittest.mock](https://stackoverflow.com/questions/16236870/how-can-i-check-call-arguments-if-they-will-change-with-unittest-mock) – AXO Jul 04 '20 at 16:20

1 Answers1

2

The following works (the importing mock from unittest is a Python 3 thing, and module is where func_a and func_b are):

import mock
from mock import call
import copy

class ModifiedMagicMock(mock.MagicMock):
    def _mock_call(_mock_self, *args, **kwargs):
        return super(ModifiedMagicMock, _mock_self)._mock_call(*copy.deepcopy(args), **copy.deepcopy(kwargs))

This inherits from MagicMock, and redefines the call behaviour to deepcopy the arguments and keyword arguments.

def test_a():
    from module import func_a
    with mock.patch('module.func_b', new_callable=ModifiedMagicMock) as func_b_mock:
        func_a()
        func_b_mock.assert_has_calls([call([0]), call([1])])

You can pass the new class into patch using the new_callable parameter, however it cannot co-exist with autospec. Note that your function calls func_b with a list, so call(0), call(1) has to be changed to call([0]), call([1]). When run by calling test_a, this does nothing (passes).

Now we cannot use both new_callable and autospec because new_callable is a generic factory but in our case is just a MagicMock override. But Autospeccing is a very cool mock's feature, we don't want lose it.

What we need is replace MagicMock by ModifiedMagicMock just for our test: we want avoid to change MagicMock behavior for all tests... could be dangerous. We already have a tool to do it and it is patch, used with the new argument to replace the destination.

In this case we use decorators to avoid too much indentation and make it more readable:

@mock.patch('module.func_b', autospec=True)
@mock.patch("mock.MagicMock", new=ModifiedMagicMock)
def test_a(func_b_mock):
    from module import func_a
    func_a()
    func_b_mock.assert_has_calls([call([0]), call([1])])

Or:

@mock.patch("mock.MagicMock", new=ModifiedMagicMock)
def test_a():
    with mock.patch('module.func_b') as func_b_mock:
        from module import func_a
        func_a()
        func_b_mock.assert_has_calls([call([0]), call([1])])
matsjoyce
  • 5,744
  • 6
  • 31
  • 38
  • 1
    Thanks! One thing to mention that in python 2.7 call to parent function would look like: return super(ModifiedMagicMock, _mock_self)._mock_call(*copy.deepcopy(args), **kwargs) – Žilvinas Rudžionis Apr 09 '15 at 07:51
  • Sorry, I forget it... And I wrote an answer about that some months ago☺. – Michele d'Amico Apr 09 '15 at 16:46
  • I know a way to solve this leak. If you are agree I can edit your answer again, otherwise I can file a new answer to cover it. I hate to lose autospec feature. – Michele d'Amico Apr 09 '15 at 17:18
  • @matsjoyce Can I use decorators instead of with statements... it is more redable. – Michele d'Amico Apr 09 '15 at 20:03
  • @Micheled'Amico Thanks. For some reason (mocking being clever), it only worked when I reversed the order of the decorators. I added a half `with` half decorator version, just in case the implementation of mocking changes. – matsjoyce Apr 09 '15 at 21:09
  • @matsjoyce Is not mock implementation dependent, but it is how decorators stack works. My fault was that I used pycharm to execute it but it was not a unitettest class and pass without test anything... Decorators are just syntactic sugar where a stack of two decorators (i.e. `d1` and `d2`) applied to `f` means `f=d1(d2(f))` so the last decorator is applied as first. – Michele d'Amico Apr 09 '15 at 21:26
  • @Micheled'Amico No, it is patching. Assuming decorators work like normal decorators, they stack like http://stackoverflow.com/questions/739654/how-can-i-make-a-chain-of-function-decorators-in-python. However, when multiple patch decorators are applied to a function, mocking keeps a list, which it appends the new patch to. However, the patches are patched starting from the start of the list, so the last decorator is executed first. – matsjoyce Apr 09 '15 at 21:34
  • @matsjoyce If you read well the answer that you post you will see that it explain exactly what I said. And ... exactly, decorators starts from the first but evaluation of a functors start from evaluate arguments :) .... follow the [link](https://docs.python.org/2/reference/compound_stmts.html#function-definitions) and you will read exactly what I wrote. Pay attention! `@patch()` don't execute `_patch.__call__` until decorator is evaluated... finally the list of patches is not reversed. I guess that implementation is to have a simple way to rollback if something goes wrong in patch stacks. – Michele d'Amico Apr 09 '15 at 22:19
  • @matsjoyce I'm very nice to meet you. You are a fine programmer that goes deep to understand the details. – Michele d'Amico Apr 09 '15 at 22:33
  • @Micheled'Amico Thanks! I would reply in the comments, but the code examples are too long, so could you see https://gist.github.com/matsjoyce/30695312d57a901b25cf#file-reply-md. I'm still sure mocking is to blame for the 'wrong' order. – matsjoyce Apr 10 '15 at 20:10
  • Ok, you win... Too much beer yesterday. – Michele d'Amico Apr 10 '15 at 22:13