Verspotten der Python-Funktion basierend auf Eingabeargumenten

150

Wir verwenden Mock seit einiger Zeit für Python.

Jetzt haben wir eine Situation, in der wir eine Funktion verspotten wollen

def foo(self, my_param):
    #do something here, assign something to my_result
    return my_result

Normalerweise wäre der Weg, dies zu verspotten (vorausgesetzt, foo ist Teil eines Objekts)

self.foo = MagicMock(return_value="mocked!")

Auch wenn ich foo () ein paar Mal aufrufe, kann ich verwenden

self.foo = MagicMock(side_effect=["mocked once", "mocked twice!"])

Jetzt stehe ich vor einer Situation, in der ich einen festen Wert zurückgeben möchte, wenn der Eingabeparameter einen bestimmten Wert hat. Wenn wir also sagen, dass "my_param" gleich "etwas" ist, möchte ich "my_cool_mock" zurückgeben.

Dies scheint auf Mockito für Python verfügbar zu sein

when(dummy).foo("something").thenReturn("my_cool_mock")

Ich habe gesucht, wie ich mit Mock dasselbe erreichen kann, ohne Erfolg?

Irgendwelche Ideen?

Juan Antonio Gomez Moriano
quelle
2
Vielleicht
hilft
@naiquevin Dies löst das Problem perfekt, danke!
Juan Antonio Gomez Moriano
Ich hatte keine Ahnung, dass Sie Mocktio mit Python verwenden könnten, +1 dafür!
Ben
Wenn Ihr Projekt Pytest verwendet, möchten Sie möglicherweise einen solchen Zweck nutzen monkeypatch. Monkeypatch ist eher für "Ersetzen dieser Funktion zum Testen", während Mock das ist, was Sie verwenden, wenn Sie auch die Funktion überprüfen mock_callsoder Aussagen darüber machen möchten, mit was sie aufgerufen wurde, und so weiter. Es gibt einen Platz für beide, und ich verwende beide oft zu unterschiedlichen Zeiten in einer bestimmten Testdatei.
Driftcatcher

Antworten:

187

Wenn side_effectes sich um eine Funktion handelt, ruft alles, was diese Funktion zurückgibt, die Scheinrückgabe auf. Die side_effectFunktion wird mit denselben Argumenten wie der Mock aufgerufen. Auf diese Weise können Sie den Rückgabewert des Anrufs basierend auf der Eingabe dynamisch variieren:

>>> def side_effect(value):
...     return value + 1
...
>>> m = MagicMock(side_effect=side_effect)
>>> m(1)
2
>>> m(2)
3
>>> m.mock_calls
[call(1), call(2)]

http://www.voidspace.org.uk/python/mock/mock.html#calling

Bernstein
quelle
24
Könnten Sie die Funktion side_effect in etwas anderes umbenennen, um die Antwort zu vereinfachen? (Ich weiß, ich weiß, es ist ziemlich einfach, aber verbessert die Lesbarkeit die Tatsache, dass Funktionsname und Parametername unterschiedlich sind :)
Juan Antonio Gomez Moriano
6
@JuanAntonioGomezMoriano Ich könnte, aber in diesem Fall zitiere ich nur direkt die Dokumentation, also hasse ich es, das Zitat zu bearbeiten, wenn es nicht speziell kaputt ist.
Amber
und nur um all diese Jahre später pedantisch zu sein, aber side effectist der genaue Begriff: en.wikipedia.org/wiki/Side_effect_(computer_science)
lsh
7
@Ish beschweren sie sich nicht über den Namen von CallableMixin.side_effect, sondern dass die im Beispiel definierte separate Funktion denselben Namen hat.
OrangeDog
48

Wie im Python Mock-Objekt angegeben, wird die Methode mehrmals aufgerufen

Eine Lösung besteht darin, meinen eigenen Nebeneffekt zu schreiben

def my_side_effect(*args, **kwargs):
    if args[0] == 42:
        return "Called with 42"
    elif args[0] == 43:
        return "Called with 43"
    elif kwargs['foo'] == 7:
        return "Foo is seven"

mockobj.mockmethod.side_effect = my_side_effect

Das macht den Trick

Juan Antonio Gomez Moriano
quelle
2
Dies machte es für mich klarer als die ausgewählte Antwort, also danke, dass Sie Ihre eigene Frage beantwortet haben :)
Luca Bezerra
14

Der Nebeneffekt hat eine Funktion (die auch eine Lambda-Funktion sein kann ). In einfachen Fällen können Sie also Folgendes verwenden:

m = MagicMock(side_effect=(lambda x: x+1))
Shubham Chaudhary
quelle
4

