Mockowanie funkcji Pythona na podstawie argumentów wejściowych

150

Od jakiegoś czasu używamy Mocka dla Pythona.

Teraz mamy sytuację, w której chcemy mockować funkcję

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

Zwykle sposobem na kpienie z tego byłoby (zakładając, że foo jest częścią obiektu)

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

Nawet jeśli kilka razy wywołam foo (), mogę użyć

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

Teraz mam do czynienia z sytuacją, w której chcę zwrócić stałą wartość, gdy parametr wejściowy ma określoną wartość. Więc jeśli powiedzmy, że „my_param” jest równe „coś”, chcę zwrócić „my_cool_mock”

Wydaje się, że jest to dostępne w mockito dla Pythona

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

Szukałem, jak osiągnąć to samo z Mockiem bez powodzenia?

Jakieś pomysły?

Juan Antonio Gomez Moriano
źródło
2
Może być to odpowiedź pomoże - stackoverflow.com/a/7665754/234606
naiquevin
@naiquevin To doskonale rozwiązuje problem kolego, dzięki!
Juan Antonio Gomez Moriano
Nie miałem pojęcia, że ​​możesz użyć Mocktio z Pythonem, +1 do tego!
Ben
Jeśli Twój projekt używa pytest, w takim celu możesz chcieć wykorzystać monkeypatch. Monkeypatch jest bardziej dla „zastąpienia tej funkcji ze względu na testowanie”, podczas gdy Mock jest tym, czego używasz, gdy chcesz również sprawdzić mock_callslub stwierdzić, z czym została wywołana i tak dalej. Jest miejsce na jedno i drugie i często używam obu w różnych momentach w danym pliku testowym.
driftcatcher
Możliwy duplikat obiektu Python Mock z wielokrotnie wywoływaną metodą
Melebius

Odpowiedzi:

187

Jeśli side_effectjest funkcją, to cokolwiek ta funkcja zwraca, jest tym, co wywołuje próbny powrót. side_effectFunkcja jest wywoływana z tych samych argumentów jak mock. Pozwala to dynamicznie zmieniać wartość zwracaną wywołania na podstawie danych wejściowych:

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

Bursztyn
źródło
25
Aby ułatwić odpowiedź, czy mógłbyś zmienić nazwę funkcji side_effect na inną? (wiem, wiem, to jest dość proste, ale poprawia czytelność fakt, że nazwa funkcji i param name różnią :)
Juan Antonio Gomez Moriano
6
@JuanAntonioGomezMoriano Mógłbym, ale w tym przypadku po prostu cytuję dokumentację, więc trochę brzydzę się edytować cytat, jeśli nie jest specjalnie uszkodzony.
Amber
i żeby być pedantycznym przez te wszystkie lata, ale side effectjest to dokładny termin: en.wikipedia.org/wiki/Side_effect_(computer_science)
lsh
7
@Ish nie narzekają na nazwę CallableMixin.side_effect, ale oddzielna funkcja zdefiniowana w przykładzie ma taką samą nazwę.
OrangeDog
48

Jak wskazano w obiekcie Python Mock z metodą wywoływaną wielokrotnie

Rozwiązaniem jest napisanie własnego efektu ubocznego

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

To załatwia sprawę

Juan Antonio Gomez Moriano
źródło
2
Dzięki temu było to dla mnie jaśniejsze niż wybrana odpowiedź, dlatego dziękuję za odpowiedź na własne pytanie :)
Luca Bezerra
15

Efekt uboczny przyjmuje funkcję (która może być również funkcją lambda ), więc w prostych przypadkach możesz użyć:

m = MagicMock(side_effect=(lambda x: x+1))
Shubham Chaudhary
źródło
4

Skończyło się na tym, że szukałem „ jak mockować funkcję na podstawie argumentów wejściowych ” i ostatecznie rozwiązałem ten problem, tworząc prostą funkcję aux:

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

Teraz:

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'

Mam nadzieję, że to komuś pomoże!

Manu
źródło
2

Możesz również użyć partialfrom, functoolsjeśli chcesz użyć funkcji, która przyjmuje parametry, ale funkcja, którą mockujesz, nie. Np. W ten sposób:

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

To zwróci wywoływalny, który nie akceptuje parametrów (jak timezone.now () Django), ale moja funkcja mock_year tak.

Hernan
źródło
Dzięki za to eleganckie rozwiązanie. Dodam, że jeśli oryginalna funkcja ma dodatkowe parametry, to trzeba je wywołać ze słowami kluczowymi w kodzie produkcyjnym, inaczej takie podejście nie zadziała. Masz błąd: got multiple values for argument.
Erik Kalkoken
1

Żeby pokazać inny sposób:

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
źródło
1

Możesz również użyć @mock.patch.object:

Powiedzmy, że moduł my_module.pyużywa pandasdo odczytu z bazy danych i chcielibyśmy przetestować ten moduł metodą mockowania pd.read_sql_table(która przyjmuje table_namejako argument).

To, co możesz zrobić, to stworzyć (wewnątrz testu) db_mockmetodę, która zwraca różne obiekty w zależności od podanego argumentu:

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

W swojej funkcji testowej wykonujesz:

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
źródło
1

Jeśli chcesz „zwrócić stałą wartość, gdy parametr wejściowy ma określoną wartość” , być może nawet nie potrzebujesz makiety i możesz użyć dictwraz z jej getmetodą:

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

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

Działa to dobrze, gdy fałszywe wyjście jest mapowaniem wejścia. Gdy jest to funkcja wejścia, sugerowałbym użycie side_effectzgodnie z odpowiedzią Amber .

Można również użyć kombinacji zarówno jeśli chcesz zachować Mock„s możliwości ( assert_called_once, call_countetc):

self.mock.side_effect = {'input1': 'value1', 'input2': 'value2'}.get
Arseniy Panfilov
źródło
To bardzo sprytne.
emyller
-6

Wiem, że to dość stare pytanie, może pomóc jako ulepszenie za pomocą Pythona lamdba

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
Nowicjusz
źródło