How can I assert a mock object was not called with specific arguments?
Asked Answered
C

5

28

unittest.mock objects have an assert_not_called method available, but what I'm looking for is an assert_not_called_with. Is there anything like that? I looked on Google and didn't see anything, and when I tried just using mock_function.assert_not_called_with(...) it raised an AttributeError, meaning the function does not exist with that name.

My current solution:

with self.assertRaises(AssertionError):
    mock_function.assert_called_with(arguments_I_want_to_test)

This works but clutters the code if I have several such calls I want to make.

Related:

Coyotillo answered 23/2, 2019 at 4:52 Comment(0)
J
17

One more solution that uses mock calls history:

from unittest.mock import call

assert call(arguments_I_want_to_test) not in mock_function.mock_calls
Jackshaft answered 25/2, 2022 at 14:1 Comment(1)
From the docs: constructor: unittest.mock.call(*args, **kwargs), example: call(1, 2, a='foo', b='bar'). To detect absence of a call you could also use assert_any_call(...) and catch exceptions. Only problem is, with both solutions, you have to include ALL args and ALL kwargs for the correct result. Supposing you just want to test all args, ignoring kwargs ... or just one arg of the args... See my answer for a solution.Rhizocarpous
P
31

You can add a assert_not_called_with method to unittest.mock.Mock on your own:

from unittest.mock import Mock

def assert_not_called_with(self, *args, **kwargs):
    try:
        self.assert_called_with(*args, **kwargs)
    except AssertionError:
        return
    raise AssertionError('Expected %s to not have been called.' % self._format_mock_call_signature(args, kwargs))

Mock.assert_not_called_with = assert_not_called_with

so that:

m = Mock()
m.assert_not_called_with(1, 2, a=3)
m(3, 4, b=5)
m.assert_not_called_with(3, 4, b=5)

outputs:

AssertionError: Expected mock(3, 4, b=5) to not have been called.
Puli answered 23/2, 2019 at 6:7 Comment(4)
Instance of 'ClassA' has no 'assert_called_with' member - How to solve this problem?Luthern
This doesn't work. assert_called_with actually works on the LAST call to the mock. Supposing you have 6 calls to the mock and you want to check that no call with given args and kwargs occurred in any of those 6 calls. This won't work. Andrey Semakin's answer will. There is also assert_any_call which, as the name suggests, looks through all calls. But there is currently no assert_any_call_with.Rhizocarpous
@mikerodent My answer was meant for the OP's requirement in the question. The OP specifically says that assert_called_with already "works" when used with assertRaises for his/her intended purpose, so my answer is simply turning what "works" for the OP into a function. You are basically modifying the question with a different logical requirement. Although the OP now accepts Andrey's solution instead, it does not mean my answer "doesn't work" for the question's literal requirement.Puli
@Puli Yep, I accept that my answer goes way beyond what was asked but I'm not sure I totally agree about your answer: basically the OP seems pretty clearly to have believed (wrongly) that his proposed solution was indeed equivalent to assert_not_called as (potentially) applicable to a multiple-calls situation. It's a minor thing but I think some acknowledgement that assert_called_with only covers the last call to the mocked function should have been made.Rhizocarpous
J
17

One more solution that uses mock calls history:

from unittest.mock import call

assert call(arguments_I_want_to_test) not in mock_function.mock_calls
Jackshaft answered 25/2, 2022 at 14:1 Comment(1)
From the docs: constructor: unittest.mock.call(*args, **kwargs), example: call(1, 2, a='foo', b='bar'). To detect absence of a call you could also use assert_any_call(...) and catch exceptions. Only problem is, with both solutions, you have to include ALL args and ALL kwargs for the correct result. Supposing you just want to test all args, ignoring kwargs ... or just one arg of the args... See my answer for a solution.Rhizocarpous
T
5

Using Pytest, I assert that "AssertionError" is called:

import pytest
from unittest.mock import Mock


def test_something():
    something.foo = Mock()
    
    # Test that something.foo(bar) is not called.
    with pytest.raises(AssertionError):
        something.foo.assert_called_with(bar)