Ich bin hier gelandet und habe nach " wie man eine Funktion basierend auf Eingabeargumenten verspottet " gesucht und habe dies schließlich gelöst, indem ich eine einfache Hilfsfunktion erstellt habe:

def mock_responses(responses, default_response=None):
  return lambda input: responses[input] if input in responses else default_response

Jetzt:

my_mock.foo.side_effect = mock_responses(
  {
    'x': 42, 
    'y': [1,2,3]
  })
my_mock.goo.side_effect = mock_responses(
  {
    'hello': 'world'
  }, 
  default_response='hi')
...

my_mock.foo('x') # => 42
my_mock.foo('y') # => [1,2,3]
my_mock.foo('unknown') # => None

my_mock.goo('hello') # => 'world'
my_mock.goo('ey') # => 'hi'

Hoffe das hilft jemandem!

Manu
quelle
2

Sie können auch partialvon verwenden, functoolswenn Sie eine Funktion verwenden möchten, die Parameter akzeptiert, die von Ihnen verspottete Funktion jedoch nicht. ZB so:

def mock_year(year):
    return datetime.datetime(year, 11, 28, tzinfo=timezone.utc)
@patch('django.utils.timezone.now', side_effect=partial(mock_year, year=2020))

Dies gibt einen Aufruf zurück, der keine Parameter akzeptiert (wie Djangos timezone.now ()), aber meine mock_year-Funktion.

Hernan
quelle
Vielen Dank für diese elegante Lösung. Ich möchte hinzufügen, dass wenn Ihre ursprüngliche Funktion zusätzliche Parameter enthält, diese mit Schlüsselwörtern in Ihrem Produktionscode aufgerufen werden müssen, da dieser Ansatz sonst nicht funktioniert. Sie erhalten den Fehler : got multiple values for argument.
Erik Kalkoken
1

Nur um einen anderen Weg zu zeigen:

def mock_isdir(path):
    return path in ['/var/log', '/var/log/apache2', '/var/log/tomcat']

with mock.patch('os.path.isdir') as os_path_isdir:
    os_path_isdir.side_effect = mock_isdir
Caleb
quelle
1

Sie können auch verwenden @mock.patch.object:

Nehmen wir an, ein Modul my_module.pyverwendet pandasaus einer Datenbank zu lesen und wir möchten dieses Modul testen , indem spöttisch pd.read_sql_tableMethode (die nimmt table_nameals Argument).

Sie können (innerhalb Ihres Tests) eine db_mockMethode erstellen , die abhängig vom angegebenen Argument unterschiedliche Objekte zurückgibt:

def db_mock(**kwargs):
    if kwargs['table_name'] == 'table_1':
        # return some DataFrame
    elif kwargs['table_name'] == 'table_2':
        # return some other DataFrame

In Ihrer Testfunktion tun Sie dann:

import my_module as my_module_imported

@mock.patch.object(my_module_imported.pd, "read_sql_table", new_callable=lambda: db_mock)
def test_my_module(mock_read_sql_table):
    # You can now test any methods from `my_module`, e.g. `foo` and any call this 
    # method does to `read_sql_table` will be mocked by `db_mock`, e.g.
    ret = my_module_imported.foo(table_name='table_1')
    # `ret` is some DataFrame returned by `db_mock`
Tomasz Bartkowiak
quelle
1

Wenn Sie "einen festen Wert zurückgeben möchten, wenn der Eingabeparameter einen bestimmten Wert hat" , benötigen Sie möglicherweise nicht einmal einen Mock und können einen dictzusammen mit seiner getMethode verwenden:

foo = {'input1': 'value1', 'input2': 'value2'}.get

foo('input1')  # value1
foo('input2')  # value2

Dies funktioniert gut, wenn die Ausgabe Ihrer Fälschung eine Zuordnung der Eingabe ist. Wenn es sich um eine Funktion der Eingabe handelt, würde ich vorschlagen, sie side_effectgemäß der Antwort von Amber zu verwenden .

Sie können auch eine Kombination aus beidem verwenden , wenn Sie erhalten wollen Mock‚s - Fähigkeiten ( assert_called_once, call_countusw.):

self.mock.side_effect = {'input1': 'value1', 'input2': 'value2'}.get
Arseniy Panfilov
quelle
Das ist sehr klug.
Emyller
-6

Ich weiß, es ist eine ziemlich alte Frage, könnte als Verbesserung mit Python Lamdba helfen

self.some_service.foo.side_effect = lambda *args:"Called with 42" \
            if args[0] == 42 \
            else "Called with 42" if args[0] == 43 \
            else "Called with 43" if args[0] == 43 \
            else "Called with 45" if args[0] == 45 \
            else "Called with 49" if args[0] == 49 else None
Anfänger
quelle