Función de burla de Python basada en argumentos de entrada

150

Hemos estado usando Mock para Python por un tiempo.

Ahora, tenemos una situación en la que queremos burlarnos de una función

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

Normalmente, la forma de burlarse de esto sería (suponiendo que foo sea parte de un objeto)

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

Incluso, si llamo a foo () un par de veces, puedo usar

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

Ahora, me enfrento a una situación en la que quiero devolver un valor fijo cuando el parámetro de entrada tiene un valor particular. Entonces, si digamos "my_param" es igual a "algo", entonces quiero devolver "my_cool_mock"

Esto parece estar disponible en mockito para python

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

¿He estado buscando cómo lograr lo mismo con Mock sin éxito?

¿Algunas ideas?

Juan Antonio Gómez Moriano
fuente
2
Puede ser que esta respuesta ayudará - stackoverflow.com/a/7665754/234606
naiquevin
@naiquevin Esto resuelve perfectamente el problema amigo, ¡gracias!
Juan Antonio Gómez Moriano
No tenía idea de que podrías usar Mocktio con Python, ¡+1 para eso!
Ben
Si su proyecto usa pytest, para tal propósito puede aprovechar monkeypatch. Monkeypatch es más para "reemplazar esta función en aras de la prueba", mientras que Mock es lo que usa cuando también desea verificar mock_callso hacer afirmaciones sobre cómo se llamó y así sucesivamente. Hay un lugar para ambos, y a menudo uso ambos en diferentes momentos en un archivo de prueba dado.
driftcatcher

Respuestas:

187

Si side_effectes una función, lo que devuelve esa función es lo que llama al retorno simulado. La side_effectfunción se llama con los mismos argumentos que el simulacro. Esto le permite variar el valor de retorno de la llamada dinámicamente, en función de la entrada:

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

Ámbar
fuente
25
Solo para facilitar la respuesta, ¿podría cambiar el nombre de la función de efecto secundario a otra cosa? (Lo sé, lo sé, es bastante simple, pero mejora la legibilidad del hecho de que el nombre de la función y el nombre del parámetro son diferentes :)
Juan Antonio Gómez Moriano
66
@JuanAntonioGomezMoriano podría, pero en este caso solo estoy citando directamente la documentación, por lo que me da un poco de odio editar la cita si no está rota específicamente.
Ámbar
y solo para ser pedante todos estos años después, pero side effectes el término exacto: en.wikipedia.org/wiki/Side_effect_(computer_science)
lsh
77
@Ish no se quejan del nombre de CallableMixin.side_effect, sino que la función separada definida en el ejemplo tiene el mismo nombre.
OrangeDog
48

Como se indica en el objeto Python Mock con el método llamado varias veces

Una solución es escribir mi propio efecto secundario

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

Eso hace el truco

Juan Antonio Gómez Moriano
fuente
2
Esto me dejó más claro que la respuesta seleccionada, así que gracias por responder tu propia pregunta :)
Luca Bezerra
15

El efecto secundario toma una función (que también puede ser una función lambda ), por lo que para casos simples puede usar:

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

Terminé aquí buscando " cómo burlarse de una función basada en argumentos de entrada " y finalmente resolví esto creando una función auxiliar simple:

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

Ahora:

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'

¡Espero que esto ayude a alguien!

Manu
fuente
2

También puede usar partialdesde functoolssi desea usar una función que toma parámetros pero la función de la que se está burlando no. Por ejemplo, así:

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

Esto devolverá un invocable que no acepta parámetros (como timezone.now () de Django), pero mi función mock_year sí.

Hernan
fuente
Gracias por esta elegante solución. Me gustaría agregar que si su función original tiene parámetros adicionales, deben llamarse con palabras clave en su código de producción o este enfoque no funcionará. Se obtiene el error: got multiple values for argument.
Erik Kalkoken
1

Solo para mostrar otra forma de hacerlo:

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

También puedes usar @mock.patch.object:

Digamos que un módulo my_module.pyusa pandaspara leer desde una base de datos y nos gustaría probar este módulo mediante un pd.read_sql_tablemétodo de burla (que toma table_namecomo argumento).

Lo que puede hacer es crear (dentro de su prueba) un db_mockmétodo que devuelva diferentes objetos según el argumento proporcionado:

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

En tu función de prueba, entonces haces:

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

Si "desea devolver un valor fijo cuando el parámetro de entrada tiene un valor particular" , tal vez ni siquiera necesita un simulacro y podría usar un dictjunto con su getmétodo:

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

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

Esto funciona bien cuando la salida de tu falso es un mapeo de entrada. Cuando es una función de entrada, sugiero usar side_effectsegún la respuesta de Amber .

También puede usar una combinación de ambos si desea preservar Mocklas capacidades ( assert_called_once, call_countetc.):

self.mock.side_effect = {'input1': 'value1', 'input2': 'value2'}.get
Arseniy Panfilov
fuente
Esto es muy inteligente.
Emyller
-6

Sé que es una pregunta bastante antigua, podría ayudar como una mejora 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
Principiante
fuente