Derisione della funzione python basata su argomenti di input


150

Abbiamo usato Mock per Python per un po '.

Ora, abbiamo una situazione in cui vogliamo deridere una funzione

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

Normalmente, il modo di deridere questo sarebbe (supponendo che foo faccia parte di un oggetto)

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

Anche se chiamo foo () un paio di volte che posso usare

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

Ora, sto affrontando una situazione in cui voglio restituire un valore fisso quando il parametro di input ha un valore particolare. Quindi, se diciamo "my_param" è uguale a "qualcosa", allora voglio restituire "my_cool_mock"

Questo sembra essere disponibile su Mockito per Python

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

Ho cercato come ottenere lo stesso con Mock senza successo?

Qualche idea?


2
Può essere questa risposta vi aiuterà - stackoverflow.com/a/7665754/234606
naiquevin

@naiquevin Questo risolve perfettamente il problema amico, grazie!
Juan Antonio Gomez Moriano,

Non avevo idea che potresti usare Mocktio con Python, +1 per quello!
Ben

Se il tuo progetto utilizza pytest, a tale scopo potresti voler sfruttare monkeypatch. Monkeypatch è più per "sostituire questa funzione per motivi di test", mentre Mock è ciò che usi quando vuoi anche controllare mock_callso fare affermazioni su ciò che è stato chiamato e così via. C'è un posto per entrambi e li uso spesso in momenti diversi in un determinato file di test.
driftcatcher

Risposte:


187

Se side_effectè una funzione, qualunque cosa restituisca tale funzione è ciò che chiama il mock return. La side_effectfunzione viene chiamata con gli stessi argomenti della simulazione. Ciò consente di variare dinamicamente il valore di ritorno della chiamata, in base all'input:

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


25
Solo per semplificare la risposta, potresti rinominare la funzione side_effect in qualcos'altro? (lo so, lo so, è piuttosto semplice, ma migliora la leggibilità del fatto che il nome della funzione e il nome del parametro sono diversi :)
Juan Antonio Gomez Moriano,

6
@JuanAntonioGomezMoriano Potrei, ma in questo caso sto solo citando direttamente la documentazione, quindi mi dispiace un po 'modificare la citazione se non è specificamente rotta.
Ambra

e solo per essere pedanti tutti questi anni dopo, ma side effectè il termine esatto: en.wikipedia.org/wiki/Side_effect_(computer_science)
lsh

7
@Ish non si lamentano del nome di CallableMixin.side_effect, ma che la funzione separata definita nell'esempio ha lo stesso nome.
OrangeDog

48

Come indicato nell'oggetto Python Mock con metodo chiamato più volte

Una soluzione è scrivere il mio side_effect

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

Questo è il trucco


2
Questo mi ha reso più chiaro della risposta selezionata, quindi grazie per aver risposto alla tua domanda :)
Luca Bezerra

15

L'effetto collaterale ha una funzione (che può anche essere una funzione lambda ), quindi per casi semplici puoi usare:

m = MagicMock(side_effect=(lambda x: x+1))

4

Sono finito qui alla ricerca di " come deridere una funzione basata su argomenti di input " e alla fine ho risolto questo creando una semplice funzione aux:

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

Adesso:

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'

Spero che questo possa aiutare qualcuno!


2

Puoi anche usare partialda functoolsse vuoi usare una funzione che accetta parametri ma la funzione che stai deridendo no. Ad esempio in questo modo:

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

Questo restituirà un callable che non accetta parametri (come il timezone.now () di Django, ma la mia funzione mock_year lo fa.


Grazie per questa soluzione elegante. Mi piace aggiungere che se la tua funzione originale ha parametri aggiuntivi, devono essere chiamati con parole chiave nel tuo codice di produzione o questo approccio non funzionerà. Si ottiene l'errore: got multiple values for argument.
Erik Kalkoken il

1

Solo per mostrare un altro modo di farlo:

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

1

Puoi anche usare @mock.patch.object:

Diciamo che un modulo my_module.pyusa pandasper leggere da un database e vorremmo testare questo modulo deridendo il pd.read_sql_tablemetodo (che prende table_namecome argomento).

Quello che puoi fare è creare (all'interno del tuo test) un db_mockmetodo che restituisca oggetti diversi a seconda dell'argomento fornito:

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

Nella funzione di test quindi fai:

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`

1

Se "vuoi restituire un valore fisso quando il parametro di input ha un valore particolare" , forse non hai nemmeno bisogno di un finto e potresti usare un dictinsieme al suo getmetodo:

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

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

Funziona bene quando l'output del tuo falso è una mappatura dell'input. Quando è una funzione di input, suggerirei di usare side_effectsecondo la risposta di Amber .

Puoi anche usare una combinazione di entrambi se vuoi preservare Mockle capacità ( assert_called_once, call_countecc.):

self.mock.side_effect = {'input1': 'value1', 'input2': 'value2'}.get

Questo è molto intelligente.
emyller,

-6

So che è una domanda piuttosto vecchia, che potrebbe aiutare a migliorare usando Python 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.