Стверджуйте, що метод викликався з одним аргументом із кількох


84

Я висміюю дзвінок до requests.postвикористання Mockбібліотеки:

requests.post = Mock()

Виклик включає кілька аргументів: URL-адресу, корисне навантаження, деякі матеріали для автентифікації тощо. Я хочу стверджувати, що requests.postвикликається з певною URL-адресою, але мені не цікаві інші аргументи. Коли я пробую це:

requests.post.assert_called_with(requests_arguments)

тест не вдається, оскільки він очікує, що його буде викликано лише з цим аргументом.

Чи є спосіб перевірити, чи використовується десь один аргумент у виклику функції, не передаючи інші аргументи?

Або, ще краще, чи є спосіб затвердити певну URL-адресу, а потім абстрактні типи даних для інших аргументів (тобто дані повинні бути словником, auth має бути екземпляром HTTPBasicAuth тощо)?


1
Ніякого відношення до питань, але якщо ви глузуєте над дзвінками REST, requests-mockмодуль також може бути цікавим.
TheHowlingHoaschd

Відповіді:


56

Наскільки я знаю, 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])

однак це не дозволяє аргументи, які можуть бути, наприклад, як inta , так і a 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є вбудованим ...


3
Я використовував варіацію щодо цього, але в нових версіях макету вони використовують! = Як порівняння (принаймні для кваргів), тому вам потрібно також замінити def __neq__(self, other): return False.
Ендрю Бекер,

2
Це вбудовано в фреймворк (без перевірки типу), як виводить інша відповідь: stackoverflow.com/a/27152023/452274
Метт

Макет забезпечує спосіб. Див відповідь від @ k0nG
Marcin

@Marcin Ні, ні. БУДЬ-ЯКИЙ не забезпечує перевірку типу.
Bakuriu

245

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

Більше про ЛЮБОГО помічника: https://docs.python.org/3/library/unittest.mock.html#any

Так, наприклад, ви можете порівняти аргумент "сесія" з будь-чим подібним:

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

1
Я теж використовую це рішення, але воно не дає перевірки типу.
jackdbernier

28
Це насправді має бути прийнятою відповіддю, оскільки вона працює чудово.
Jernej Jerin

1
Це, безумовно, правильна відповідь. Працює, як очікувалося, і дуже чисто.
Орі,


3

Якщо передається занадто багато параметрів і перевіряється лише один із них, робити щось подібне {'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'
)

0

Ви можете використовувати Mock.call_args для збору аргументів, за допомогою яких був викликаний ваш метод. Якщо ваш знущаний метод був викликаний, він поверне аргументи, з якими був викликаний ваш метод, у вигляді набору впорядкованих аргументів та аргументів ключового слова.

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)

Наведений вище код виведе замовлені аргументи та аргументи ключових слів наступним чином:

('vikalp', 'veer')
{'c': 'Test'}

Ви можете стверджувати про це, як:

assert args[0] == "vikalp"
assert kwargs['c'] == "Test"

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.