Assert that a method was called with one argument out of several
Question:
I’m mocking out a call to requests.post
using the Mock
library:
requests.post = Mock()
The the call involves multiple arguments: the URL, a payload, some auth stuff, etc. I want to assert that requests.post
is called with a particular URL, but I don’t care about the other arguments. When I try this:
requests.post.assert_called_with(requests_arguments)
the test fails, as it expects it to be called with only that argument.
Is there any way to check if a single argument is used somewhere in the function call without having to pass in the other arguments?
Or, even better, is there a way to assert a specific URL and then abstract data types for the other arguments (i.e., data should be a dictionary, auth should be an instance of HTTPBasicAuth, etc.)?
Answers:
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…
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)
@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
You can use : assert_any_call(args)
https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_any_call
requests.post.assert_any_call(requests_arguments)
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'
)
You can use Mock.call_args to collect arguments your method was called with. If your mocked method has been called, it would return arguments your method was called with in the form of tuple of ordered arguments and key word arguments.
class A(object):
def a_method(self, a, b,c=None):
print("Method A Called")
def main_method():
# Main method instantiates a class A and call its method
a = A()
a.a_method("vikalp", "veer",c= "Test")
# Test main method : We patch instantiation of A.
with patch(__name__ + '.A') as m:
ret = m.return_value
ret.a_method = Mock()
res = main_method()
args, kwargs = ret.a_method.call_args
print(args)
print(kwargs)
Above code will output odered arguments and keywords arguments as follows:
('vikalp', 'veer')
{'c': 'Test'}
You can assert on this like :
assert args[0] == "vikalp"
assert kwargs['c'] == "Test"
The following one liner will assert that requests_arguments
appears at least once in the list of positional arguments made to the calls of requests.post
assert any(map(lambda args: requests_arguments in args[0], requests.post.call_args_list))
Full working example:
import unittest.mock
post = unittest.mock.Mock()
post('foo')
post('bar')
assert any(map(lambda args: 'foo' in args[0], requests.post.call_args_list))
I’m mocking out a call to requests.post
using the Mock
library:
requests.post = Mock()
The the call involves multiple arguments: the URL, a payload, some auth stuff, etc. I want to assert that requests.post
is called with a particular URL, but I don’t care about the other arguments. When I try this:
requests.post.assert_called_with(requests_arguments)
the test fails, as it expects it to be called with only that argument.
Is there any way to check if a single argument is used somewhere in the function call without having to pass in the other arguments?
Or, even better, is there a way to assert a specific URL and then abstract data types for the other arguments (i.e., data should be a dictionary, auth should be an instance of HTTPBasicAuth, etc.)?
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…
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)
@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
You can use : assert_any_call(args)
https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_any_call
requests.post.assert_any_call(requests_arguments)
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'
)
You can use Mock.call_args to collect arguments your method was called with. If your mocked method has been called, it would return arguments your method was called with in the form of tuple of ordered arguments and key word arguments.
class A(object):
def a_method(self, a, b,c=None):
print("Method A Called")
def main_method():
# Main method instantiates a class A and call its method
a = A()
a.a_method("vikalp", "veer",c= "Test")
# Test main method : We patch instantiation of A.
with patch(__name__ + '.A') as m:
ret = m.return_value
ret.a_method = Mock()
res = main_method()
args, kwargs = ret.a_method.call_args
print(args)
print(kwargs)
Above code will output odered arguments and keywords arguments as follows:
('vikalp', 'veer')
{'c': 'Test'}
You can assert on this like :
assert args[0] == "vikalp"
assert kwargs['c'] == "Test"
The following one liner will assert that requests_arguments
appears at least once in the list of positional arguments made to the calls of requests.post
assert any(map(lambda args: requests_arguments in args[0], requests.post.call_args_list))
Full working example:
import unittest.mock
post = unittest.mock.Mock()
post('foo')
post('bar')
assert any(map(lambda args: 'foo' in args[0], requests.post.call_args_list))