Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assert that a method was called with one argument out of several

You can also use the ANY helper to always match arguments you don't know or aren't checking for.

More on the ANY helper: https://docs.python.org/3/library/unittest.mock.html#any

So for instance you could match the argument 'session' to anything like so:

from unittest.mock import ANY
requests_arguments = {'slug': 'foo', 'session': ANY}
requests.post.assert_called_with(requests_arguments)

As far as I know Mock doesn't provide a way to achieve what you want via assert_called_with. You could access the call_args and call_args_list members and perform the assertions manually.

However the is a simple (and dirty) way of achieving almost what you want. You have to implement a class whose __eq__ method always returns True:

def Any(cls):
    class Any(cls):
        def __eq__(self, other):
            return True
    return Any()

Using it as:

In [14]: caller = mock.Mock(return_value=None)


In [15]: caller(1,2,3, arg=True)

In [16]: caller.assert_called_with(Any(int), Any(int), Any(int), arg=True)

In [17]: caller.assert_called_with(Any(int), Any(int), Any(int), arg=False)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-17-c604faa06bd0> in <module>()
----> 1 caller.assert_called_with(Any(int), Any(int), Any(int), arg=False)

/usr/lib/python3.3/unittest/mock.py in assert_called_with(_mock_self, *args, **kwargs)
    724         if self.call_args != (args, kwargs):
    725             msg = self._format_mock_failure_message(args, kwargs)
--> 726             raise AssertionError(msg)
    727 
    728 

AssertionError: Expected call: mock(0, 0, 0, arg=False)
Actual call: mock(1, 2, 3, arg=True)

As you can see it only checks for the arg. You have to create subclasses of int, otherwise the comparisons wont work1. However you still have to provide all the arguments. If you have many arguments you might shorten your code using tuple-unpacking:

In [18]: caller(1,2,3, arg=True)

In [19]: caller.assert_called_with(*[Any(int)]*3, arg=True)

Except for this I cannot think of a way to avoid passing all parameters to assert_called_with and work it as you intend.


The above solution can be extended to check for types of other arguments. For example:

In [21]: def Any(cls):
    ...:     class Any(cls):
    ...:         def __eq__(self, other):
    ...:             return isinstance(other, cls)
    ...:     return Any()

In [22]: caller(1, 2.0, "string", {1:1}, [1,2,3])

In [23]: caller.assert_called_with(Any(int), Any(float), Any(str), Any(dict), Any(list))

In [24]: caller(1, 2.0, "string", {1:1}, [1,2,3])

In [25]: caller.assert_called_with(Any(int), Any(float), Any(str), Any(dict), Any(tuple))
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-25-f607a20dd665> in <module>()
----> 1 caller.assert_called_with(Any(int), Any(float), Any(str), Any(dict), Any(tuple))

/usr/lib/python3.3/unittest/mock.py in assert_called_with(_mock_self, *args, **kwargs)
    724         if self.call_args != (args, kwargs):
    725             msg = self._format_mock_failure_message(args, kwargs)
--> 726             raise AssertionError(msg)
    727 
    728 

AssertionError: Expected call: mock(0, 0.0, '', {}, ())
Actual call: mock(1, 2.0, 'string', {1: 1}, [1, 2, 3])

however this doesn't allow arguments that can be, for example, both an int or a str. Allowing multiple arguments to Any and using multiple-inheritance wont help. We can solve this using abc.ABCMeta

def Any(*cls):
    class Any(metaclass=abc.ABCMeta):
        def __eq__(self, other):
            return isinstance(other, cls)
    for c in cls:
        Any.register(c)
    return Any()

Example:

In [41]: caller(1, "ciao")

In [42]: caller.assert_called_with(Any(int, str), Any(int, str))

In [43]: caller("Hello, World!", 2)

In [44]: caller.assert_called_with(Any(int, str), Any(int, str))

1 I used the name Any for the function since it is "used as a class" in the code. Also any is a built-in...


@mock.patch.object(module, 'ClassName')
def test_something(self, mocked):
    do_some_thing()
    args, kwargs = mocked.call_args
    self.assertEqual(expected_url, kwargs.get('url'))

see: calls-as-tuples


TLDR:

args, kwargs = requests.post.call_args_list[-1]
self.assertTrue('slug' in kwargs, '`slug` not passed to requests.post')

In simple words, we get access to a tuple with all the positional arguments and a dictionary with all the named arguments that were passed to the function - so now you can check anything you want.



I find this approach much cleaner than the other popular answers because:
If there are too many parameters being passed and only one of them is to be checked, doing something like {'slug': 'foo', 'field1': ANY, 'field2': ANY, 'field3': ANY, ' . . . } can be clumsy.


Furthermore, if you wanted to check the datatype of a few fields:

args, kwargs = requests.post.call_args_list[0]
self.assertTrue(isinstance(kwargs['data'], dict))

Also, if you're passing arguments (instead of keyword arguments), you can access them via args like this:

self.assertEqual(
    len(args), 1,
    'post called with different number of arguments than expected'
)