Tooling answered 2/8, 2021 at 0:21 Comment(0)
R
2

Andrey Semakin's answer should be the accepted one. The accepted answer is problematic because assert_called_with only examines the last call of the mocked funtion. Supposing you have 6 calls: you may want to find out whether a given call with given parameters was or was not made as any (or more than one) of those 6 calls. The accepted answer doesn't work for that case, but Andrey Semakin's does.

In fact you could also accomplish what his solution does using assert_any_call and catching the assert exception.

Andrey's answer can be made even more useful, though: supposing you don't want to stipulate all the args or all the kwargs? With his solution (and assert_any_call exception-catching) you have to stipulate precisely ALL the args and all the kwargs, or a match doesn't happen. If you're looking for the absence of a match, equally, having to stipulate ALL args and ALL kwargs may typically be too demanding a requirement in a testing context.

case 1: disregarding the args
This can be accomplished pretty easily by using a dummy class:

class AnyArg(object):
    def __eq__(self, b):
        return True
args = (AnyArg(),)
kwargs = {'command': 'put'}
assert mock.call(*args, **kwargs) in mock_process.mock_calls

NB if your args is a mix of real args and AnyArg, I'm not sure what would happen and haven't experimented to find out. Really this solution is simply for the purpose of disregarding ALL args. But it is still problematic: any other supplied kwargs in a call and the match fails (or you get what might be a false negative if you're checking on absence).

case 2: disregarding the kwargs (or both args and kwargs)
This is more difficult. You can't fake the finding or not finding of key-value pairs in another list of key-value pairs. I think this is about as compact as possible:

def is_match_found(mock_func, required_args=(), required_kwargs={}):
    for args_list in mock_func.call_args_list:
        for required_arg in required_args:
            if required_arg not in args_list.args:
                break # go on to next call
        else:
            for required_key in required_kwargs:
                if required_key not in args_list.kwargs:
                    break # go on to next call
                else:
                    if required_kwargs[required_key] != args_list.kwargs[required_key]:
                        # values differ
                        break # go on to next call
            else:
                return True
    return False

examples of use:

# stipulating some args, disregarding kwargs
assert is_match_found(mock_process, ('some parameter', 33,)) 

# stipulating some args and some kwargs, "permissively"
assert not is_match_found(mock_process, ('some parameter', 33,), {'command': 'put',}) 

# stipulating some args and some kwargs, disregarding *values* of named params, 
# but ensuring that those named parameters are present
assert not is_match_found(mock_process, ('some parameter', 33,), {'command': AnyArg(),}) 

I use the word "permissively", meaning that any call to this function, as written, disregards completely any extra args or kwargs which DON'T match. Obviously, infinite refinements could be made to the above (in particular the order of the args might be a requirement), but for testing purposes I think this is quite a useful tool as written.

respecting order of required args
This'll hopefully do the trick (and hopefully give correct results with repeated args):

def is_match_found(mock_func, required_args=(), required_kwargs={}, args_must_be_leading=True):
    for args_list in mock_func.call_args_list:
        call_args = args_list.args
        for required_arg in required_args:
            if required_arg not in call_args:
                break # go on to next call
            index_in_call_args = call_args.index(required_arg)
            if args_must_be_leading and index_in_call_args != 0:
                break # go on to next call
            call_args = call_args[index_in_call_args + 1:]
     [snip...]

Unfortunately, it turns out you can't assign to args_list.args, so I thought initially that we'd be forced to make a (potentially expensive deep) copy of that list. However it seems that just assigning call_args to args_list.args lets you circumvent the problem.

This is far from perfect, of course. In particular, in the above first example, if args_must_be_leading == False a call with args (None, 12, 'some parameter', 3, 3, 33, 'entropy') would qualify as a "pass". I added that param to achieve a slightly more sensible balance between permissiveness and restriction.

Rhizocarpous answered 20/2 at 19:9 Comment(0)
H
0

Another option:

self.assertNotIn(argument_you_want_to_test, mock_function.call_args.kwargs)
Harlamert answered 24/10, 2023 at 22:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.