web-dev-qa-db-fra.com

Affirmez qu'une méthode a été appelée avec un argument parmi plusieurs

Je me moque d'un appel à requests.post en utilisant la bibliothèque Mock:

requests.post = Mock()

L'appel implique plusieurs arguments: l'URL, une charge utile, des informations d'authentification, etc. Je veux affirmer que requests.post est appelé avec une URL particulière, mais je me fiche des autres arguments. Quand j'essaye ceci:

requests.post.assert_called_with(requests_arguments)

le test échoue, car il s'attend à ce qu'il soit appelé avec uniquement cet argument.

Existe-t-il un moyen de vérifier si un seul argument est utilisé quelque part dans l'appel de fonction sans avoir à passer les autres arguments?

Ou, mieux encore, existe-t-il un moyen d'affirmer une URL spécifique, puis des types de données abstraits pour les autres arguments (c'est-à-dire que les données doivent être un dictionnaire, auth doit être une instance de HTTPBasicAuth, etc.)?

51
user1427661

Pour autant que je sache, Mock ne fournit pas un moyen de réaliser ce que vous voulez via assert_called_with. Vous pouvez accéder à call_args et call_args_list membres et effectuez les assertions manuellement.

Cependant, c'est un moyen simple (et sale) d'obtenir presque ce que vous voulez. Vous devez implémenter une classe dont __eq__ la méthode renvoie toujours True:

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

L'utiliser comme:

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)

Comme vous pouvez le voir, il ne vérifie que le arg. Vous devez créer des sous-classes de int, sinon les comparaisons ne fonctionneront pas1. Cependant, vous devez toujours fournir tous les arguments. Si vous avez de nombreux arguments, vous pouvez raccourcir votre code en utilisant le décompactage de tuple:

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

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

Sauf pour cela, je ne peux pas penser à un moyen d'éviter de passer tous les paramètres à assert_called_with et travaillez comme vous le souhaitez.


La solution ci-dessus peut être étendue pour vérifier les types d'autres arguments. Par exemple:

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

cependant, cela n'autorise pas les arguments qui peuvent être, par exemple, à la fois un int ou un str. Autoriser plusieurs arguments à Any et utiliser l'héritage multiple n'aidera pas. Nous pouvons résoudre ce problème en utilisant 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()

Exemple:

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 J'ai utilisé le nom Any pour la fonction car il est "utilisé comme classe" dans le code. any est également intégré ...

40
Bakuriu

Vous pouvez également utiliser l'assistant ANY pour toujours faire correspondre les arguments que vous ne connaissez pas ou que vous ne recherchez pas.

Plus d'informations sur TOUT assistant: https://docs.python.org/3/library/unittest.mock.html#any

Ainsi, par exemple, vous pouvez faire correspondre l'argument "session" à quelque chose comme ça:

from unittest.mock import ANY
requests_arguments = {'slug': 'foo', 'session': ANY}
requests.post.assert_called_with(requests_arguments)
145
k0nG
@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'))

voir: appels en tant que tuples

17
ZhiQiang Fan

S'il y a trop de paramètres passés et qu'un seul d'entre eux doit être vérifié, faire quelque chose comme {'slug': 'foo', 'field1': ANY, 'field2': ANY, 'field3': ANY, ' . . . } peut être maladroit.


J'ai adopté l'approche suivante pour y parvenir:

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

En termes simples, cela renvoie un Tuple avec tous les arguments positionnels et un dictionnaire avec tous les arguments nommés passés à l'appel de fonction, vous pouvez donc maintenant vérifier tout ce que vous voulez.


De plus, si vous vouliez vérifier le type de données de quelques champs

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


De plus, si vous passez des arguments (au lieu d'arguments de mots clés), vous pouvez y accéder via args comme ceci

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