Утверждение, что метод был вызван одним аргументом из нескольких

Я издеваюсь над вызовом requests.post с помощью библиотеки Mock:

requests.post = Mock()

Вызов включает в себя несколько аргументов: URL-адрес, полезную нагрузку, некоторые файлы auth и т.д. Я хочу утверждать, что requests.post вызывается с определенным URL-адресом, но меня не интересуют другие аргументы. Когда я пробую это:

requests.post.assert_called_with(requests_arguments)

тест терпит неудачу, поскольку он ожидает, что он будет вызван только с этим аргументом.

Есть ли способ проверить, используется ли какой-либо один аргумент где-либо в вызове функции без необходимости передавать другие аргументы?

Или, что еще лучше, есть способ утверждать определенный URL-адрес, а затем абстрактные типы данных для других аргументов (т.е. Данные должны быть словарем, auth должен быть экземпляром HTTPBasicAuth и т.д.)?

Ответ 1

Насколько я знаю, Mock не обеспечивает способ достижения желаемого через assert_called_with. Вы можете получить доступ к call_args и call_args_list и выполнять эти утверждения вручную.

Однако простой (и грязный) способ достичь почти того, что вы хотите. Вы должны реализовать класс, метод __eq__ которого всегда возвращает True:

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

Используя его как:

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)

Как вы можете видеть, это проверяет только arg. Вы должны создавать подклассы int, иначе сравнения не будут работать 1. Однако вы все равно должны предоставить все аргументы. Если у вас много аргументов, вы можете сократить свой код, используя распаковку:

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

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

Кроме этого, я не могу придумать способ избежать передачи всех параметров assert_called_with и работать с ним так, как вы планируете.


Вышеупомянутое решение может быть расширено для проверки типов других аргументов. Например:

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])

однако это не позволяет аргументы, которые могут быть, например, как int и str. Разрешение нескольких аргументов Any и использование множественного наследования не помогут. Мы можем решить это, используя 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()

Пример:

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 Я использовал имя Any для функции, поскольку оно "используется как класс" в коде. Также any встроенный в...

Ответ 2

Вы также можете использовать ANY помощника, чтобы всегда сопоставлять аргументы, которые вы не знаете или не проверяете.

Подробнее о ЛЮБОМ помощнике: https://docs.python.org/3/library/unittest.mock.html#any

Так, например, вы можете сопоставить аргумент 'session' с чем угодно

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

Ответ 3

@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'))

см.: звонки-кортежи

Ответ 4

Если передается слишком много параметров, и нужно проверить только один из них {'slug': 'foo', 'field1': ANY, 'field2': ANY, 'field3': ANY, '... } что-то вроде {'slug': 'foo', 'field1': ANY, 'field2': ANY, 'field3': ANY, '... } {'slug': 'foo', 'field1': ANY, 'field2': ANY, 'field3': ANY, '... } {'slug': 'foo', 'field1': ANY, 'field2': ANY, 'field3': ANY, '... } может быть неуклюжим.


Я выбрал следующий подход для достижения этой цели:

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

Проще говоря, это возвращает кортеж со всеми позиционными аргументами и словарь со всеми именованными аргументами, передаваемыми в вызов функции, так что теперь вы можете проверить все, что захотите.


Кроме того, если вы хотите проверить тип данных нескольких полей

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


Кроме того, если вы передаете аргументы (вместо аргументов ключевых слов), вы можете получить к ним доступ через args подобные этой

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