Stellen Sie sicher, dass eine Methode mit einem von mehreren Argumenten aufgerufen wurde

76

Ich verspotte einen Aufruf zur requests.postNutzung der MockBibliothek:

requests.post = Mock()

Der Aufruf umfasst mehrere Argumente: die URL, eine Nutzlast, einige Authentifizierungsdaten usw. Ich möchte behaupten, dass diese requests.postmit einer bestimmten URL aufgerufen wird, aber die anderen Argumente interessieren mich nicht. Wenn ich das versuche:

requests.post.assert_called_with(requests_arguments)

Der Test schlägt fehl, da erwartet wird, dass er nur mit diesem Argument aufgerufen wird.

Gibt es eine Möglichkeit zu überprüfen, ob irgendwo im Funktionsaufruf ein einzelnes Argument verwendet wird, ohne die anderen Argumente übergeben zu müssen?

Oder, noch besser, gibt es eine Möglichkeit, eine bestimmte URL zu bestätigen und dann Datentypen für die anderen Argumente zu abstrahieren (dh Daten sollten ein Wörterbuch sein, auth sollte eine Instanz von HTTPBasicAuth sein usw.)?

user1427661
quelle
Keine Beziehung zu den Fragen, aber wenn Sie REST-Aufrufe verspotten, requests-mockkönnte das Modul auch interessant sein.
TheHowlingHoaschd

Antworten:

54

Soweit ich weiß, Mockgibt es keine Möglichkeit, über das zu erreichen, was Sie wollen assert_called_with. Sie könnten die zugreifen call_argsund call_args_listMitglieder und die Behauptungen manuell durchführen.

Dies ist jedoch eine einfache (und schmutzige) Methode, um fast das zu erreichen, was Sie wollen. Sie müssen eine Klasse implementieren, deren __eq__Methode immer Folgendes zurückgibt True:

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

Verwenden Sie es als:

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)

Wie Sie sehen können, wird nur nach dem gesucht arg. Sie müssen Unterklassen von erstellen int, sonst funktionieren die Vergleiche nicht 1 . Sie müssen jedoch noch alle Argumente angeben. Wenn Sie viele Argumente haben, können Sie Ihren Code mithilfe des Tupel-Entpackens verkürzen:

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

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

Abgesehen davon kann ich mir keine Möglichkeit vorstellen, zu vermeiden, dass alle Parameter an Sie übergeben assert_called_withund so bearbeitet werden, wie Sie es beabsichtigen.


Die obige Lösung kann erweitert werden, um nach anderen Argumenttypen zu suchen. Zum Beispiel:

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

Dies erlaubt jedoch keine Argumente, die beispielsweise sowohl ein intals auch ein sein können str. Das Zulassen mehrerer Argumente für Anyund die Verwendung von Mehrfachvererbung hilft nicht. Wir können dies mit lösenabc.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()

Beispiel:

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 Ich habe den Namen Anyfür die Funktion verwendet, da sie im Code "als Klasse verwendet" wird. Auch anyist ein eingebauter ...

Bakuriu
quelle
3
Ich habe eine Variation davon verwendet, aber in neueren Versionen von mock verwenden sie! = Als Vergleich (zumindest für kwargs), daher müssen Sie auch überschreiben def __neq__(self, other): return False.
Andrew Backer
2
Dies ist jetzt in das Framework integriert (ohne Typprüfung), wie eine andere Antwort zeigt: stackoverflow.com/a/27152023/452274
Matt
Mock bietet einen Weg. Siehe Antwort von @ k0nG
Marcin
@Marcin Nein, tut es nicht. ANY bietet keine Typprüfung.
Bakuriu
229

Sie können den ANYHelfer auch verwenden, um immer Argumente abzugleichen, die Sie nicht kennen oder nach denen Sie nicht suchen.

Mehr zum ANY-Helfer: https://docs.python.org/3/library/unittest.mock.html#any

So könnten Sie beispielsweise das Argument 'Sitzung' wie folgt zuordnen:

from unittest.mock import ANY
requests_arguments = {'slug': 'foo', 'session': ANY}
requests.post.assert_called_with(requests_arguments)
k0nG
quelle
1
Ich benutze diese Lösung zwar auch, aber sie gibt keine Typprüfung.
Jackdbernier
23
Dies sollte eigentlich als Antwort akzeptiert werden, da es perfekt funktioniert.
Jernej Jerin
1
Dies ist definitiv die richtige Antwort. Funktioniert wie erwartet und sehr sauber.
Ori
27
@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'))

Siehe: Call-as-Tuples

ZhiQiang Fan
quelle
3

Wenn zu viele Parameter übergeben werden und nur einer überprüft werden soll, {'slug': 'foo', 'field1': ANY, 'field2': ANY, 'field3': ANY, ' . . . }kann es umständlich sein, so etwas zu tun .


Ich habe den folgenden Ansatz gewählt, um dies zu erreichen:

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

In einfachen Worten, dies gibt ein Tupel mit allen Positionsargumenten und ein Wörterbuch mit allen benannten Argumenten zurück, die an den Funktionsaufruf übergeben wurden, sodass Sie jetzt alles überprüfen können, was Sie wollen.


Außerdem, wenn Sie den Datentyp einiger Felder überprüfen möchten

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


Auch, wenn Sie Argumente vorbei sind (anstelle von Keyword - Argumente), können Sie auf diese zugreifen über argswie diese

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

Sie können Mock.call_args verwenden , um Argumente zu sammeln, mit denen Ihre Methode aufgerufen wurde. Wenn Ihre verspottete Methode aufgerufen wurde, würde sie Argumente zurückgeben, mit denen Ihre Methode in Form eines Tupels geordneter Argumente und Schlüsselwortargumente aufgerufen wurde.

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)

Der obige Code gibt geordnete Argumente und Schlüsselwortargumente wie folgt aus:

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

Sie können dies wie folgt behaupten:

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